Redux
Redux is a JavaScript state container that provides predictable state management. In addition to being used with React, other interface libraries are also supported. It is small and powerful (only 2kB, including dependencies).
Three Principles
Single data source
The state of the entire application is stored in an object tree, and this object tree only exists in a single store.
console.log(store.getState())
/* 输出
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/
State is read-only
The only way to change the state is to trigger an action, which is an ordinary object used to describe an event that has occurred.
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
Use pure functions to perform modifications
In order to describe how actions change the state tree, you need to write reducers.
function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
import { combineReducers, createStore } from 'redux'
let reducer = combineReducers({ visibilityFilter, todos })
let store = createStore(reducer)
Action
Action is the payload that transfers data from the application to the store. It is the only source of store data. Generally you will () The action spread through the store store.dispatch .
Action is essentially a normal JavaScript object. We agree that action must use a string type type
field to indicate the action to be executed.
// Action 创建函数
export const ADD_TODO = 'ADD_TODO';
export function addTodo(text) {
return { type: ADD_TODO, text }
}
// 发起dispatch
dispatch(addTodo(text))
Action creation functions can also be asynchronous non-pure functions.
Reducer
Reducers specify how to respond to changes in the application state of . Remember that actions only describe the fact that something has happened, and do not describe how the application updates the state.
A reducer is a pure function that receives the old state and action, and returns the new state.
(previousState, action) => newState
Never do these operations in the reducer:
- Modify the incoming parameters;
- Perform operations with side effects, such as API requests and route jumps;
- Call impure functions, such as
Date.now()
orMath.random()
.
As long as the input parameters are the same, the next state returned by the calculation must be the same. There are no special circumstances, no side effects, no API requests, no variable modification, and simple calculations are performed.
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
Notice:
- Do not modify the state directly, but return a new object
- In the case of default, the old state is returned. When encountering an unknown action, it must return to the old state.
Store
The Store has the following responsibilities:
- Maintain the state of the application;
- Provide getState() method to get state;
- Provide dispatch(action) method to update state;
- Register the listener through subscribe(listener);
- Unregister the listener through the function returned by subscribe(listener).
import { createStore } from 'redux'
import todoApp from './reducers'
const store = createStore(todoApp)
Middleware
In this type of framework, middleware refers to the code that can be embedded in the framework from receiving a request to generating a response.
It provides an extension point after the action is initiated and before it reaches the reducer. You can use Redux middleware to log records, create crash reports, call asynchronous interfaces or routes, and so on.
The most outstanding feature of middleware is that it can be combined in a chain. You can use multiple independent third-party middleware in a project.
const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware, // 允许我们 dispatch() 函数
loggerMiddleware // 一个很便捷的 middleware,用来打印 action 日志
)
)
data flow
Strict one-way data flow is the design core of the Redux architecture. This means that all data in the application follows the same life cycle, following the following 4 steps:
- Call store.dispatch(action).
- Redux store calls the reducer function passed in.
- The root reducer should merge multiple sub-reducer outputs into a single state tree.
- The Redux store saves the complete state tree returned by the root reducer.
Side Effects: asynchronous network request , local read localStorage/Cookie and other external operations
Summarize
Redux, a one-way data flow library, has obvious advantages and disadvantages
Predictability
action creation function and reducer are pure functions
state and action are simple objects
state can use immutable persistent data
The responsibilities of the whole process are very clear, and the data can be traced and traced back, which can ensure the stability of the project.
Scalability
Customize action processing through middleware , expand reducer reducer enhancer
Management trouble
Redux projects are usually divided into reducer, action, saga, component, etc., and need to switch back and forth during development
redux-saga
redux-saga is a library for managing side effects of applications (side effects, such as obtaining data asynchronously, accessing browser caches, etc.). Its goal is to make side effects management easier, more efficient to execute, simpler to test, and to handle failures. Time is easier.
redux-saga uses ES6's Generator function to make asynchronous processes easier to read, write and test.
Core term
Effect
An effect is a Plain Object JavaScript object that contains some instructions that will be executed by the saga middleware.
Use the factory function provided by redux-saga to create an effect. For example, you can use call(myfunc, 'arg1', 'arg2')
instruct the middleware to call myfunc('arg1', 'arg2')
and return the result to the generator of the yield effect.
Task
A task is like a process running in the background. In applications based on redux-saga, multiple tasks can be run at the same time. Create a task through the fork
function* saga() {
...
const task = yield fork(otherSaga, ...args)
...
}
Blocking call/non-blocking call
The blocking call means that Saga will wait for its execution result to return after the yield effect, and resume execution of the next instruction in the Generator after the result is returned.
The non-blocking call means that Saga will resume execution immediately after the yield effect.
function* saga() {
yield take(ACTION) // 阻塞: 将等待 action
yield call(ApiFn, ...args) // 阻塞: 将等待 ApiFn (如果 ApiFn 返回一个 Promise 的话)
yield call(otherSaga, ...args) // 阻塞: 将等待 otherSaga 结束
yield put(...) // 阻塞: 将同步发起 action (使用 Promise.then)
const task = yield fork(otherSaga, ...args) // 非阻塞: 将不会等待 otherSaga
yield cancel(task) // 非阻塞: 将立即恢复执行
// or
yield join(task) // 阻塞: 将等待 task 结束
}
Watcher/Worker
Refers to a way of using two separate Sagas to organize the flow of control.
- Watcher: Monitor the initiated action and
fork
a worker every time an action is received. - Worker: Process the action and end it.
function* watcher() {
while(true) {
const action = yield take(ACTION)
yield fork(worker, action.payload)
}
}
function* worker(payload) {
// ... do some stuff
}
Saga helper function
redux-saga provides some auxiliary functions and wraps some internal methods to derive tasks when some specific actions are initiated to the Store.
Let us demonstrate through common AJAX examples. Every time we click the Fetch button, we initiate an action of FETCH_REQUESTED
We want to process this action by starting a task that gets some data from the server.
First we create a task that will execute an asynchronous action:
import { call, put } from 'redux-saga/effects'
export function* fetchData(action) {
try {
// 发起请求
const data = yield call(Api.fetchUser, action.payload.url);
// 创建action
yield put({type: "FETCH_SUCCEEDED", data});
} catch (error) {
yield put({type: "FETCH_FAILED", error});
}
}
Then start the above task every time the FETCH_REQUESTED
import { takeEvery } from 'redux-saga'
function* watchFetchData() {
yield* takeEvery('FETCH_REQUESTED', fetchData)
}
There are also many auxiliary functions with different functions
- takeEvery(pattern, saga, ...args)
- takeEvery(channel, saga, ...args)
- takeLatest(pattern, saga, ..args)
- takeLatest(channel, saga, ..args)
- takeLeading(pattern, saga, ..args)
- takeLeading(channel, saga, ..args)
- throttle(ms, pattern, saga, ..args)
Declarative Effects
In redux-saga
, Sagas is implemented with Generator function. We yield pure JavaScript objects from Generator to express Saga logic. We call those objects Effect . Effect is a simple object, this object contains some information for the middleware to interpret and execute. You can think of Effect as instructions sent to middleware to perform certain operations
For example, suppose we have a Saga PRODUCTS_REQUESTED
Every time an action is matched, it will start a task to get the product list from the server.
import { takeEvery } from 'redux-saga/effects'
import Api from './path/to/api'
function* watchFetchProducts() {
yield takeEvery('PRODUCTS_REQUESTED', fetchProducts)
}
function* fetchProducts() {
const products = yield Api.fetch('/products')
console.log(products)
}
Suppose we want to test the generator above:
const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??) // 我们期望得到什么?
We want to check the first value of the result of generator yield. In our case, this value is the result Api.fetch('/products')
During the test, perform the actual service (real service) is neither feasible nor practical approach, so we must simulation (mock) Api.fetch
function. In other words, we need to replace the real function with a fake one. This fake function does not actually send AJAX requests but only checks whether Api.fetch
In fact, all we need is to ensure that the fetchProducts
task yield calls the correct function and that the function has the correct parameters.
Compared to direct asynchronous function calls in the Generator, we can only yield a function call information describing . In other words, we will simply yield an object that looks like the following:
// Effect -> 调用 Api.fetch 函数并传递 `./products` 作为参数
{
CALL: {
fn: Api.fetch,
args: ['./products']
}
}
In this case, when testing the Generator, all we need to do is to make the object after the yield a simple deepEqual
to check whether it yields the instruction we expect
For this reason, redux-saga
provides a different way to perform asynchronous calls.
import { call } from 'redux-saga/effects'
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// ...
}
Now we do not execute the asynchronous call immediately. Instead, call
creates a message describing the result. Just like in Redux, you use the action creator to create a plain text object describing the action that will be executed by the Store. call
creates a plain text object describing the function call. redux-saga
middleware ensures that the function call is executed and the generator is restored when the response is resolved.
This allows you to test Generator easily, even if it is outside of the Redux environment. Because call
is just a function that returns a plain text object.
import { call } from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// expects a call instruction
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)
There are also many auxiliary function Effect creators with different functions
- take(pattern)
- take.maybe(pattern)
- take(channel)
- take.maybe(channel)
- put(action)
- put.resolve(action)
- put(channel, action)
- call(fn, ...args)
- call([context, fn], ...args)
- call([context, fnName], ...args)
- apply(context, fn, args)
- cps(fn, ...args)
- cps([context, fn], ...args)
- fork(fn, ...args)
- fork([context, fn], ...args)
- spawn(fn, ...args)
- spawn([context, fn], ...args)
- join(task)
- join(...tasks)
- cancel(task)
- cancel(...tasks)
- cancel()
- select(selector, ...args)
- actionChannel(pattern, [buffer])
- flush(channel)
- cancelled()
- setContext(props)
- getContext(prop)
Dispatch Actions
Assuming that after each save, we want to initiate some actions to notify the Store that the data acquisition is successful
//...
function* fetchProducts(dispatch)
const products = yield call(Api.fetch, '/products')
dispatch({ type: 'PRODUCTS_RECEIVED', products })
}
It has the same shortcomings as we saw in the previous section to call functions directly from inside the Generator. If we want to test that fetchProducts
receives the AJAX response and execute dispatch, we also need to simulate the dispatch
function.
We need the same declarative solution. Just create an object to indicate to the middleware that we need to initiate some actions, and then let the middleware execute the real dispatch. In this way, we can test the generator's dispatch in the same way: only needs to check the Effect after yield and make sure it contains the correct instructions.
redux-saga provides another function put
for this purpose, this function is used to create a dispatch effect.
import { call, put } from 'redux-saga/effects'
//...
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// 创建并 yield 一个 dispatch Effect
yield put({ type: 'PRODUCTS_RECEIVED', products })
}
Now, we can test the Generator as easily as in the previous section:
import { call, put } from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// 期望一个 call 指令
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)
// 创建一个假的响应对象
const products = {}
// 期望一个 dispatch 指令
assert.deepEqual(
iterator.next(products).value,
put({ type: 'PRODUCTS_RECEIVED', products }),
"fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED', products })"
)
Now we pass the fake response object to Generator next
Outside the middleware environment, we can fully control the Generator. By simply simulating the results and restoring the Generator, we can simulate a real environment. Compared to simulating functions and spying calls, simulating data is much simpler.
Error handling
We assume that the remote reading fails for some reason, and the API function Api.fetch
returns a rejected Promise.
We hope to handle those errors PRODUCTS_REQUEST_FAILED
import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'
// ...
function* fetchProducts() {
try {
const products = yield call(Api.fetch, '/products')
yield put({ type: 'PRODUCTS_RECEIVED', products })
}
catch(error) {
yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
}
}
In order to test failure cases, we will use Generator's throw
method.
import { call, put } from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// 期望一个 call 指令
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)
// 创建一个模拟的 error 对象
const error = {}
// 期望一个 dispatch 指令
assert.deepEqual(
iterator.throw(error).value,
put({ type: 'PRODUCTS_REQUEST_FAILED', error }),
"fetchProducts should yield an Effect put({ type: 'PRODUCTS_REQUEST_FAILED', error })"
)
We pass a simulated error object to throw
, which will cause the Generator to interrupt the current execution flow and execute the catch block.
You can also make your API service return a normal value with an error flag. For example, you can capture Promise rejections and map them to an error field object.
import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'
function fetchProductsApi() {
return Api.fetch('/products')
.then(response => ({ response }))
.catch(error => ({ error }))
}
function* fetchProducts() {
const { response, error } = yield call(fetchProductsApi)
if (response)
yield put({ type: 'PRODUCTS_RECEIVED', products: response })
else
yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
}
An example of a login process
import { take, put, call, fork, cancel } from 'redux-saga/effects'
import Api from '...'
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
yield call(Api.storeItem, {token})
return token
} catch(error) {
yield put({type: 'LOGIN_ERROR', error})
} finally {
// finally 区块执行在任何类型的完成上(正常的 return, 错误, 或强制取消), 返回该 generator 是否已经被取消
if (yield cancelled()) {
// ... put special cancellation handling code here
}
}
}
function* loginFlow() {
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
// fork return a Task object
const task = yield fork(authorize, user, password)
const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
if(action.type === 'LOGOUT') yield cancel(task)
yield call(Api.clearItem('token'))
}
}
loginFlow
- Monitor
LOGIN_REQUEST
waiting to initiate action - Obtain parameters from the outside and execute the request in the form of non-blocking call
- Monitor
LOGOUT
andLOGIN_ERROR
waiting to be initiated - If it belongs to
LOGOUT
, cancel the above request - Both types of initiation will perform the cleanup process
authorize
Call authorize request
success
- Initiate
LOGIN_SUCCESS
save data - Execute Api.storeItem
- Return token
- Initiate
- Error: initiate
LOGIN_ERROR
- Add cancellation logic
Summarize
- is powerful , a variety of auxiliary functions and APIs, through which all business logic can be put into saga, elegant and powerful, and maintain the Redux
- testability , it can be different to achieve the effect of function test
- creates a complex , and the flexible and fine-grained writing method improves the barriers to writing and understanding
Dva
dva is a lightweight package based on the existing application architecture ( redux + react-router + redux-saga etc.) without introducing any new concepts, and the total code is less than 100 lines. (Inspired by elm and choo.)
Model
His core is to provide the app.model
method, used to encapsulate reducer, initialState, action, saga together
for example:
app.model({
namespace: 'products',
state: {
list: [],
loading: false,
},
subscriptions: [
function(dispatch) {
dispatch({type: 'products/query'});
},
],
effects: {
['products/query']: function*() {
yield call(delay(800));
yield put({
type: 'products/query/success',
payload: ['ant-tool', 'roof'],
});
},
},
reducers: {
['products/query'](state) {
return { ...state, loading: true, };
},
['products/query/success'](state, { payload }) {
return { ...state, loading: false, list: payload };
},
},
});
sagas/products.js
, we usually create 061697b62e9975, reducers/products.js
and actions/products.js
, and then switch back and forth between these files.
Data flow
Data changes are usually triggered by user interaction behaviors or browser behaviors (such as routing jumps, etc.). When such behaviors will change the data, an action dispatch
- If it is a synchronous behavior, it will be directly changed
Reducers
State
- If it is an asynchronous behavior (side effects), it will first trigger
Effects
and then flow toReducers
finally changeState
State
Model State indicates the status of the data, it may be values of any type .
When operating, it must be treated as immutable data (immutable data) , to ensure that each time is a new object, there is no reference relationship, so as to ensure the independence of State, easy to test and track changes.
Action
Action is a general JavaScript objects , it is the only way to change the State of . Whether it is data obtained from UI events, network callbacks, or data sources such as WebSocket, an action will eventually be called through the dispatch function to change the corresponding data. The action must have the type
indicate the specific behavior, and other fields can be customized. If you want to initiate an action, you need to use the function dispatch
dispatch({
type: 'add',
});
dispatch function
dispatching function is for trigger action function , State action is the only way to change, but it only describes a behavior, dipatch can be seen as a way to trigger this behavior, but Reducer is a description of how to change data .
dispatch({
type: 'user/add', // 如果在 model 外调用,需要添加 namespace
payload: {}, // 需要传递的信息
});
Reducer
$$ type Reducer<S, A> = (state: S, action: A) => S $$
It accepts two parameters: the result of the previously accumulated operation and the current value to be accumulated, and a new accumulated result is returned.
, the result of aggregation of reducers is the state object current model of 161697b62e9cf8. Through the value passed in the actions, the new value is obtained by calculating with the value in the current reducers. It should be noted that Reducer must be a pure function , so the same input must get the same output, and they should not produce any side effects. immutable data should be used for each calculation. The simple understanding of this feature is that each operation returns a new data (independent and pure), so the functions of hot reload and time travel can be used.
Effect
Effect is called a side effect, and it is called a side effect because it makes our function impure, and the same input does not necessarily get the same output.
In order to control the operation of side effects, the bottom layer introduced redux-sagas for asynchronous process control. Because of the use of the related concept of generator, it converts asynchronous to synchronous writing, thereby converting effects into pure functions.
Subscription
$$ ({ dispatch, history }, done) => unlistenFunction $$
Subscriptions is a source , which comes from elm. It app.start()
. The data source can be the current time, websocket connection of the server, keyboard input, geolocation change, history routing change, etc.
Subscription semantics is subscription, which is used to subscribe to a data source, and then dispatch required actions based on conditions.
import key from 'keymaster';
...
app.model({
namespace: 'count',
subscriptions: {
keyEvent({dispatch}) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
}
});
The official website is more general, in fact, its process is probably as follows
key
does not have any restrictions, it is only used for saving, and the maximum effect is to cancel monitoringmodel
can only work onreducer
andeffects
- Only when calling app.start() , it traverses all the subscriptions in the model and executes it again.
- The configured function needs to return a function that should be used to unsubscribe the data source. Call app.unmodel() execute
Dva diagram
One of the most common Web class examples: TodoList = Todo list + Add todo button
Diagram 1: React notation
According to the official guidelines of React, if there are interactions between multiple components, then the state (i.e.: data) is maintained on the minimum convention parent node of these components, which is <App/>
<TodoList/> <Todo/>
and <AddTodoBtn/>
themselves do not maintain any state. The parent node <App/> passes in props to determine its display. It is a pure function existence form, namely: Pure Component
Diagram 2: Redux notation
React is only responsible for page rendering, not page logic. The page logic can be extracted separately from it and become a store.
Compared with Figure 1, there are several obvious improvements:
- The state and page logic
<App/>
and become an independent store, and the page logic is the reducer <TodoList/>
and<AddTodoBtn/>
are Pure Component. Through the connect method, it is convenient to add a layer of wrapper to them to establish a connection with the store: can inject actions into the store through dispatch, prompt the state of the store to change, and subscribe at the same time The state of the store changes, once the state changes, the- The process of using dispatch to send actions to the store can be intercepted. Naturally, various
Middleware
can be added here to implement various custom functions.
In this way, each part performs its own duties, with lower coupling, higher reuse, and better scalability.
Diagram 3: Join Saga
- Click the Create Todo button to initiate an action with type = addTodo
- saga intercepts this action and initiates an http request. If the request is successful, it will continue to send an action of type = addTodoSucc to the reducer, prompting that the creation is successful, otherwise, send an action of type = addTodoFail.
Diagram 4: Dva notation
Dva is based on the best practice precipitation of React + Redux + Saga, and has done 3 very important things, which greatly improves the coding experience:
- Unify store and saga into a
model
, written in a js file - Added a Subscriptions to collect actions from other sources
- Model writing is very simple, similar to DSL or RoR
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
reducers: {
add(state) {
const newCurrent = state.current + 1;
return {
...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1};
},
},
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
subscriptions: {
keyboardWatcher({ dispatch }) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
},
});
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。