Redux
- Single Data Source
- All states is stored in an Object Tree, and the Object Tree is stored in only one store
- Easy to maintain, track and modify
- State is read-only
- The only way to change states is to use action
- Use pure function
sequenceDiagram
participant v as view
participant r as reducer
participant s as state
v->>r: dispatch(action)
r->>s: change state
s->>s: Shallow comparison and Update
s->>v: call render() to update view
-
Basic Usage
// node index.js const redux = require("redux"); // store const store = redux.createStore(reducer); // state const initState = { counter: 0, }; // subscribe store.subscribe(() => { console.log("counter", store.getState().counter); }); // reducer const reducer = (state = initState, action) => { const { type, payload } = action; switch (type) { case "INCREMENT": return { ...state, counter: state.counter + 1 }; case "DECREMENT": return { ...state, counter: state.counter + 1 }; case "ADD_NUMBER": return { ...state, counter: state.counter + payload.counter }; case "SUB_NUMBER": return { ...state, counter: state.counter - payload.counter }; default: return state; } }; // action const action1 = () => ({ type: "INCREMENT", }); const action2 = () => ({ type: "DECREMENT", }); const action3 = (num) => ({ type: "ADD_NUMBER", payload: { num, }, }); const action4 = (num) => ({ type: "SUB_NUMBER", payload: { num, }, }); // dispatch store.dispatch(action1()); store.dispatch(action1()); store.dispatch(action2()); store.dispatch(action2()); store.dispatch(action3(5)); store.dispatch(action4(12));
-
Module Redux
/** * │ index.js * │ package-lock.json * │ package.json * └─store * actionCreator.js * constants.js * index.js * reducer.js */ // store // index.js import redux from "redux"; import reducer from "./reducer.js"; const store = redux.createStore(reducer); export default store; // actionCreator.js import { ADD_NUMBER, SUB_NUMBER } from "./constants.js"; export const addAction = (num) => ({ type: ADD_NUMBER, num, }); export const subAction = (num) => ({ type: SUB_NUMBER, num, }); // reducer.js import { ADD_NUMBER, SUB_NUMBER } from "./constants.js"; const defaultState = { counter: 0, }; function reducer(state = defaultState, action) { switch (action.type) { case ADD_NUMBER: return { ...state, counter: state.counter + action.num }; case SUB_NUMBER: return { ...state, counter: state.counter - action.num }; default: return state; } } export default reducer; // index.js import store from "./store/index.js"; import { addAction, subAction } from "./store/actionCreator.js"; store.subscribe(() => { console.log(store.getState()); }); store.dispatch(addAction(10)); store.dispatch(addAction(15)); store.dispatch(addAction(10)); store.dispatch(subAction(20)); store.dispatch(subAction(3)); store.dispatch(subAction(1));
Connect(HOC)
// App.js
const mapStateToProps = (state) => ({
xxx: xxx
});
const mapDispatchToProps = (dispatch) => ({
func() {
dispatch(action())
}
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
// Basic connect.js
import React, { PureComponent } from "react";
import store from "../store";
export const connect = (mapStateToProps, mapDispatchToProps) => {
return (WrappedComponent) => {
return class extends PureComponent {
constructor(props) {
super();
this.stateStore = {
// Listen the incoming state change through own state, and call render function to update
stateStore: mapStateToProps(store.getState()),
};
}
componentDidMount() {
this.unSubscribe = store.subscribe(() => {
this.setState({
stateStore: mapStateToProps(store.getState()),
});
});
}
componentWillUnmount() {
this.unSubscribe();
}
render() {
return (
<WrappedComponent
{...this.props}
{...mapStateToProps(store.getState())}
{...mapDispatchToProps(store.dispatch)}
/>
);
}
};
};
};
props and context[^props]
// Advanced(Use Context) connect.js
// context.js
import React from "react";
const StoreContext = React.createContext();
export { StoreContext };
// connect.js
import React, { PureComponent } from "react";
import { StoreContext } from "./context";
export const connect = (mapStateToProps, mapDispatchToProps) => {
return (WrappedComponent) => {
class EnhanceComponent extends PureComponent {
constructor(props, context) {
super();
this.stateStore = {
// This.context hasn't been assigned here yet
stateStore: mapStateToProps(context.getState()),
};
}
componentDidMount() {
this.unSubscribe = this.context.subscribe(() => {
this.setState({
stateStore: mapStateToProps(this.context.getState()),
});
});
}
componentWillUnmount() {
this.unSubscribe();
}
render() {
return (
<WrappedComponent
{...this.props}
{...mapStateToProps(this.context.getState())}
{...mapDispatchToProps(this.context.dispatch)}
/>
);
}
}
EnhanceComponent.contextType = StoreContext;
return EnhanceComponent;
};
};
// index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import store from "./store";
import { StoreContext } from "./utils/context";
ReactDOM.render(
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>,
document.getElementById("root")
);
Middleware
-
redux-thunk
dispatch(action Object ==> action Function): We can make network requests in the funciton
// App.js import { getHomeMultidataAction } from "../store/home/actionCreator"; const mapDispatchToProps = (dispatch) => ({ // What we pass in here is a function, not an obejct // and we do not need to call the function manually getHomeMultidata() { dispatch(getHomeMultidataAction); }, }); // actionCreator.js import axios from "axios"; import { CHANGE_BANNER, CHANGE_RECOMMEND } from "./constants.js"; export const changeBannerAction = (banner) => ({ type: CHANGE_BANNER, banner, }); export const getHomeMultidataAction = (dispatch, getState) => { axios({ url: "http://123.207.32.32:8000/home/multidata", }).then((res) => { const { data } = res.data; // console.log(data); dispatch(changeBannerAction(data.banner.list)); }); // console.log(getState()); }; // reducer.js import { CHANGE_BANNER, CHANGE_RECOMMEND } from "./constants.js"; // home const initalHomeState = { banner: [], recommend: [], }; function homeReducer(homeInfo = initalHomeState, action) { switch (action.type) { case CHANGE_BANNER: return { ...homeInfo, banner: action.banner }; case CHANGE_RECOMMEND: return { ...homeInfo, recommend: action.recommend }; default: return homeInfo; } } export default homeReducer;
-
redux-saga
// index.js import { createStore, applyMiddleware } from "redux"; import reducer from "./reducer"; // thunk import thunkMiddleware from "redux-thunk"; // saga import createSagaMiddleware from "redux-saga"; import mySaga from "./home/saga"; const sagaMiddleware = createSagaMiddleware(); const storeEnhancer = applyMiddleware(thunkMiddleware, sagaMiddleware); const store = createStore(reducer, storeEnhancer); // generator Function sagaMiddleware.run(mySaga); export default store; // saga.js import axios from "axios"; import { all, put, takeEvery } from "redux-saga/effects"; import { FETCH_GET_MULTIDATA } from "./constants"; import { changeBannerAction, changeRecommendAction } from "./actionCreator"; function* fetchGetMultidata(action) { const { data: { data }, } = yield axios({ url: "http://123.207.32.32:8000/home/multidata", }); yield all([ put(changeBannerAction(data.banner.list)), put(changeRecommendAction(data.recommend.list)), ]); } function* mySaga() { // takeLaste: Excute the latest one // takeEvery: Every will be excuted // action.type, generator yield takeEvery(FETCH_GET_MULTIDATA, fetchGetMultidata); } export default mySaga; // actionCreator.js export const fetchGetMultidataAction = () => ({ type: FETCH_GET_MULTIDATA, }); // App.js const mapDispatchToProps = (dispatch) => ({ // Pass in an object fetchGetMultidata() { dispatch(fetchGetMultidataAction()); }, });