Redux - คู่มือฉบับย่อ
Redux เป็นคอนเทนเนอร์สถานะที่คาดเดาได้สำหรับแอป JavaScript เมื่อแอปพลิเคชันเติบโตขึ้นการจัดระเบียบและรักษาการไหลของข้อมูลจะทำได้ยาก Redux แก้ปัญหานี้ด้วยการจัดการสถานะของแอปพลิเคชันด้วยวัตถุส่วนกลางเดียวที่เรียกว่า Store หลักการพื้นฐานของ Redux ช่วยในการรักษาความสม่ำเสมอตลอดทั้งแอปพลิเคชันของคุณซึ่งทำให้การดีบักและการทดสอบง่ายขึ้น
ที่สำคัญกว่านั้นคือช่วยให้คุณสามารถแก้ไขโค้ดสดร่วมกับดีบักเกอร์ที่เดินทางข้ามเวลาได้ มีความยืดหยุ่นที่จะไปกับเลเยอร์มุมมองใด ๆ เช่น React, Angular, Vue และอื่น ๆ
หลักการของ Redux
ความสามารถในการคาดเดาของ Redux ถูกกำหนดโดยหลักการที่สำคัญที่สุดสามประการดังที่ระบุไว้ด้านล่าง -
แหล่งเดียวของความจริง
สถานะของแอปพลิเคชันทั้งหมดของคุณจะถูกเก็บไว้ในโครงสร้างวัตถุภายในที่เก็บเดียว เนื่องจากสถานะแอปพลิเคชันทั้งหมดถูกเก็บไว้ในโครงสร้างเดียวทำให้การดีบักเป็นเรื่องง่ายและพัฒนาได้เร็วขึ้น
สถานะเป็นแบบอ่านอย่างเดียว
วิธีเดียวที่จะเปลี่ยนสถานะคือการแสดงการกระทำซึ่งเป็นวัตถุที่อธิบายถึงสิ่งที่เกิดขึ้น ซึ่งหมายความว่าไม่มีใครสามารถเปลี่ยนสถานะแอปพลิเคชันของคุณได้โดยตรง
การเปลี่ยนแปลงเกิดขึ้นด้วยฟังก์ชันที่บริสุทธิ์
ในการระบุวิธีที่ต้นไม้สถานะถูกเปลี่ยนโดยการกระทำคุณต้องเขียนตัวลดค่าบริสุทธิ์ ตัวลดเป็นจุดศูนย์กลางที่มีการปรับเปลี่ยนสถานะ Reducer เป็นฟังก์ชันที่รับสถานะและการดำเนินการเป็นอาร์กิวเมนต์และส่งคืนสถานะที่อัปเดตใหม่
ก่อนติดตั้ง Redux we have to install Nodejs and NPM. ด้านล่างนี้คือคำแนะนำที่จะช่วยคุณในการติดตั้ง คุณสามารถข้ามขั้นตอนเหล่านี้ได้หากคุณติดตั้ง Nodejs และ NPM ในอุปกรณ์ของคุณแล้ว
เยี่ยมชม https://nodejs.org/ และติดตั้งไฟล์แพคเกจ
เรียกใช้โปรแกรมติดตั้งทำตามคำแนะนำและยอมรับข้อตกลงใบอนุญาต
รีสตาร์ทอุปกรณ์ของคุณเพื่อเรียกใช้
คุณสามารถตรวจสอบการติดตั้งที่สำเร็จได้โดยเปิดพรอมต์คำสั่งและพิมพ์ node -v นี่จะแสดง Node เวอร์ชันล่าสุดในระบบของคุณ
หากต้องการตรวจสอบว่าติดตั้ง npm สำเร็จหรือไม่คุณสามารถพิมพ์ npm –v ซึ่งจะคืนค่าเวอร์ชัน npm ล่าสุดให้คุณ
ในการติดตั้ง redux คุณสามารถทำตามขั้นตอนด้านล่างนี้ -
เรียกใช้คำสั่งต่อไปนี้ในพรอมต์คำสั่งของคุณเพื่อติดตั้ง Redux
npm install --save redux
ในการใช้ Redux กับแอปพลิเคชัน react คุณต้องติดตั้งการอ้างอิงเพิ่มเติมดังนี้ -
npm install --save react-redux
ในการติดตั้งเครื่องมือสำหรับนักพัฒนาสำหรับ Redux คุณต้องติดตั้งสิ่งต่อไปนี้เป็นการอ้างอิง -
เรียกใช้คำสั่งด้านล่างในพรอมต์คำสั่งของคุณเพื่อติดตั้ง Redux dev-tools
npm install --save-dev redux-devtools
หากคุณไม่ต้องการติดตั้งเครื่องมือ Redux dev และรวมเข้ากับโปรเจ็กต์ของคุณคุณสามารถติดตั้งได้ Redux DevTools Extension สำหรับ Chrome และ Firefox
ให้เราถือว่าสถานะแอปพลิเคชันของเราอธิบายโดยวัตถุธรรมดาที่เรียกว่า initialState ซึ่งมีดังต่อไปนี้ -
const initialState = {
isLoading: false,
items: [],
hasError: false
};
รหัสทุกชิ้นในแอปพลิเคชันของคุณไม่สามารถเปลี่ยนสถานะนี้ได้ ในการเปลี่ยนสถานะคุณต้องส่งการดำเนินการ
การกระทำคืออะไร?
การดำเนินการเป็นวัตถุธรรมดาที่อธิบายถึงความตั้งใจที่จะทำให้เกิดการเปลี่ยนแปลงด้วยคุณสมบัติชนิด ต้องมีคุณสมบัติ type ที่บอกประเภทของการกระทำที่กำลังดำเนินการ คำสั่งสำหรับการดำเนินการมีดังนี้ -
return {
type: 'ITEMS_REQUEST', //action type
isLoading: true //payload information
}
การดำเนินการและสถานะจะรวมกันโดยฟังก์ชันที่เรียกว่า Reducer มีการส่งการดำเนินการโดยมีเจตนาที่จะทำให้เกิดการเปลี่ยนแปลง การเปลี่ยนแปลงนี้ดำเนินการโดยตัวลด Reducer เป็นวิธีเดียวในการเปลี่ยนสถานะใน Redux ทำให้สามารถคาดเดาได้รวมศูนย์และแก้ไขข้อบกพร่องได้มากขึ้น ฟังก์ชันลดที่จัดการกับการดำเนินการ "ITEMS_REQUEST" มีดังต่อไปนี้ -
const reducer = (state = initialState, action) => { //es6 arrow function
switch (action.type) {
case 'ITEMS_REQUEST':
return Object.assign({}, state, {
isLoading: action.isLoading
})
default:
return state;
}
}
Redux มีร้านค้าเดียวที่มีสถานะแอปพลิเคชัน หากคุณต้องการแยกรหัสของคุณตามตรรกะการจัดการข้อมูลคุณควรเริ่มแยกตัวลดของคุณแทนร้านค้าใน Redux
เราจะพูดถึงวิธีที่เราสามารถแยกตัวลดและรวมเข้ากับร้านค้าได้ในบทช่วยสอนนี้
ส่วนประกอบ Redux มีดังนี้ -
Redux เป็นไปตามกระแสข้อมูลทิศทางเดียว หมายความว่าข้อมูลแอปพลิเคชันของคุณจะเป็นไปตามกระแสข้อมูลที่มีผลผูกพันทางเดียว เมื่อแอปพลิเคชันเติบโตขึ้นและมีความซับซ้อนจึงยากที่จะสร้างปัญหาซ้ำและเพิ่มคุณสมบัติใหม่หากคุณไม่สามารถควบคุมสถานะของแอปพลิเคชันของคุณได้
Redux ลดความซับซ้อนของโค้ดโดยบังคับใช้ข้อ จำกัด ว่าการอัปเดตสถานะจะเกิดขึ้นได้อย่างไรและเมื่อใด ด้วยวิธีนี้การจัดการสถานะที่อัปเดตเป็นเรื่องง่าย เราทราบแล้วเกี่ยวกับข้อ จำกัด ตามหลักการสามประการของ Redux แผนภาพต่อไปนี้จะช่วยให้คุณเข้าใจการไหลของข้อมูล Redux ได้ดีขึ้น -
การดำเนินการจะถูกส่งไปเมื่อผู้ใช้โต้ตอบกับแอปพลิเคชัน
ฟังก์ชันลดรากถูกเรียกใช้ด้วยสถานะปัจจุบันและการดำเนินการที่ส่ง ตัวลดรูทอาจแบ่งงานระหว่างฟังก์ชันตัวลดขนาดเล็กลงซึ่งจะส่งคืนสถานะใหม่ในที่สุด
ร้านค้าแจ้งมุมมองโดยเรียกใช้ฟังก์ชันเรียกกลับ
มุมมองสามารถดึงสถานะที่อัปเดตและแสดงใหม่อีกครั้ง
ร้านค้าคือต้นไม้วัตถุที่ไม่เปลี่ยนรูปใน Redux ร้านค้าคือคอนเทนเนอร์ของรัฐที่เก็บสถานะของแอปพลิเคชัน Redux สามารถมีได้เพียงร้านเดียวในแอปพลิเคชันของคุณ เมื่อใดก็ตามที่มีการสร้างร้านค้าใน Redux คุณต้องระบุตัวลด
ให้เราดูว่าเราจะสร้างร้านค้าโดยใช้ไฟล์ createStoreวิธีการจาก Redux จำเป็นต้องนำเข้าแพ็คเกจ createStore จากไลบรารี Redux ที่รองรับกระบวนการสร้างร้านค้าดังที่แสดงด้านล่าง -
import { createStore } from 'redux';
import reducer from './reducers/reducer'
const store = createStore(reducer);
ฟังก์ชัน createStore สามารถมีได้สามอาร์กิวเมนต์ ต่อไปนี้เป็นไวยากรณ์ -
createStore(reducer, [preloadedState], [enhancer])
ตัวลดคือฟังก์ชันที่ส่งคืนสถานะถัดไปของแอป preloadedState เป็นอาร์กิวเมนต์ที่เป็นทางเลือกและเป็นสถานะเริ่มต้นของแอปของคุณ ตัวเพิ่มประสิทธิภาพยังเป็นอาร์กิวเมนต์ที่เป็นทางเลือก มันจะช่วยคุณปรับปรุงร้านค้าด้วยความสามารถของบุคคลที่สาม
ร้านค้ามีสามวิธีที่สำคัญดังที่ระบุด้านล่าง -
getState
ช่วยให้คุณเรียกดูสถานะปัจจุบันของร้านค้า Redux ของคุณ
ไวยากรณ์สำหรับ getState มีดังนี้ -
store.getState()
จัดส่ง
ช่วยให้คุณสามารถส่งการดำเนินการเพื่อเปลี่ยนสถานะในแอปพลิเคชันของคุณ
ไวยากรณ์สำหรับการจัดส่งมีดังนี้ -
store.dispatch({type:'ITEMS_REQUEST'})
ติดตาม
ช่วยให้คุณลงทะเบียนการโทรกลับที่ Redux store จะโทรหาเมื่อมีการส่งการดำเนินการ ทันทีที่อัปเดตสถานะ Redux มุมมองจะแสดงผลใหม่โดยอัตโนมัติ
ไวยากรณ์สำหรับการจัดส่งมีดังนี้ -
store.subscribe(()=>{ console.log(store.getState());})
โปรดทราบว่าฟังก์ชัน subscribe ส่งคืนฟังก์ชันสำหรับการยกเลิกการสมัครรับฟัง หากต้องการยกเลิกการสมัครรับฟังเราสามารถใช้รหัสด้านล่างนี้ -
const unsubscribe = store.subscribe(()=>{console.log(store.getState());});
unsubscribe();
การดำเนินการเป็นแหล่งข้อมูลเดียวสำหรับร้านค้าตามเอกสารทางการของ Redux นำข้อมูลจากแอปพลิเคชันของคุณไปจัดเก็บ
ตามที่กล่าวไว้ก่อนหน้านี้การดำเนินการเป็นวัตถุ JavaScript ธรรมดาที่ต้องมีแอตทริบิวต์ type เพื่อระบุประเภทของการดำเนินการ มันบอกเราว่าเกิดอะไรขึ้น ควรกำหนดประเภทเป็นค่าคงที่สตริงในแอปพลิเคชันของคุณตามที่ระบุด้านล่าง -
const ITEMS_REQUEST = 'ITEMS_REQUEST';
นอกเหนือจากแอตทริบิวต์ประเภทนี้โครงสร้างของวัตถุการกระทำขึ้นอยู่กับนักพัฒนา ขอแนะนำให้เก็บวัตถุการกระทำของคุณให้เบาที่สุดและส่งผ่านเฉพาะข้อมูลที่จำเป็นเท่านั้น
ในการทำให้เกิดการเปลี่ยนแปลงใด ๆ ในร้านค้าคุณต้องส่งการดำเนินการก่อนโดยใช้ฟังก์ชัน store.dispatch () วัตถุการกระทำมีดังนี้ -
{ type: GET_ORDER_STATUS , payload: {orderId,userId } }
{ type: GET_WISHLIST_ITEMS, payload: userId }
ผู้สร้างการดำเนินการ
ผู้สร้างการกระทำคือฟังก์ชันที่ห่อหุ้มกระบวนการสร้างวัตถุการกระทำ ฟังก์ชันเหล่านี้เพียงแค่ส่งคืนวัตถุ Js ธรรมดาซึ่งเป็นการกระทำ ส่งเสริมการเขียนโค้ดที่สะอาดและช่วยให้สามารถใช้ซ้ำได้
ให้เราเรียนรู้เกี่ยวกับผู้สร้างแอ็คชั่นที่ให้คุณส่งการกระทำ ‘ITEMS_REQUEST’ที่ร้องขอข้อมูลรายการสินค้าจากเซิร์ฟเวอร์ ในขณะเดียวกันisLoading สถานะถูกทำให้เป็นจริงในตัวลดในประเภทการดำเนินการ "ITEMS_REQUEST" เพื่อระบุว่ารายการกำลังโหลดและข้อมูลยังไม่ได้รับจากเซิร์ฟเวอร์
ในขั้นต้นสถานะ isLoading เป็นเท็จในไฟล์ initialStateวัตถุสมมติว่าไม่มีอะไรโหลด เมื่อได้รับข้อมูลที่เบราว์เซอร์สถานะ isLoading จะถูกส่งกลับเป็นเท็จในประเภทการดำเนินการ "ITEMS_REQUEST_SUCCESS" ในตัวลดที่เกี่ยวข้อง สถานะนี้สามารถใช้เป็นส่วนประกอบในการตอบสนองเพื่อแสดงตัวโหลด / ข้อความบนเพจของคุณในขณะที่คำขอข้อมูลเปิดอยู่ ผู้สร้างการกระทำมีดังนี้ -
const ITEMS_REQUEST = ‘ITEMS_REQUEST’ ;
const ITEMS_REQUEST_SUCCESS = ‘ITEMS_REQUEST_SUCCESS’ ;
export function itemsRequest(bool,startIndex,endIndex) {
let payload = {
isLoading: bool,
startIndex,
endIndex
}
return {
type: ITEMS_REQUEST,
payload
}
}
export function itemsRequestSuccess(bool) {
return {
type: ITEMS_REQUEST_SUCCESS,
isLoading: bool,
}
}
ในการเรียกใช้ฟังก์ชันการจัดส่งคุณต้องส่งผ่านการดำเนินการเป็นอาร์กิวเมนต์เพื่อส่งฟังก์ชัน
dispatch(itemsRequest(true,1, 20));
dispatch(itemsRequestSuccess(false));
คุณสามารถจัดส่งการดำเนินการโดยใช้ store.dispatch () ได้โดยตรง อย่างไรก็ตามมีโอกาสมากกว่าที่คุณจะเข้าถึงด้วยวิธีการตัวช่วย react-Redux ที่เรียกว่าconnect(). คุณยังสามารถใช้bindActionCreators() วิธีการผูกผู้สร้างแอ็คชันจำนวนมากด้วยฟังก์ชันการจัดส่ง
ฟังก์ชันคือกระบวนการที่รับอินพุตที่เรียกว่าอาร์กิวเมนต์และสร้างผลลัพธ์บางอย่างที่เรียกว่าค่าส่งคืน ฟังก์ชันเรียกว่าบริสุทธิ์หากเป็นไปตามกฎต่อไปนี้ -
ฟังก์ชันจะส่งคืนผลลัพธ์เดียวกันสำหรับอาร์กิวเมนต์เดียวกัน
การประเมินผลไม่มีผลข้างเคียงกล่าวคือไม่เปลี่ยนแปลงข้อมูลอินพุต
ไม่มีการกลายพันธุ์ของตัวแปรท้องถิ่นและทั่วโลก
ไม่ได้ขึ้นอยู่กับสถานะภายนอกเหมือนตัวแปรทั่วโลก
ให้เรานำตัวอย่างของฟังก์ชันที่ส่งคืนค่าสองเท่าของค่าที่ส่งผ่านมาเป็นอินพุตของฟังก์ชัน โดยทั่วไปจะเขียนเป็น f (x) => x * 2 หากฟังก์ชันถูกเรียกด้วยค่าอาร์กิวเมนต์ 2 ผลลัพธ์จะเป็น 4, f (2) => 4
ให้เราเขียนคำจำกัดความของฟังก์ชันใน JavaScript ดังที่แสดงด้านล่าง -
const double = x => x*2; // es6 arrow function
console.log(double(2)); // 4
Here, double is a pure function.
ตามหลักการสามประการใน Redux การเปลี่ยนแปลงจะต้องทำโดยฟังก์ชันบริสุทธิ์นั่นคือตัวลดใน Redux ตอนนี้มีคำถามเกิดขึ้นว่าทำไมตัวลดต้องเป็นฟังก์ชันบริสุทธิ์
สมมติว่าคุณต้องการส่งการกระทำที่มีประเภท 'ADD_TO_CART_SUCCESS' เพื่อเพิ่มรายการในแอปพลิเคชันตะกร้าสินค้าของคุณโดยคลิกปุ่มเพิ่มลงในรถเข็น
ให้เราถือว่าตัวลดกำลังเพิ่มสินค้าลงในรถเข็นของคุณตามที่ระบุด้านล่าง -
const initialState = {
isAddedToCart: false;
}
const addToCartReducer = (state = initialState, action) => { //es6 arrow function
switch (action.type) {
case 'ADD_TO_CART_SUCCESS' :
state.isAddedToCart = !state.isAddedToCart; //original object altered
return state;
default:
return state;
}
}
export default addToCartReducer ;
ให้เราสมมติว่า isAddedToCart เป็นคุณสมบัติบนวัตถุสถานะที่ช่วยให้คุณตัดสินใจว่าจะปิดการใช้งานปุ่ม 'เพิ่มลงในรถเข็น' สำหรับสินค้านั้นเมื่อใดโดยส่งคืนค่าบูลีน ‘true or false’. สิ่งนี้ป้องกันไม่ให้ผู้ใช้เพิ่มผลิตภัณฑ์เดียวกันหลายครั้ง ตอนนี้แทนที่จะส่งคืนวัตถุใหม่เรากำลังกลายพันธุ์ isAddedToCart prop ในสถานะเหมือนข้างบน ตอนนี้ถ้าเราพยายามเพิ่มสินค้าลงในรถเข็นไม่มีอะไรเกิดขึ้น ปุ่มเพิ่มในรถเข็นจะไม่ถูกปิดใช้งาน
เหตุผลสำหรับพฤติกรรมนี้มีดังนี้ -
Redux เปรียบเทียบวัตถุเก่าและใหม่ตามตำแหน่งหน่วยความจำของทั้งสองวัตถุ คาดว่าจะมีวัตถุใหม่จากตัวลดหากมีการเปลี่ยนแปลงเกิดขึ้น และยังคาดว่าจะได้รับวัตถุเก่ากลับคืนมาหากไม่มีการเปลี่ยนแปลงเกิดขึ้น ในกรณีนี้ก็เหมือนกัน ด้วยเหตุนี้ Redux จึงสันนิษฐานว่าไม่มีอะไรเกิดขึ้น
ดังนั้นจึงจำเป็นสำหรับตัวลดที่จะเป็นฟังก์ชันบริสุทธิ์ใน Redux ต่อไปนี้เป็นวิธีการเขียนโดยไม่มีการกลายพันธุ์ -
const initialState = {
isAddedToCart: false;
}
const addToCartReducer = (state = initialState, action) => { //es6 arrow function
switch (action.type) {
case 'ADD_TO_CART_SUCCESS' :
return {
...state,
isAddedToCart: !state.isAddedToCart
}
default:
return state;
}
}
export default addToCartReducer;
Reducers เป็นฟังก์ชันที่บริสุทธิ์ใน Redux ฟังก์ชันบริสุทธิ์สามารถคาดเดาได้ ตัวลดเป็นวิธีเดียวในการเปลี่ยนสถานะใน Redux เป็นที่เดียวที่คุณสามารถเขียนตรรกะและการคำนวณได้ ฟังก์ชันลดจะยอมรับสถานะก่อนหน้าของแอปและการดำเนินการที่ถูกส่งออกคำนวณสถานะถัดไปและส่งคืนวัตถุใหม่
ไม่ควรทำบางสิ่งต่อไปนี้ภายในตัวลด -
- การกลายพันธุ์ของอาร์กิวเมนต์ของฟังก์ชัน
- การเรียก API และตรรกะการกำหนดเส้นทาง
- การเรียกใช้ฟังก์ชัน non-pure เช่น Math.random ()
ต่อไปนี้เป็นไวยากรณ์ของตัวลด -
(state,action) => newState
ให้เราดำเนินการต่อตัวอย่างการแสดงรายการสินค้าบนหน้าเว็บซึ่งกล่าวถึงในโมดูลผู้สร้างการดำเนินการ ให้เราดูด้านล่างวิธีการเขียนตัวลด
const initialState = {
isLoading: false,
items: []
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'ITEMS_REQUEST':
return Object.assign({}, state, {
isLoading: action.payload.isLoading
})
case ‘ITEMS_REQUEST_SUCCESS':
return Object.assign({}, state, {
items: state.items.concat(action.items),
isLoading: action.isLoading
})
default:
return state;
}
}
export default reducer;
ประการแรกหากคุณไม่ได้ตั้งค่าสถานะเป็น 'initialState' Redux จะเรียกตัวลดด้วยสถานะที่ไม่ได้กำหนด ในตัวอย่างโค้ดนี้ฟังก์ชัน concat () ของ JavaScript ถูกใช้ใน 'ITEMS_REQUEST_SUCCESS' ซึ่งไม่เปลี่ยนแปลงอาร์เรย์ที่มีอยู่ ส่งกลับอาร์เรย์ใหม่แทน
In this way, you can avoid mutation of the state. Never write directly to the state. In 'ITEMS_REQUEST', we have to set the state value from the action received.
It is already discussed that we can write our logic in reducer and can split it on the logical data basis. Let us see how we can split reducers and combine them together as root reducer when dealing with a large application.
Suppose, we want to design a web page where a user can access product order status and see wishlist information. We can separate the logic in different reducers files, and make them work independently. Let us assume that GET_ORDER_STATUS action is dispatched to get the status of order corresponding to some order id and user id.
/reducer/orderStatusReducer.js
import { GET_ORDER_STATUS } from ‘../constants/appConstant’;
export default function (state = {} , action) {
switch(action.type) {
case GET_ORDER_STATUS:
return { ...state, orderStatusData: action.payload.orderStatus };
default:
return state;
}
}
Similarly, assume GET_WISHLIST_ITEMS action is dispatched to get the user’s wishlist information respective of a user.
/reducer/getWishlistDataReducer.js
import { GET_WISHLIST_ITEMS } from ‘../constants/appConstant’;
export default function (state = {}, action) {
switch(action.type) {
case GET_WISHLIST_ITEMS:
return { ...state, wishlistData: action.payload.wishlistData };
default:
return state;
}
}
Now, we can combine both reducers by using Redux combineReducers utility. The combineReducers generate a function which returns an object whose values are different reducer functions. You can import all the reducers in index reducer file and combine them together as an object with their respective names.
/reducer/index.js
import { combineReducers } from ‘redux’;
import OrderStatusReducer from ‘./orderStatusReducer’;
import GetWishlistDataReducer from ‘./getWishlistDataReducer’;
const rootReducer = combineReducers ({
orderStatusReducer: OrderStatusReducer,
getWishlistDataReducer: GetWishlistDataReducer
});
export default rootReducer;
Now, you can pass this rootReducer to the createStore method as follows −
const store = createStore(rootReducer);
Redux itself is synchronous, so how the async operations like network request work with Redux? Here middlewares come handy. As discussed earlier, reducers are the place where all the execution logic is written. Reducer has nothing to do with who performs it, how much time it is taking or logging the state of the app before and after the action is dispatched.
In this case, Redux middleware function provides a medium to interact with dispatched action before they reach the reducer. Customized middleware functions can be created by writing high order functions (a function that returns another function), which wraps around some logic. Multiple middlewares can be combined together to add new functionality, and each middleware requires no knowledge of what came before and after. You can imagine middlewares somewhere between action dispatched and reducer.
Commonly, middlewares are used to deal with asynchronous actions in your app. Redux provides with API called applyMiddleware which allows us to use custom middleware as well as Redux middlewares like redux-thunk and redux-promise. It applies middlewares to store. The syntax of using applyMiddleware API is −
applyMiddleware(...middleware)
And this can be applied to store as follows −
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const store = createStore(rootReducer, applyMiddleware(thunk));
Middlewares will let you write an action dispatcher which returns a function instead of an action object. Example for the same is shown below −
function getUser() {
return function() {
return axios.get('/get_user_details');
};
}
Conditional dispatch can be written inside middleware. Each middleware receives store’s dispatch so that they can dispatch new action, and getState functions as arguments so that they can access the current state and return a function. Any return value from an inner function will be available as the value of dispatch function itself.
The following is the syntax of a middleware −
({ getState, dispatch }) => next => action
The getState function is useful to decide whether new data is to be fetched or cache result should get returned, depending upon the current state.
Let us see an example of a custom middleware logger function. It simply logs the action and new state.
import { createStore, applyMiddleware } from 'redux'
import userLogin from './reducers'
function logger({ getState }) {
return next => action => {
console.log(‘action’, action);
const returnVal = next(action);
console.log('state when action is dispatched', getState());
return returnVal;
}
}
Now apply the logger middleware to the store by writing the following line of code −
const store = createStore(userLogin , initialState=[ ] , applyMiddleware(logger));
Dispatch an action to check the action dispatched and new state using the below code −
store.dispatch({
type: 'ITEMS_REQUEST',
isLoading: true
})
Another example of middleware where you can handle when to show or hide the loader is given below. This middleware shows the loader when you are requesting any resource and hides it when resource request has been completed.
import isPromise from 'is-promise';
function loaderHandler({ dispatch }) {
return next => action => {
if (isPromise(action)) {
dispatch({ type: 'SHOW_LOADER' });
action
.then(() => dispatch({ type: 'HIDE_LOADER' }))
.catch(() => dispatch({ type: 'HIDE_LOADER' }));
}
return next(action);
};
}
const store = createStore(
userLogin , initialState = [ ] ,
applyMiddleware(loaderHandler)
);
Redux-Devtools provide us debugging platform for Redux apps. It allows us to perform time-travel debugging and live editing. Some of the features in official documentation are as follows −
It lets you inspect every state and action payload.
It lets you go back in time by “cancelling” actions.
If you change the reducer code, each “staged” action will be re-evaluated.
If the reducers throw, we can identify the error and also during which action this happened.
With persistState() store enhancer, you can persist debug sessions across page reloads.
There are two variants of Redux dev-tools as given below −
Redux DevTools − It can be installed as a package and integrated into your application as given below −
https://github.com/reduxjs/redux-devtools/blob/master/docs/Walkthrough.md#manual-integration
Redux DevTools Extension − A browser extension that implements the same developer tools for Redux is as follows −
https://github.com/zalmoxisus/redux-devtools-extension
Now let us check how we can skip actions and go back in time with the help of Redux dev tool. Following screenshots explain about the actions we have dispatched earlier to get the listing of items. Here we can see the actions dispatched in the inspector tab. On the right, you can see the Demo tab which shows you the difference in the state tree.
You will get familiar with this tool when you start using it. You can dispatch an action without writing the actual code just from this Redux plugin tool. A Dispatcher option in the last row will help you with this. Let us check the last action where items are fetched successfully.
We received an array of objects as a response from the server. All the data is available to display listing on our page. You can also track the store’s state at the same time by clicking on the state tab on the upper right side.
In the previous sections, we have learnt about time travel debugging. Let us now check how to skip one action and go back in time to analyze the state of our app. As you click on any action type, two options: ‘Jump’ and ‘Skip’ will appear.
By clicking on the skip button on a certain action type, you can skip particular action. It acts as if the action never happened. When you click on jump button on certain action type, it will take you to the state when that action occurred and skip all the remaining actions in sequence. This way you will be able to retain the state when a particular action happened. This feature is useful in debugging and finding errors in the application.
We skipped the last action, and all the listing data from background got vanished. It takes back to the time when data of the items has not arrived, and our app has no data to render on the page. It actually makes coding easy and debugging easier.
Testing Redux code is easy as we mostly write functions, and most of them are pure. So we can test it without even mocking them. Here, we are using JEST as a testing engine. It works in the node environment and does not access DOM.
We can install JEST with the code given below −
npm install --save-dev jest
With babel, you need to install babel-jest as follows −
npm install --save-dev babel-jest
And configure it to use babel-preset-env features in the .babelrc file as follows −
{
"presets": ["@babel/preset-env"]
}
And add the following script in your package.json:
{
//Some other code
"scripts": {
//code
"test": "jest",
"test:watch": "npm test -- --watch"
},
//code
}
Finally, run npm test or npm run test. Let us check how we can write test cases for action creators and reducers.
Test Cases for Action Creators
Let us assume you have action creator as shown below −
export function itemsRequestSuccess(bool) {
return {
type: ITEMS_REQUEST_SUCCESS,
isLoading: bool,
}
}
This action creator can be tested as given below −
import * as action from '../actions/actions';
import * as types from '../../constants/ActionTypes';
describe('actions', () => {
it('should create an action to check if item is loading', () => {
const isLoading = true,
const expectedAction = {
type: types.ITEMS_REQUEST_SUCCESS, isLoading
}
expect(actions.itemsRequestSuccess(isLoading)).toEqual(expectedAction)
})
})
Test Cases for Reducers
We have learnt that reducer should return a new state when action is applied. So reducer is tested on this behaviour.
Consider a reducer as given below −
const initialState = {
isLoading: false
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'ITEMS_REQUEST':
return Object.assign({}, state, {
isLoading: action.payload.isLoading
})
default:
return state;
}
}
export default reducer;
To test above reducer, we need to pass state and action to the reducer, and return a new state as shown below −
import reducer from '../../reducer/reducer'
import * as types from '../../constants/ActionTypes'
describe('reducer initial state', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual([
{
isLoading: false,
}
])
})
it('should handle ITEMS_REQUEST', () => {
expect(
reducer(
{
isLoading: false,
},
{
type: types.ITEMS_REQUEST,
payload: { isLoading: true }
}
)
).toEqual({
isLoading: true
})
})
})
If you are not familiar with writing test case, you can check the basics of JEST.
In the previous chapters, we have learnt what is Redux and how it works. Let us now check the integration of view part with Redux. You can add any view layer to Redux. We will also discuss react library and Redux.
Let us say if various react components need to display the same data in different ways without passing it as a prop to all the components from top-level component to the way down. It would be ideal to store it outside the react components. Because it helps in faster data retrieval as you need not pass data all the way down to different components.
Let us discuss how it is possible with Redux. Redux provides the react-redux package to bind react components with two utilities as given below −
- Provider
- Connect
Provider makes the store available to rest of the application. Connect function helps react component to connect to the store, responding to each change occurring in the store’s state.
Let us have a look at the root index.js file which creates store and uses a provider that enables the store to the rest of the app in a react-redux app.
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducers/reducer'
import thunk from 'redux-thunk';
import App from './components/app'
import './index.css';
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
applyMiddleware(thunk)
)
render(
<Provider store = {store}>
<App />
</Provider>,
document.getElementById('root')
)
Whenever a change occurs in a react-redux app, mapStateToProps() is called. In this function, we exactly specify which state we need to provide to our react component.
With the help of connect() function explained below, we are connecting these app’s state to react component. Connect() is a high order function which takes component as a parameter. It performs certain operations and returns a new component with correct data which we finally exported.
With the help of mapStateToProps(), we provide these store states as prop to our react component. This code can be wrapped in a container component. The motive is to separate concerns like data fetching, rendering concern and reusability.
import { connect } from 'react-redux'
import Listing from '../components/listing/Listing' //react component
import makeApiCall from '../services/services' //component to make api call
const mapStateToProps = (state) => {
return {
items: state.items,
isLoading: state.isLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: () => dispatch(makeApiCall())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Listing);
The definition of a component to make an api call in services.js file is as follows −
import axios from 'axios'
import { itemsLoading, itemsFetchDataSuccess } from '../actions/actions'
export default function makeApiCall() {
return (dispatch) => {
dispatch(itemsLoading(true));
axios.get('http://api.tvmaze.com/shows')
.then((response) => {
if (response.status !== 200) {
throw Error(response.statusText);
}
dispatch(itemsLoading(false));
return response;
})
.then((response) => dispatch(itemsFetchDataSuccess(response.data)))
};
}
mapDispatchToProps() function receives dispatch function as a parameter and returns you callback props as plain object that you pass to your react component.
Here, you can access fetchData as a prop in your react listing component, which dispatches an action to make an API call. mapDispatchToProps() is used to dispatch an action to store. In react-redux, components cannot access the store directly. The only way is to use connect().
Let us understand how the react-redux works through the below diagram −
STORE − Stores all your application state as a JavaScript object
PROVIDER − Makes stores available
CONTAINER − Get apps state & provide it as a prop to components
COMPONENT − User interacts through view component
ACTIONS − Causes a change in store, it may or may not change the state of your app
REDUCER − Only way to change app state, accept state and action, and returns updated state.
However, Redux is an independent library and can be used with any UI layer. React-redux is the official Redux, UI binding with the react. Moreover, it encourages a good react Redux app structure. React-redux internally implements performance optimization, so that component re-render occurs only when it is needed.
To sum up, Redux is not designed to write shortest and the fastest code. It is intended to provide a predictable state management container. It helps us understand when a certain state changed, or where the data came from.
Here is a small example of react and Redux application. You can also try developing small apps. Sample code for increase or decrease counter is given below −
This is the root file which is responsible for the creation of store and rendering our react app component.
/src/index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux';
import reducer from '../src/reducer/index'
import App from '../src/App'
import './index.css';
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
)
render(
<Provider store = {store}>
<App />
</Provider>, document.getElementById('root')
)
This is our root component of react. It is responsible for rendering counter container component as a child.
/src/app.js
import React, { Component } from 'react';
import './App.css';
import Counter from '../src/container/appContainer';
class App extends Component {
render() {
return (
<div className = "App">
<header className = "App-header">
<Counter/>
</header>
</div>
);
}
}
export default App;
The following is the container component which is responsible for providing Redux’s state to react component −
/container/counterContainer.js
import { connect } from 'react-redux'
import Counter from '../component/counter'
import { increment, decrement, reset } from '../actions';
const mapStateToProps = (state) => {
return {
counter: state
};
};
const mapDispatchToProps = (dispatch) => {
return {
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement()),
reset: () => dispatch(reset())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Given below is the react component responsible for view part −
/component/counter.js
import React, { Component } from 'react';
class Counter extends Component {
render() {
const {counter,increment,decrement,reset} = this.props;
return (
<div className = "App">
<div>{counter}</div>
<div>
<button onClick = {increment}>INCREMENT BY 1</button>
</div>
<div>
<button onClick = {decrement}>DECREMENT BY 1</button>
</div>
<button onClick = {reset}>RESET</button>
</div>
);
}
}
export default Counter;
The following are the action creators responsible for creating an action −
/actions/index.js
export function increment() {
return {
type: 'INCREMENT'
}
}
export function decrement() {
return {
type: 'DECREMENT'
}
}
export function reset() {
return { type: 'RESET' }
}
Below, we have shown line of code for reducer file which is responsible for updating the state in Redux.
reducer/index.js
const reducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT': return state + 1
case 'DECREMENT': return state - 1
case 'RESET' : return 0 default: return state
}
}
export default reducer;
Initially, the app looks as follows −
When I click increment two times, the output screen will be as shown below −
When we decrement it once, it shows the following screen −
And reset will take the app back to initial state which is counter value 0. This is shown below −
Let us understand what happens with Redux dev tools when the first increment action takes place −
State of the app will be moved to the time when only increment action is dispatched and rest of the actions are skipped.
We encourage to develop a small Todo App as an assignment by yourself and understand the Redux tool better.