1. Introduction to Redux
Redux is a JavaScript application state container that provides predictable state management.
Allows you to build consistent applications that run in different environments (client, server, native) and are easy to test. Not only that, but it also offers a great development experience, such as a time travel debugger that allows you to preview in real time after editing.
2. Why and when should you use redux
Someone once said that.
“If you don’t know if you need a Redux, you don’t need it.”
“Added Dan Abramov, Redux’s creator.
“You only need a Redux if you have a problem that React really can’t solve.”
If an application isn’t complex enough to manage data using React’s internal state, there’s no need to use Redux.
Because every time you write code using Redux, you have to write extra code and you may need to create more files.
So when is a good time to use redux?
- The component tree is large, and different component nodes need to share state
- A component needs to change the global state or change the state of another component
- There is more data interaction with the back-end server, and the component view depends on the data returned by the back-end
What are the benefits of using Redux?
- Redux makes changes in state predictable, since it can only be changed by dispatching an action, which is monitored, and in a development environment, with redux-Devtools, time travel, recording, replay, etc
- Redux unified state management, will make the code more regular, easy to maintain and management.
3. Redux basics
3.1 redux API
- Top-level exposed API
- createStore(reducer, [preloadedState], [enhancer])
- combineReducers(reducers)
- applyMiddleware(… middlewares)
- bindActionCreators(actionCreators, dispatch)
- compose(… functions)
- Store API
- getState()
- dispatch(action)
- subscribe(listener)
- replaceReducer(nextReducer)
3.1.1 createStore
The createStore function is used to create a store instance. When creating a store, the first parameter is passed to the reducer function. The store stores the state data, and the reducer function is passed to the reducer that defines the rules to modify state.
3.1.2 store. GetState
Store.getsate () returns the state data inside the store. The state data cannot be accessed directly from the outside and must be obtained through the getState method
3.1.3 store. Dispatch
Store.dispatch () dispatches an Action. An action is an object that contains at least one type field. For example: {type: ‘add’}, can contain other data except the type field. Type is used to match the modification rules in the reducer, and other data is used to update the state.
To modify data in state, you must go through Dispatch
3.1.4 store. The subscribe
Subscribe is used to subscribe to changes in the store. The listening function is executed when the dispatch is called
3.2 redux flow
Redux data streams in conjunction with React:
- Called in the React component
dispatch(action)
And distribute oneaction
.action
Is a common object that describes what happens. For example:{ type: 'add', value: 1 }
, this action object can be interpreted as “increment the value by 1” store
Get to theaction
After that, the build is invokedstore
The incoming toreducer
Function, and pass the currentstate
和action
reducer
Get to thestate
和action
After, will passaction.type
Match to modifystate
And then modify and return the new onestate
- Store saves the new state, and then all subscriptions
store.subscribe()
All listeners are called and passedstore.getState()
Get the new state and rerender the component.
4. Use redux
4.1 reducer is introduced
When creating a store, you need to pass in a Reducer function.
reducer = (state, action) => {}
The Reducer (also called reducing function) function takes two arguments:
-
State: The result of the previous accumulation and the value currently being accumulated
-
Action: An object that describes what happens, usually with a type field. For example: {type: ‘ADD’}
A new cumulative result (state) is returned.
Reducers specifies how changes in the application state are sent to the Store in response to actions. Remember that actions only describe the fact that something has happened, not how the application updates state.
The reducer is a pure function, that is, the return value does not change if the received parameters are unchanged.
Never do these operations in Reducer:
- Modify incoming parameters;
- Perform operations that have side effects, such as API requests and route jumps;
- Calls to impure functions such as
Date.now()
或Math.random()
.
The reducer must be kept pure. As long as the parameters passed in are the same, the next calculated state returned must be the same. No special cases, no side effects, no API requests, no variable changes, just do the calculation.
4.2 Creating a Store
Install redux: NPM install redux or Yarn add redux
src/store/index.js
import { createStore } from 'redux';
Define the reducer function
const reducer = (state = 0, action) = > {
switch (action.type) {
case 'add':
return state + 1;
case 'minus':
return state - 1;
default:
returnstate; }};// Use createStore and pass in the Reducer function to generate a store
const store = createStore(reducer);
export default store;
Copy the code
4.3 use the store
src/pages/ReduxPage.js
import React, { Component } from 'react';
import store from '.. /store';
export default class ReduxPage extends Component {
componentDidMount() {
// So far, the page does not automatically update when the state is changed by dispath an action
// Update the interface in this way for the time being, subscribe to store changes, and then force the page to be updated without using other libraries
this.unsubscribe = store.subscribe(() = > {
this.forceUpdate();
});
}
add = () = > {
store.dispatch({ type: 'ADD' });
};
minus = () = > {
store.dispatch({ type: 'MINUS' });
};
render() {
return (
<div>
ReduxPage
<div>count: {store.getState()}</div>
<button onClick={this.add}>add</button>
<button onClick={this.minus}>minus</button>
<button onClick={()= > this.unsubscribe()}>unsubscribe</button>
</div>); }}Copy the code
src/App.js
import React from 'react';
import ReduxPage from './page/ReduxPage';
function App() {
return (
<div className="App">
<ReduxPage />
</div>
);
}
export default App;
Copy the code
At this point, you can easily use Redux for state management
5. The story
5.1 createStore implementation
Create redux and expose the createStore method
src/redux/index.js
import createStore from './createStore'
export {
createStore
}
Copy the code
5.1.1 implementation createStore
CreateStore creates a Store instance that contains getState, SUBSCRIBE, Dispatch, and other methods
export default function createStore(reducer) {
function getSate() {}
function subscribe() {}
function dispatch() {}
return {
getSate,
subscribe,
dispatch,
}
}
Copy the code
5.1.2 Implementing the getState method
GetState simply returns the current equal state
export default function createStore(reducer) {
// define currentState to save the currentState
let currentState;
function getSate() {
// When getState is called, the current state is returned
return currentState;
}
function subscribe() {}
function dispatch() {}
return {
getSate,
subscribe,
dispatch,
}
}
Copy the code
5.1.3 Implement the subscribe method
Subscribe, which saves the subscribed callback functions in the callback array
export default function createStore(reducer) {
let currentState;
// Define currentListeners to hold the callback functions of the subscription
let lisenters = [];
function getSate() {
return currentState;
}
function subscribe(listener) {
// Save the callback function in the callback function array
lisenters.push(listener)
// Return an unsubscribed function
return function unsubscribe() {
const index = lisenters.indexOf(lisenter);
lisenters.splice(index, 1); }}function dispatch() {}
return {
getSate,
subscribe,
dispatch,
}
}
Copy the code
5.1.4 Implement the Dispatch method
Dispatch an action (dispatch(action)) is the only way to modify the state.
The Dispatch method calls the Reducer function passed in when the store was created, along with the current state and action.
Finally, the callback collected by the subscribe method is called in a loop.
export default function createStore(reducer) {
let currentState;
let lisenters = [];
function getSate() {
return currentState;
}
function subscribe(listener) {
lisenters.push(listener);
return function unsubscribe() {
const index = lisenters.indexOf(lisenter);
lisenters.splice(index, 1);
};
}
function dispatch(action) {
// Call the Reducer function to change the current state
currentState = reducer(currentState, action)
// Loop to call the callback function
for (let i = 0; i < lisenters.length; i++) {
const lisenter = lisenters[i];
lisenter()
}
}
return {
getSate,
subscribe,
dispatch,
}
}
Copy the code
5.1.5 Initializing the State operation
export default function createStore(reducer) {
let currentState;
let lisenters = [];
function getState() {/ * * /}
function subscribe(listener) {/ * * /}
function dispatch(action) {/ * * /}
// Execute the initial dispatch. this ensures that the entire state tree has the initial state value. In this case, the reducer function will take effect only when the initial state is defined.
dispatch({ type: '@@redux/INIT' });
return {
getState,
subscribe,
dispatch,
};
}
Copy the code
Up to here, the basic function of redux has been implemented. In addition, the above methods also need to do some parameter compatibility processing, edge case processing and so on.
Redux (SRC /redux/index.js) : redux (SRC /redux/index.js)
At this point, actions can only support js plain objects. Actions do not support passing a function (asynchronous). For asynchronous operations, passing in an action with a function value also requires middleware.
How to implement asynchronous operations?
So far, using Redux has been limited to using normal objects as actions. If you need to update state asynchronously, such as dynamically fetching data from the back end and then changing state, you can’t do that.
import React, { Component } from 'react';
import store from '.. /store';
export default class ReduxPage extends Component {
componentDidMount() {
store.subscribe(() = > {
this.forceUpdate();
});
}
add = () = > {
store.dispatch({ type: 'ADD' });
};
minus = () = > {
store.dispatch({ type: 'MINUS' });
};
// Pass action as a function to perform an asynchronous operation
asyncAdd = () = > {
store.dispatch(() = > {
setTimeout(() = > {
store.dispatch({ type: 'ADD' });
});
});
};
render() {
return (
<div>
<div>count: {store.getState()}</div>
<button onClick={this.add}>add</button>
<button onClick={this.minus}>minus</button>
<button onClick={this.asyncAdd}>asyncAdd</button>
</div>); }}Copy the code
To implement asynchronous operations, redux middleware needs to be accessed
Use of Redux middleware
Redux is a pure state manager and only supports synchronization by default. Asynchronous tasks such as latency and network requests need middleware support
The store is at the heart of Redux. In addition to the store-related content, Redux also provides a number of other apis for extending and accessing other libraries to achieve even greater functionality. ApplyMiddlewares is used to access middleware applications.
Execution of the middleware occurs between the call to Dispatch and the final reducer to modify the state.
Next, try the simplest of the Thunk and Logger middleware
src/store/index.js
import { createStore, applyMiddleware } from 'redux';
// import { createStore } from '.. /redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
const reducer = function (state = 0, action) {
switch (action.type) {
case 'ADD':
return state + 1;
case 'MINUS':
return state - 1;
default:
returnstate; }};// Use applyMiddleware to access Logger and Thunk middleware
const store = createStore(reducer, applyMiddleware(logger, thunk));
export default store;
Copy the code
The middleware is executed in a certain order. Adjusting the location of the middleware may result in different execution results.
For example, if logger is placed before thunk, you can print out the date of the asynchronous action. If logger is placed after thunk, you cannot print out the date of the asynchronous action, because in the logic of thunk, if action is a function, The function is executed, bypassing subsequent middleware. This is something you might want to know and pay attention to in actual development.
7. Redux middleware implementation
In fact, the middleware enhances and extends the original dispatch, and packages a layer of functions outside the dispatch to perform the functions of the middleware itself. After performing the functions of the middleware itself, the original Dispatch is called.
There may be one or countless pieces of middleware, and to ensure that they are all executed efficiently and in order, there needs to be a good way to string them together.
7.1 Implement the compose function
The compose function implementation strings the middleware together, executes one middleware at a time, and passes the result of the previous middleware execution to the next middleware.
Compose (f1, f2, f3)(… args) => f1(f2(f3(… args)))
The implementation of compose uses the array.prototype.reduce () method.
src/redux/compose.js
export default function compose(. funcs) {
If funcs is of length 0, return a default function
if(funcs.length === 0) {
return arg= > arg
}
Return funcs of length 1
if(funcs.length === 1) {
return funcs[0]}// Use reduce to concatenate functions
return funcs.reduce((a,b) = > (. args) = >a(b(... args)))// In order to understand this code, you must first understand the use of Reduce.
// For example, funcs is [logger, thunk], then logger(thunk(.. args))
// return funcs.reduce(function(a, b) {
// return function(... args) {
// return a(b(... args))
/ /}
// })
}
Copy the code
7.2 Implementing the applyMiddleware function
The applyMiddleware function is passed into the middleware and returns an enhancement function that enhances the createStore function.
src/redux/applyMiddleware.js
import compose from './compose';
export default function applyMiddleware(. middlewares) {
return (createStore) = > (reducer) = > {
let store = createStore(reducer);
let dispatch = () = > {
throw new Error(
'Dispatch is not allowed when the middleware is being built, and will not be applied to other middleware. '
);
};
// Provide middleware to API
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ... args) = >dispatch(action, ... args), };// Call the middleware function (build) and return a post call to the data
const chain = middlewares.map((middleware) = > middleware(middlewareAPI));
// String up the middleware array using composedispatch = compose(... chain)(store.dispatch);return {
...store,
dispatch,
};
};
}
Copy the code
Here, let Dispatch defines a function and assigns dispatch “enhanced” at the end to prevent it from being called when the middleware is being built.
src/redux/createStore.js
Modify the createStore function to add an enhancer parameter that enhances the createStore function
export default function createStore(reducer, enhancer) {
if (typeofenhancer ! = ='undefined') {
if (typeofenhancer ! = ='function') {
throw new Error('Enhancer must be a function.');
}
return enhancer(createStore)(reducer);
}
/ * * /
}
Copy the code
Enhancer is an applyMiddleware when adding store functionality to the middleware using the applyMiddleware function
7.3 Implementing middleware
There are three layers of functions in the middleware,
The first layer of functions, which are middleware builders, are passed getState and Dispatch methods. Initialization of the middleware is also performed here, making these two methods available to the middleware during execution.
The second layer of functions is primarily used to concatenate middleware
The third layer functions are the main logical code implementation of the functions to be extended by the middleware
7.3.1 redux – thunk
// When building the middleware, pass getState and Dispatch to make the middleware use these two methods
const thunk = ({ getState, dispatch }) = > {
// The second layer is a layer of packet functions generated for compose, where next is the next layer of middleware, and the last next is the original dispatch
return (next) = > {
// Main logic code of middleware
return (action) = > {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
};
};
Copy the code
8. react-redux
With redux alone, you can only subscribe to data changes, manually update the interface, and get the latest data through getState every time.
It’s a bit cumbersome to do that.
To better combine React with Redux, you need a library like React-Redux.
8.1 the react – redux API
React-redux provides two apis: Provider and Connect
API
<Provider store>
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
Provider
Make the component hierarchyconnect()
Method can get the Redux store. Normally, your root component should be nested in<Provider>
Can be used inconnect()
Methods.connect
As the name implies, this connects the Redux Store to the component. It is a high-order component (HOC) that passes in a component and returns a new component that extends the original component so that the original component can access the data in SOTRE and changes the method of data. Wiring does not change the original component class. butreturnA new component class that has been connected to the Redux Store.
8.2 the react – use redux
Change the example above to use React-redux
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from './react-redux';
import store from './store'
// Use the Provider to provide stores for future components, so that the connect() method in the component hierarchy can obtain the Redux Store
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>.document.getElementById('root'));Copy the code
src/page/ReactReduxPage.js
import React, { Component } from 'react';
import { connect } from '.. /react-redux';
class ReactReduxPage extends Component {
add = () = > {
this.props.dispatch({ type: 'ADD' });
};
minus = () = > {
this.props.dispatch({ type: 'MINUS' });
};
asyncAdd = () = > {
this.props.dispatch((dispatch) = > {
setTimeout(() = > {
dispatch({ type: 'ADD' });
}, 1000);
});
};
render() {
return (
<div>
<div>count: {this.props.count}</div>{/ *<button onClick={this.add}>add</button>* /}<button onClick={this.props.add}>add</button>
<button onClick={this.minus}>minus</button>
<button onClick={this.asyncAdd}>asyncAdd</button>
</div>); }}// mapStateToProps is used to pass the redux state to the component
// mapDispatchToProps is used to encapsulate dispatchToprops and action methods, and then pass them to the component. It is easy for the component to change state without having to write a lot of dispatchtoprops.
const mapStateToProps = (state) = > ({ count: state });
// mapDispatchToProps specifies how to use an object
const mapDispatchToProps = {
add: () = > ({ type: 'ADD'})};// mapDispatchToProps () functions to use
// const mapDispatchToProps = (dispatch) => {
// return {
// dispatch,
// add: () => dispatch({ type: 'ADD' }),
/ /};
// };
// Connect the component to the store using the connect method
// The incoming mapStateToProps and mapDispatchToProps define the state and dispatch to be passed to the component
export default connect(mapStateToProps, mapDispatchToProps)(ReactReduxPage);
Copy the code
8.3 React API: Context
Here the Context is used to pass the state in redux to the components, making it easy for the components to use the data in the redux state.
In a typical React application, data is passed from top to bottom (by parent and child) via the props attribute, but this is extremely cumbersome for certain types of attributes (e.g., locale preferences, UI themes) that are required by many components in the application. Context provides a way to share such values between components without having to explicitly pass props through the component tree.
Context is used less in daily development and more in third-party libraries. This feature is used in react-Redux.
The Context of use
// Context allows us to pass values deep into the component tree without explicitly passing them through each component.
// Create a context for the current theme (" light "is the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the following component tree.
// This value can be read by any component, no matter how deep.
// In this example, we pass "dark" as the current value.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>); }}// The middle component no longer has to specify to pass theme down.
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// contextType reads the current theme context.
// React will look up to the nearest theme Provider and use its value.
// In this example, the current theme value is "dark".
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />; }}function Button({ theme }) {
return <button style={{ backgroundColor: theme}} >Test Button</button>;
}
// You can also use Consumer to get the context
// function ThemedButton () {
// return (
//
// {value => }
//
/ /)
// }
// or use hooks, useContext()
Copy the code
React-redux API: bindActionCreators
BindActionCreators, apply with connect, mapDispatchToProps is used to bind the value (ActionCreator) corresponding to each key in the object with a dispatch call. Then pass the props to the component to make it easier for the component to change the state of the redux and reduce the number of dispatches (XXX) in the component. We’ll talk about that in Action Creator. Use: bindActionCreators(actionCreators, dispatch)
BindActionCreators, apply a value with a different ActionCreator, and turn it into a key with the same name. Each Action Creator is also wrapped with Dispatches so that they can be called directly.
The effect is something like this:
{
add: () = > ({ type: 'ADD'})};/ / into
{
add: () = > dispatch({ type: 'ADD'})}Copy the code
Parameters:
actionCreators
(Function or Object): An Action Creator, or a value is an Action Creator object.dispatch
(Function) : aStore
Instance provideddispatch
Function.
8.4.1 Action Creator
Action Creator is the function used to generate Action
Such as:
function ADD_TODO(text) {
return { type: 'ADD', text}
}
dispatch(ADD_TODO('To-do list 1')) // Dispatch ({type: 'ADD', text: '1'})
Copy the code
In the example above, ADD_TODO is used to generate Action {type: ‘ADD’, text}. ADD_TODO is an Action Creator function
An Action is a payload of information, and Action Creator is a factory for creating actions.
8.4.1 bindActionCreators implementation
{add: () => ({type: ‘add ‘})} equals add = () => dispatch({type:’ add ‘})
// bindActionCreator wraps the ActionCreator, plus the dispatch call
function bindActionCreator(actionCreator, dispatch) {
return (. args) = >dispatch(actionCreator(... args)); }export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
const boundActionCreators = {};
for (let key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch); }}return boundActionCreators;
}
Copy the code
8.4 the react – redux implementation
src/react-redux/index.js
import Provider from './Provider';
import connect from './connect';
// Expose the Provider, connect API
export { Provider, connect };
Copy the code
Create a Context for data communication
src/react-redux/Context.js
import React from 'react';
export const ReactReduxContext = React.createContext(null);
export default ReactReduxContext;
Copy the code
8.5 Provider Implementation
import React, { Component } from 'react';
import { ReactReduxContext } from './Context';
export default class Provider extends Component {
render() {
// Use context. Provider to store components
return (
<ReactReduxContext.Provider value={this.props.store}>
{this.props.children}
</ReactReduxContext.Provider>); }}Copy the code
8.6 Connect implementation
Connect takes the mapStateToProps and mapDispatchToProps arguments and returns a high-level component.
The connection uses the component of the Store and passes the value in the Store to the component
import React, { useLayoutEffect, useReducer, useContext } from 'react';
// import { bindActionCreators } from 'redux';
import { bindActionCreators } from '.. /redux';
import { ReactReduxContext } from './Context';
const connect = (mapStateToProps = (state) => state, mapDispatchToProps) = > (
WrappedComponent
) = > (props) = > {
const store = useContext(ReactReduxContext);
const { getState, dispatch, subscribe } = store;
// mapStateToProps is the first parameter passed when using connect,
MapStateToProps = props; // getState = props
// mapStateToProps Returns the props to be passed to the component
const stateProps = mapStateToProps(getState());
// Connect's second argument mapDispatchToProps can be an object or function
let dispatchProps;
if (typeof mapDispatchToProps === 'object') {
// If mapDispatchToProps is an object, use bindActionCreators to package the object into a function object that can be called directly
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
} else if (typeof mapDispatchToProps === 'function') {
// If mapDispatchToProps is a function, call the function and pass dispatch
dispatchProps = mapDispatchToProps(dispatch);
} else {
// The default is dispatch
dispatchProps = { dispatch };
}
function storeStateUpdatesReducer(state, action) {
return state + 1;
}
// Use useReducer to force updates to the page
// useReducer returns an array containing two items [state, Dispatch]. When dispatch returns a new state, the component rerenders
const [, forceComponentUpdateDispatch] = useReducer(
storeStateUpdatesReducer,
0
);
useLayoutEffect(() = > {
// Subscribe to update the data in the store to force the refresh page
const ubsubscribe = subscribe(() = > {
forceComponentUpdateDispatch({ type: 'STORE_UPDATED' });
});
return () = > {
// Unsubscribe the component
ubsubscribe && ubsubscribe();
};
}, [store]);
// Return the component that needs to be "wired" and pass the data that the component needs
return <WrappedComponent {. props} {. stateProps} {. dispatchProps} / >;
};
export default connect;
Copy the code
9. redux devtools
9.1 access to the story
import { createStore, applyMiddleware } from 'redux';
/ / introduce composeWithDevTools
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
const reducer = function (state = 0, action) {
switch (action.type) {
case 'ADD':
return state + 1;
case 'MINUS':
return state - 1;
default:
returnstate; }};const store = createStore(
reducer,
composeWithDevTools(applyMiddleware(logger, thunk))
);
export default store;
Copy the code
Redux Devtools is a Chrome extension
-
Open the Chrome debugging tool and select Redux to go to the debugging screen of Redux DevTools.
-
Select the functions to use: Log Monitor, Inspector, and Chart. The default is Inspector.
-
A list of actions executed during the recorded redux execution. Click each action to enter the detailed status of the action
-
Action You can view details about the current Action
-
State Displays details about the current State data
-
Diff Displays the state changes after the current action is executed
-
Trace Traces the code where the current action is executed
-
Test Test template
-
Start recording/Pause Recording Starts/stops recording. You can specify when recording starts and when recording stops.
-
Lock changes Locks the current recording state. Action does not change the current recording state.
-
The Dispatcher dispatches actions. You can edit actions in the Dispatcher box.
-
Slider is used to automatically play the entire recording process.
-
Import/ Export Export the current recorded JSON file, which is used for Import. After Import, the file will be restored to the saved state.
-
Settings is the redux DevTools configuration menu.
-
Switch to Log Monitor to view the logs of the entire process
-
Switch the function panel to Chart to view the status of the entire store
Read more:
Liverpoolfc.tv: redux.js.org/
Making: github.com/reduxjs/red…
Chinese documentation: www.redux.org.cn/
The React Context:zh-hans.reactjs.org/docs/contex…
Redux: You Might Not Need redux