Redux Introduction Notes

Recently started to read some knowledge about react, and inevitably came into contact with redux.

I took a look at his doc to briefly summarize.

Note that most of the original content is outdated, because react updated the hook. I will go back and study what improvements react-redux has made to the hook, and then reexplain it. However, most of the content of this blog is about native redux, which can also be safely eaten.

What is Redux?

Redux is a JavaScript state container that provides predictable state management. (If you need a WordPress framework, check out Redux Framework。)

It allows you to build consistent applications that run in different environments (client, server, native applications) and are easy to test. Not only that, it also provides a super cool development experience, such as having a时间旅行调试器可以编辑后实时预览

** Redux except and React In addition to using it together, it also supports other interface libraries **. It is small and powerful (only 2kB, including dependencies).

Redux in brief

All states in the application are stored in a single * store * in the form of an object tree. The only way to change the state is to trigger * action *, an object that describes what happened. To describe how actions change the state tree, you need to write * reducers *.

That’s it!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { createStore } from 'redux';

/**
This is a pure function of the form (state, action) = > state.
Describes how an action transforms a state into the next state.
*
The form of state depends on you, it can be primitive type, array, object,
* Even data structures generated by Immutable.js. The only point is
* When the state changes, it is necessary to return a brand new object instead of modifying the passed parameters.
*
The following example uses the'switch 'statement and string to make judgments, but you can write helper classes
* Judging by different conventions (such as method mapping), as long as they apply to your project.
*/
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}

Create a Redux store to store the state of the application.
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);

Updates can be subscribed manually or events can be bound to the view layer.
store.subscribe(() =>
console.log(store.getState())
);

The only way to change the internal state is to dispatch an action.
//actions can be serialized, journaled and stored, and later executed in playback
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1

Instead of modifying the state directly, you should make the modification into a normal object called * action *. Then write a special function to determine how each action changes the state of the application, this function is called * reducer *.

Core concepts

Redux itself is simple.

When using normal objects to describe the state of an application. For example, the state of a todo application might look like this:

1
2
3
4
5
6
7
8
9
10
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}

This object is like “Model”, except that it has no setter (modifier method). Therefore, other code cannot modify it at will, resulting in a bug that is difficult to reproduce.

To update the data in state, you need to initiate an action. An Action is a normal JavaScript object (notice, there is no magic here) used to describe what happened. Here are some examples of actions:

1
2
3
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

** The benefit of forcing action to describe all changes is that you can clearly know what happened in the application **. If something changes, you can know why. Action is like an indicator describing what happened. Eventually, ** in order to string action and state together, develop some function, this is the reducer **.

Again, without any magic, the reducer is just a function that takes state and action and returns the new state. For large applications, it is unlikely to just write one such function, so we write many small functions to manage parts of the state separately:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type = 'SET_VISIBILITY_FILTER') {
return action.filter;
} else {
return state;
}
}

function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }]);
case 'TOGGLE_TODO':
return state.map((todo, index) =>
action.index = index ?
{ text: todo.text, completed: !todo.completed } :
todo
)
default:
return state;
}
}

Then develop a reducer that calls these two reducers to manage the state of the entire application.

1
2
3
4
5
6
function todoApp(state = {}, action) {
return {
all: all (state.all, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
};
}

Three principles

Single data source, i.e. only one state

  • state is read-only, you cannot assign a value to state directly, the only way to modify state is to trigger action
  • Use pure function to perform modification. When an action is triggered, the reducer needs a pure function, that is, it cannot directly modify the state, but returns a new state object.

Basic

Action

First, let’s define action.

** Action ** is the payload that transfers data from the application (Translator’s Note: The reason why it is not called a view here is that the data may be server responses, user input or other non-view data) to the store. It is the ** only * source of stored data. Generally you will pass store.dispatch() Pass the action to the store.

The action to add a new todo task is like this:

1
2
3
4
5
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}

** Actions are essentially ordinary JavaScript objects **. We agree that a’type ‘field of type string must be used in the action to represent the action to be performed. In most cases,’ type 'will be defined as a string constant. When the application scale is getting larger, it is recommended to use a separate module or file to store actions.

Action

** Action creates function ** is the method of generating action. The concepts of “action” and “action creates function” are easy to mix together, and it is best to pay attention to the distinction when using them.

The action creation function in Redux simply returns an action:

1
2
3
4
5
6
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}

Doing so will make action creation functions easier to port and test.

In Redux, simply pass the result of the action creation function to the dispatch () method to initiate a dispatch process.

1
2
dispatch(addTodo(text))
dispatch(completeTodo(index))

Or create a ** bound action creation function ** to automatically dispatch:

1
2
const boundAddTodo = text => dispatch(addTodo(text))
const boundCompleteTodo = index => dispatch(completeTodo(index))

Then call them directly:

1
2
boundAddTodo(text);
boundCompleteTodo(index);

Reducer

** Reducers ** Specifies how to respond to changes in the application state actions Sent to the store, remember that actions only describe the fact that something happened, and do not describe how the application updates the state.

Action

Now that we have determined the structure of the state object, we can start developing the reducer. A reducer is a pure function that takes the old state and action and returns the new state.

1
(previousState, action) => newState

The reason why such a function is called a reducer is because it is the same as the function being passed in Array.prototype.reduce(reducer, ?initialValue) The callback functions in are of the same type. It is very important to keep the reducer pure. ** Never ** do these operations in a reducer:

  • Modify the incoming parameters;
  • Perform operations with side effects, such as API requests and route jumps;
    Call a non-pure function, such as Date.now () or Math.random ().

Remember that the reducer must be kept pure. ** As long as the passed parameters are the same, the next state returned from the calculation must be the same. No special cases, no side effects, no API requests, no variable modifications, just perform the calculation. **

Split and Synthesis of Reducer

As the state becomes more and more complex, our reducer will inevitably become more and more complex, so we have to find a way to split the reducer and finally combine it like a way, because we can only have one state.

For the example we started with in the Core Concepts section, Redux provides combineReducers() Utility class

1
2
3
4
5
6
7
8
import { combineReducers } from 'redux'

const todoApp = combineReducers({
visibilityFilter,
todos
})

export default todoApp

Note that the above writing is exactly equivalent to the following:

1
2
3
4
5
6
export default function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
all: all (state.all, action)
}
}

You can also give them different keys or call different functions. The following two synthetic reducer methods are exactly equivalent:

1
2
3
4
5
6
7
8
9
10
11
12
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)
}
}

combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。

Store

** Store ** is the object that connects them together. The Store has the following responsibilities:

Maintain the state of the application;

Again ** Redux application only has a single store **. When you need to split data processing logic, you should use reducer 组合 Instead of creating multiple stores.

It is very easy to create a store based on an existing reducer. In前一个章节In, we use combineReducers() Merge multiple reducers into one. Now we import it and pass createStore()

1
2
3
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)

createStore() 的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。

1
let store = createStore(todoApp, window.STATE_FROM_SERVER)

A few questions

So far, the basic concepts and usage of Redux have been explained, and we still have several problems

So far, it seems that React and Redux do not have any linkage, there is no relationship, at least there is no special care in usage.

  • Redux looks like a state management, not global state management, so why can you do global state management in react?
  • How to implement an asynchronous Action, what if I just want to perform some side effects in the Reducer after triggering the Action?

Let’s solve it one by one

With React

There is no connection between Redux and React. Redux supports React, Angular, Ember, jQuery and even pure JavaScript.

Redux does not include by default React 绑定库It needs to be installed separately.

1
npm install --save react-redux

If you don’t use npm, you can also get the latest UMD package from unpkg (including开发环境包And生产环境包). If you import a UMD package with the < script > tag, it will throw a window. ReactRedux object globally.

Container components (Smart/Container

Redux’s React binding library is based on 容器组件和展示组件相分离 Development ideas. So it is recommended to read this article first and then come back to continue learning. This idea is very important.

This article predates hooks and was the most popular development method in the class era, but when hooks appeared, the author acknowledges that hooks are a better practice, but if your version does not support hooks, this idea is still very important.

So let’s summarize the differences again:

Display ComponentsContainer Components
FunctionDescribe how to display (skeleton, style)Describe how to run (data acquisition, status update)
Direct use of ReduxNoYes
Data sourcepropsListening to Redux state
data modificationcall callback function from propsdispatch actions to Redux
Call methodManualUsually generated by React Redux

Most components should be presentational, but generally a few container components are needed to connect them to the Redux store. This and the following design brief do not mean that container components must be at the top of the component tree. If a container component becomes too complex (for example, it has a lot of nested components and passes countless callback functions), then introduce another container in the component tree, likeFAQAs mentioned in

Technically, you can use’store.subscribe () 'to write container components directly. But the reason why this is not recommended is that you cannot use the Performance optimization brought by React Redux. Therefore, instead of writing container components by hand, use the’connect () ’ method of React Redux to generate **, which will be described in detail later.

Why all components can access the state of redux

All container components have access to the Redux store, so you can listen to it manually. One way is to pass it as props to all container components. But this is too much trouble because you have to wrap the display component with’store ', just because a container component happens to be rendered in the component tree.

The recommended way is to use the specified React Redux component `` Come 魔法般的 Make the store accessible to all container components without having to explicitly pass it. Just use it when rendering the root component.

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

Middleware creates asynchronous actions, reducers with side effects

Standard practice is to use Redux Thunk 中间件To use it, we need to introduce the specialized library of’redux-thunk '. We 后面 It will introduce how middleware works in general; for now, you only need to know one important point: by using the specified middleware, the action creating function can return a function in addition to an action object. In this case, the action creating function becomes thunk

When an action creates a function and returns a function, the function is executed by Redux Thunk middleware. This function does not need to be pure; it can also have side effects, including executing asynchronous API requests. This function can also dispatch actions, just like dispatch the synchronous action defined earlier.

We can still define these special thunk action creation functions in actions.js.

actions.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import fetch from 'cross-fetch'

export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}

export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}

export const INVALIDATE_SUBREDDIT = ‘INVALIDATE_SUBREDDIT
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}

Take a look at the first thunk action creation function we wrote!
//Although the inner operation is different, you can use it like any other action creates a function:
// store.dispatch(fetchPosts('reactjs'))

export function fetchPosts(subreddit) {

Thunk middleware knows how to handle functions.
//here the dispatch method is passed to the function as parameters,
This allows it to dispatch actions itself.

return function (dispatch) {

//First dispatch: update the application state to notify
The API request has been initiated.

dispatch(requestPosts(subreddit))

The function called by thunk middleware can have a return value.
It will be passed as the return value of the dispatch method.

In this case, we return a promise waiting to be processed.
//This is not necessary for redux middleware, but it is convenient for us.

return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
Do not use catch because it will catch
//any errors in dispatch and rendering,
//Causes an'Unexpected batch number 'error.
// https://github.com/facebook/react/issues/6895
error => console.log('An error occurred.', error)
)
.then(json =>
//Can be dispatched multiple times!
//Here, use the API request result to update the state of the application.

dispatch(receivePosts(subreddit, json))
)
}
}

How did we introduce Redux Thunk middleware into the dispatch mechanism? We used applyMiddleware(), as follows:

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'

const loggerMiddleware = createLogger()

const store = createStore(
rootReducer,
applyMiddleware(
ThunkMiddleware,//allows us to dispatch () function
LoggerMiddleware//A very convenient middleware for printing action logs
)
)

store.dispatch(selectSubreddit('reactjs'))
store
.dispatch(fetchPosts('reactjs'))
.then(() => console.log(store.getState())
)

One advantage of thunk is that its results can be dispatched again:

actions.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import fetch from 'cross-fetch'

export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}

export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}

export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}

function fetchPosts(subreddit) {
return dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`http://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
}

function shouldFetchPosts(state, subreddit) {
const posts = state.postsBySubreddit[subreddit]
if (!posts) {
return true
} else if (posts.isFetching) {
return false
} else {
return posts.didInvalidate
}
}

export function fetchPostsIfNeeded(subreddit) {

//Note that this function also receives the getState () method
It lets you choose what to dispatch next.

//When the cached value is available,
Reducing network requests is very useful.

return (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
//dispatch another thunk in the thunk!
return dispatch(fetchPosts(subreddit))
} else {
Tell the calling code not to wait any longer.
return Promise.resolve()
}
}
}

This allows us to gradually develop complex asynchronous control flows while keeping the code as clean as ever.

index.js

1
2
3
4
store
.dispatch(fetchPostsIfNeeded('reactjs'))
.then(() => console.log(store.getState())
)