Redux introductory tutorial (1): basic usage
A year and a half ago, I wrote ” Introduction to React Example Tutorial” , which introduced the basic usage of React.
React is just an abstraction layer of DOM, not a complete solution for web applications. There are two aspects, it is not involved.
- Code structure
- Communication between components
For large and complex applications, these two aspects are precisely the most critical. Therefore, it is not possible to write large-scale applications with only React.
In order to solve this problem, Facebook proposed the concept of Flux architecture in 2014, which triggered many implementations. In 2015, Redux appeared, combining Flux with functional programming, and it became the most popular front-end architecture in a short time.
This article introduces the Redux architecture in detail. Due to the large content, the full text is divided into three parts. Today is the first part, introducing basic concepts and usage.
Zero, you may not need Redux
First of all, make it clear that Redux is a useful architecture, but it is not a must. In fact, in most cases, you don’t need it, just React is enough.
Someone once said such a sentence.
“If you don’t know if you need Redux, you don’t need it.”
Dan Abramov, the creator of Redux, added another sentence.
“Only when you encounter problems that React can’t solve, you need Redux.”
Simply put, if your UI layer is very simple and does not have a lot of interaction, Redux is unnecessary. Using it will increase the complexity.
- The way users use it is very simple
- No collaboration between users
- There is no need to interact with the server a lot, and WebSocket is not used
- The view layer (View) only obtains data from a single source
In these cases, Redux is not required.
- The user’s usage is complicated
- Users with different identities have different ways to use them (such as ordinary users and administrators)
- Collaboration between multiple users
- A lot of interaction with the server, or use of WebSocket
- View needs to get data from multiple sources
The above situations are the applicable scenarios for Redux: multiple interactions and multiple data sources.
From a component perspective, if your application has the following scenarios, you can consider using Redux.
- The state of a component needs to be shared
- A certain state needs to be available anywhere
- A component needs to change the global state
- One component needs to change the state of another component
When the above situation occurs, if you do not use Redux or other state management tools, and do not handle state reading and writing according to certain rules, the code will quickly become a mess. You need a mechanism that can query status, change status, and propagate status changes in the same place.
In short, don’t use Redux as a panacea, if your application is not that complicated, there is no need to use it. On the other hand, Redux is just a solution for the Web architecture, and other solutions are also available.
1. Preliminary knowledge
To read this article, you only need to know React. If you still know Flux, it will be better. It will be easier to understand some concepts, but it is not necessary.
Redux has very good documentation , as well as supporting small videos (the first 30 episodes , the last 30 episodes ). You can read this article first, and then go to the official materials to study in detail.
My goal is to provide a concise, easy-to-understand, comprehensive entry-level reference document.
Two, design ideas
Redux’s design philosophy is very simple, just two sentences.
(1) A web application is a state machine, and views and states correspond one-to-one.
(2) All states are stored in an object.
Please remember these two sentences, the following is a detailed explanation.
Three, basic concepts and API
3.1 Store
Store is a place to save data, you can think of it as a container. There can only be one Store for the entire application.
Redux provides
createStore
this function to generate Store.
import { createStore } from 'redux'; const store = createStore(fn);
In the above code, the
createStore
function accepts another function as a parameter and returns the newly generated Store object.
3.2 State
Store
The object contains all the data.
If you want to get the data at a certain point in time, you must generate a snapshot of the Store.
The data collection at this point in time is called State.
The State at the current moment can be obtained through
store.getState()
.
import { createStore } from 'redux'; const store = createStore(fn); const state = store.getState();
Redux stipulates that a State corresponds to a View. As long as the State is the same, the View is the same. You know the State, you know what the View is, and vice versa.
3.3 Action
The change of State will cause the change of View. However, the user can’t touch the State, but can only touch the View. Therefore, the change of State must be caused by View. Action is the notification sent by View, indicating 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 for
reference.
const action = { type: 'ADD_TODO', payload: 'Learn Redux' };
In the above code, the name of the Action is
ADD_TODO
, and the information it carries is a string
Learn Redux
.
It can be understood that Action describes what is currently happening. The only way to change State is to use Action. It will ship the data to the Store.
3.4 Action Creator
There will be as many actions as there are as many types of messages as the View sends. It would be very troublesome if they were all handwritten. You can define a function to generate Action, this function is called Action Creator.
const ADD_TODO = '添加 TODO'; function addTodo(text) { return { type: ADD_TODO, text } } const action = addTodo('Learn Redux');
In the above code, the
addTodo
function is an Action Creator.
3.5 store.dispatch()
store.dispatch()
It is the only way for View to issue Action.
import { createStore } from 'redux'; const store = createStore(fn); store.dispatch({ type: 'ADD_TODO', payload: 'Learn Redux' });
In the above code,
store.dispatch
an Action object is accepted as a parameter and sent out.
Combined with Action Creator, this code can be rewritten as follows.
store.dispatch(addTodo('Learn Redux'));
3.6 Reducer
After the Store receives the Action, it must give a new State so that the View will change. The calculation process of this State is called Reducer.
Reducer is a function that accepts Action and current State as parameters and returns a new State.
const reducer = function (state, action) { // ... return new_state; };
The initial state of the entire application can be used as the default value of State. The following is a practical example.
const defaultState = 0; const reducer = (state = defaultState, action) => { switch (action.type) { case 'ADD': return state + action.payload; default: return state; } }; const state = reducer(1, { type: 'ADD', payload: 2 });
In the above code, after the
reducer
function receives the named
ADD
Action, it returns a new State as the calculation result of the addition.
The logic of other operations (such as subtraction) can also be implemented according to different Actions.
In actual applications, the Reducer function does not need to be manually called as above, the
store.dispatch
method will trigger the automatic execution of the Reducer.
For this reason, the Store needs to know the Reducer function. The
createStore
method is to
pass the Reducer into the
method
when generating the Store
.
import { createStore } from 'redux'; const store = createStore(reducer);
In the above code, the
createStore
Reducer is accepted as a parameter to generate a new Store.
In the future, whenever
store.dispatch
a new Action is sent, the Reducer will be called automatically to get the new State.
Why is this function called Reducer?
Because it can be used as
reduce
a parameter of
an array
method.
Please look at the following example, a series of Action objects in order as an array.
const actions = [ { type: 'ADD', payload: 0 }, { type: 'ADD', payload: 1 }, { type: 'ADD', payload: 2 } ]; const total = actions.reduce(reducer, 0); // 3
In the above code, the array
actions
indicates that there are three actions in sequence, namely, add
0
, add,
1
and
add
2
.
The array
reduce
method accepts the Reducer function as a parameter, and you can get the final state directly
3
.
3.7 Pure functions
The most important feature of the Reducer function is that it is a pure function. In other words, as long as it is the same input, the same output must be obtained.
Pure function is a concept of functional programming and must comply with the following constraints.
- Do not overwrite parameters
- Cannot call system I/O API
- You cannot call
Date.now()
orMath.random()
wait for impure methods, because you will get different results each time
Since Reducer is a pure function, it can guarantee that the same State must get the same View. But because of this, the Reducer function cannot change the State, and must return a brand new object. Please refer to the following writing.
// State 是一个对象 function reducer(state, action) { return Object.assign({}, state, { thingToChange }); // 或者 return { ...state, ...newState }; } // State 是一个数组 function reducer(state, action) { return [...state, newItem]; }
It is best to set the State object as read-only. You can’t change it, the only way to get a new State is to generate a new object. The advantage of this is that at any time, the State corresponding to a View is always an unchanging object.
3.8 store.subscribe()
Store allows the use of
store.subscribe
methods to set up a monitoring function, once the State changes, it will automatically execute this function.
import { createStore } from 'redux'; const store = createStore(reducer); store.subscribe(listener);
Obviously, as long as you put the update function of the View (for React projects, it is the
render
method or
setState
method of the
component
)
listen
, the automatic rendering of the View will be realized.
store.subscribe
The method returns a function, and the monitoring can be released by calling this function.
let unsubscribe = store.subscribe(() => console.log(store.getState()) ); unsubscribe();
Fourth, the realization of Store
The previous section introduced the basic concepts involved in Redux, and you can find that Store provides three methods.
- store.getState()
- store.dispatch()
- store.subscribe()
import { createStore } from 'redux'; let { subscribe, dispatch, getState } = createStore(reducer);
createStore
The method can also accept a second parameter, which represents the initial state of State.
This is usually given by the server.
let store = createStore(todoApp, window.STATE_FROM_SERVER)
In the above code, it
window.STATE_FROM_SERVER
is the initial value of the entire application state.
Note that if this parameter is provided, it will override the default initial value of the Reducer function.
The following is
createStore
a simple implementation
of the
method, you can understand how the Store is generated.
const createStore = (reducer) => { let state; let listeners = []; const getState = () => state; const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; const subscribe = (listener) => { listeners.push(listener); return () => { listeners = listeners.filter(l => l !== listener); } }; dispatch({}); return { getState, dispatch, subscribe }; };
V. Splitting of Reducer
The Reducer function is responsible for generating State. Since the entire application has only one State object, which contains all the data, for large applications, this State must be very large, resulting in a very large Reducer function.
Please see the example below.
const chatReducer = (state = defaultState, action = {}) => { const { type, payload } = action; switch (type) { case ADD_CHAT: return Object.assign({}, state, { chatLog: state.chatLog.concat(payload) }); case CHANGE_STATUS: return Object.assign({}, state, { statusMessage: payload }); case CHANGE_USERNAME: return Object.assign({}, state, { userName: payload }); default: return state; } };
In the above code, the three actions change the three properties of State respectively.
- ADD_CHAT:
chatLog
attributes- CHANGE_STATUS:
statusMessage
attributes- CHANGE_USERNAME:
userName
attributes
There is no connection between these three properties, which suggests that we can split the Reducer function. Different functions are responsible for processing different attributes, and finally merge them into a big Reducer.
const chatReducer = (state = defaultState, action = {}) => { return { chatLog: chatLog(state.chatLog, action), statusMessage: statusMessage(state.statusMessage, action), userName: userName(state.userName, action) } };
In the above code, the Reducer function is split into three small functions, each of which is responsible for generating corresponding attributes.
With this disassembly, the Reducer is much easier to read and write. Moreover, this split is consistent with the structure of the React application: a React root component is composed of many sub-components. That is to say, the sub-component and the sub-Reducer can completely correspond to each other.
Redux provides a
combineReducers
method for the splitting of Reducer.
You only need to define each sub-Reducer function, and then use this method to synthesize them into a big Reducer.
import { combineReducers } from 'redux'; const chatReducer = combineReducers({ chatLog, statusMessage, userName }) export default todoApp;
The above code
combineReducers
merges the three sub-Reducers into one big function
through the
method.
This writing has a premise that the attribute name of State must be the same as the name of the child Reducer. If the name is different, use the following notation.
const reducer = combineReducers({ a: doSomethingWithA, b: processB, c: c }) // 等同于 function reducer(state = {}, action) { return { a: doSomethingWithA(state.a, action), b: processB(state.b, action), c: c(state.c, action) } }
In short,
combineReducers()
what is done is to generate an overall Reducer function.
This function executes the corresponding sub-Reducer according to the State key, and merges the returned result into a large State object.
The following is
combineReducer
a simple implementation.
const combineReducers = reducers => { return (state = {}, action) => { return Object.keys(reducers).reduce( (nextState, key) => { nextState[key] = reducers[key](state[key], action); return nextState; }, {} ); }; };
You can put all the sub-Reducers in one file, and then import them uniformly.
import { combineReducers } from 'redux' import * as reducers from './reducers' const reducer = combineReducers(reducers)
Six, work process
This section summarizes the workflow of Redux.
First, the user issues an Action.
store.dispatch(action);
Then, the Store automatically calls the Reducer and passes in two parameters: the current State and the received Action. The Reducer will return the new State.
let nextState = todoApp(previousState, action);
Once the State changes, the Store will call the listener function.
// 设置监听函数 store.subscribe(listener);
listener
You can
store.getState()
get the current status
through
.
If you are using React, you can trigger the re-rendering of the View at this time.
function listerner() { let newState = store.getState(); component.setState(newState); }
Seven, example: counter
Let’s look at one of the simplest examples.
const Counter = ({ value }) => ( <h1>{value}</h1> ); const render = () => { ReactDOM.render( <Counter value={store.getState()}/>, document.getElementById('root') ); }; store.subscribe(render); render();
The above is a simple counter whose only function is to
value
display the value of
the parameter
on the web page.
The store’s listening function is set to
render
cause the web page to be re-rendered every time the State changes.
Let’s add a little change to
Counter
add increment and decrement Action.
const Counter = ({ value, onIncrement, onDecrement }) => ( <div> <h1>{value}</h1> <button onClick={onIncrement}>+</button> <button onClick={onDecrement}>-</button> </div> ); const reducer = (state = 0, action) => { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }; const store = createStore(reducer); const render = () => { ReactDOM.render( <Counter value={store.getState()} onIncrement={() => store.dispatch({type: 'INCREMENT'})} onDecrement={() => store.dispatch({type: 'DECREMENT'})} />, document.getElementById('root') ); }; render(); store.subscribe(render);
The complete code can be found here .
The basic usage of Redux is introduced here, next time I will introduce its advanced usage: middleware and asynchronous operation.
(over)