This article covers the Redux architecture, including the introduction and usage of basic concepts, and the Principles of Redux. github.com/reduxjs
You probably don’t need Redux
To be clear, Redux is a useful architecture, but you don’t have to use it. Someone once said this sentence.
- If you don’t know if you need Redux, you don’t need it.
- You only need Redux when you have a React problem that really can’t be solved.
Simply put, if your UI layer is very simple and doesn’t have a lot of interaction, Redux is unnecessary and adds complexity.
- The way users use it is very simple
- There is no collaboration between users
- There is no heavy interaction with the server and no WebSocket
- The View layer only gets data from a single source
If:
- The way users use it is complex
- Users of different identities have different usage modes (for example, ordinary users and administrators)
- Multiple users can collaborate
- A lot of interaction with the server, or using websockets
- The View gets data from multiple sources
These are the scenarios for Redux: multiple interactions, multiple data sources.
From a component perspective, consider using Redux if your application has the following scenarios.
- The state of a component that needs to be shared
- A state needs to be available anywhere
- A component needs to change global state
- One component needs to change the state of another component
Redux, on the other hand, is just one solution to the Web architecture, and other alternatives are possible. For example, if it’s React, you can also use Recoil.
Design idea
(1) Web application is a state machine, and view and state are one-to-one corresponding. (2) All states are stored in one object.
Basic concepts and apis
store
A Store is a place where data is stored, you can think of it as a container. There can only be one Store for the entire app. Redux provides the createStore function to generate a Store.
state
The Store object contains all the data. If you want data at a point in time, take a snapshot of the Store. This collection of data at this point in time is called State. The current State can be obtained from store.getState().
Redux states that one State corresponds to one View. As long as the State is the same, the View is the same. You know what State is, you know what View is, and vice versa.
action
If the State changes, the View changes. However, the user does not touch State, only View. Therefore, the change in State must be caused by the View. An Action is a notification from the View that the State should change. Action is an object. The type attribute is required and represents the name of the Action. Other attributes can be set freely, and the community has a specification to refer to.
const action = {
type: 'ADD_TODO'.payload: 'Learn Redux'
};
Copy the code
Actions describe what is currently happening. The only way to change State is to use Action. It will ship data to the Store.
Action Creator
There are as many actions as there are messages that the View wants to send. If they were all handwritten, it would be troublesome. You can define a function to generate Action, called Action Creator.
const ADD_TODO = 'add TODO;
function addTodo(text) {
return {
type: ADD_TODO, text}};const action = addTodo('Learn Redux');
Copy the code
In the code above, the addTodo function is an Action Creator.
Think about it: the above code doesn’t subtract much code from the writing, and seems useless. So what’s the benefit of this? Using the above method, we write the action every time when we don’t need dispatch, which has the effect of reuse and reduces errors.
store.dispatch
Store.dispatch () is the only way a View can issue an Action.
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type: 'ADD_TODO'.payload: 'Learn Redux'
});
Copy the code
Reducer
When the Store receives the Action, it must give a new State for the View to change. This State calculation process is called Reducer. Reducer is a function that takes Action and the current State as parameters and returns a new State.
const reducer = function (state, action) {
// ...
return new_state;
};
Copy the code
The initial State of the entire application can be used as the default value for State. Here is a practical example.
const defaultState = 0;
const reducer = (state = defaultState, action) = > {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
returnstate; }};const state = reducer(1, {
type: 'ADD'.payload: 2
});
Copy the code
In the code above, the createStore takes the Reducer as an argument and generates a new Store. In the future, when a new Action is sent by Store. dispatch, Reducer will be automatically called to obtain the new State.
Why is this function called Reducer? Because it can be used as a parameter to the array’s Reduce method. Consider the following example, where a sequence of Action objects is an array.
const actions = [
{ type: 'ADD'.payload: 0 },
{ type: 'ADD'.payload: 1 },
{ type: 'ADD'.payload: 2}];const total = actions.reduce(reducer, 0); / / 3
Copy the code
If it helps to follow the above explanation, it can be followed. What if someone asks you what reducer is? How to accurately describe the reducer role in one sentence?
Reducer defines data update rules.
So what are combineReducers? If the reducer in the project is very large and it is very inconvenient to maintain, the reducer can be divided into sub-reducer, combineReducers is to synthesize a total reducer from the sub-reducer.
The principle of
CreateStore and combineReducers
function createStore(reducer, initialState) {
let currentState = initialState;
const getState = () = > currentState;
let listeners = [];
const subscribe = (sub) = > {
listeners.push(sub);
// Unsubscribe
return () = > {
let index = listeners.findIndex((s) = > s === sub);
if(index ! = = -1) {
listeners.splice(index, 1); }}}const dispatch = (action) = > {
const newState = reducer(currentState, action);
if(newState ! == currentState) { currentState = newState; listeners.forEach((sub) = >{ sub && sub(); }); }}return {
getState,
dispatch,
subscribe
};
}
function combineReducers(reducers){
const reducerKeys = Object.keys(reducers);
const finalReducers = {};
// Remove attributes in reducers that are not functions
reducerKeys.forEach((key) = > {
if(typeof reducers[key] === 'function') { finalReducers[key] = reducers[key]; }});return function combination(state, action) {
let hasChanged = false;
const nextState = {};
Object.keys(finalReducers).forEach((key) = > {
const reducer = finalReducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
if(nextStateForKey == null) {
throw new Error('When called with an action, the reducer returned undefined'); } nextState[key] = nextStateForKey; hasChanged = hasChanged || nextStateForKey ! == previousStateForKey; }); hasChanged = hasChanged ||Object.keys(state).length ! = =Object.keys(nextState).length;
return hasChanged ? nextState : state;
};;
}
export {
createStore,
combineReducers
};
Copy the code
Simple implementation of combineReducer
const combineReducers = reducers= > {
return (state = {}, action) = > {
return Object.keys(reducers).reduce(
(nextState, key) = > {
nextState[key] = reducers[key](state[key], action);
returnnextState; }, {}); }; }Copy the code
We have implemented our own redux, including createStore and combineReducers. Can we use it? Verify that some of our implementations are the same as real Redux?
Use in HTML
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
<style>
.con{
border: 1px solid green;
margin: 20px;
min-height: 50px;
}
</style>
</head>
<body>
<div>
<div class="con">
<button id="btnAdd">increase</button>
<button id="btnMinus">To reduce</button>
<button id="btnCancel">cancel</button>
<div>
<span id="span1">
</span>
</div>
</div>
<div class="con">
<input id="msgInput" type="text"/>
<button id="btnMsg">change</button>
<div>
<span id="msg2">
</span>
</div>
</div>
</div>
<script type="module">
import { createStore, combineReducers } from './redux.js';
function counterReducer(state, action){
switch(action.type) {
case 'ADD':
return {
counter: state.counter + Number(action.payload)
};
case 'MINUS':
return {
counter: state.counter - Number(action.payload)
};
default:}return state;
}
function messageReducer(state, action){
switch(action.type){
case 'CHANGEMSG':
return {
msg: action.payload
};
default:}return state;
}
const reducer = combineReducers({
count: counterReducer,
message: messageReducer
});
const store = createStore(reducer, {
count: {
counter: 0
},
message: {
msg: ' '}});let btnAdd = document.querySelector('#btnAdd');
let btnMinus = document.querySelector('#btnMinus');
let btnCancel = document.querySelector('#btnCancel');
let btnMsg = document.querySelector('#btnMsg');
btnAdd.onclick = function(){
store.dispatch({
type: 'ADD'.payload: 1
});
}
btnMinus.onclick = function(){
store.dispatch({
type: 'MINUS'.payload: 1
});
}
btnCancel.onclick = function(){
unsubscribe();
}
btnMsg.onclick = function(){
let input = document.querySelector('#msgInput');
if(input.value) {
store.dispatch({
type: 'CHANGEMSG'.payload: input.value }); }}const unsubscribe = store.subscribe(() = > {
renderDOM();
});
function renderDOM(){
let state = store.getState();
console.log(state);
let span1 = document.querySelector('#span1');
span1.innerHTML = state.count.counter;
let msg = document.querySelector('#msg2');
msg.innerHTML = state.message.msg;
}
renderDOM();
</script>
</body>
</html>
Copy the code
Used in React
We used React as an example to use our own Redux. In fact, we can use our own Redux in any javascript framework.
redux/index.js
import { createStore, combineReducers } from './redux';
export const countReducer = (state, action) = > {
switch (action.type) {
case 'ADD':
return {
count: state.count + action.payload
};
case 'MINUS':
return {
count: state.count - action.payload
};
case 'RESET':
return {
count: 0
};
default:
return state
}
}
export const msgReducer = (state, action) = > {
switch (action.type) {
case 'CHANGE':
return {
msg: action.payload
};
default:
return state
}
}
const reducer = combineReducers({
counter: countReducer,
message: msgReducer
});
export default createStore(reducer, {
counter: {
count: 0
},
message: {
msg: ' '}});Copy the code
components/ReduxPage.js
import React, { Component } from 'react';
import store from '.. /redux';
export default class ReduxPage extends Component {
constructor(props){
super(props);
}
addBtnClick = () = >{
store.dispatch({
type: 'ADD'.payload: 1
});
}
componentDidMount(){
store.subscribe(() = > {
this.forceUpdate();
});
}
render() {
const { count } = store.getState().counter;
return (
<div>
<div>
<span>{count}</span>
<button onClick={ this.addBtnClick} >increase</button>
</div>
</div>)}}Copy the code
components/ReduxPage2.js
import React, { Component } from 'react';
import store from '.. /redux';
export default class ReduxPage2 extends Component {
constructor(props){
super(props);
this.state = {
value: ' '
};
}
msgChange = () = >{
if (this.state.value) {
store.dispatch({
type: 'CHANGE'.payload: this.state.value
});
}
}
input = (e) = > {
if(e.target.value) {
this.setState({
value: e.target.value }); }}componentDidMount(){
store.subscribe(() = > {
this.forceUpdate();
});
}
render() {
const { value } = this.state;
const { msg } = store.getState().message;
return (
<div>
<div>
<input type="text" value={value} onInput={ this.input} / >
<button onClick={ this.msgChange} >change</button>
</div>
<span>{ msg }</span>
</div>)}}Copy the code
app.js
import logo from './logo.svg';
import './App.css';
import ReduxPage from './components/ReduxPage';
import ReduxPage2 from './components/ReduxPage2';
function App() {
return (
<div className="App">
<ReduxPage/>
<ReduxPage2/>
</div>
);
}
export default App;
Copy the code
The bindActionCreators implementation is simple and can be found at github.com/reduxjs/red…
The applyMiddleware and Compose implementations are a little more complex, and you can refer to them. I’ll explain its implementation in a future article. Github.com/reduxjs/red…