1

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() or Math.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:

  1. Do not modify the state directly, but return a new object
  2. 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:

  1. Call store.dispatch(action).
  2. Redux store calls the reducer function passed in.
  3. The root reducer should merge multiple sub-reducer outputs into a single state tree.
  4. 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

  1. Monitor LOGIN_REQUEST waiting to initiate action
  2. Obtain parameters from the outside and execute the request in the form of non-blocking call
  3. Monitor LOGOUT and LOGIN_ERROR waiting to be initiated
  4. If it belongs to LOGOUT , cancel the above request
  5. Both types of initiation will perform the cleanup process

authorize

Call authorize request

  • success

    1. Initiate LOGIN_SUCCESS save data
    2. Execute Api.storeItem
    3. Return token
  • 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 to Reducers finally change State

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

  1. key does not have any restrictions, it is only used for saving, and the maximum effect is to cancel monitoring
  2. model can only work on reducer and effects
  3. Only when calling app.start() , it traverses all the subscriptions in the model and executes it again.
  4. 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:

  1. The state and page logic <App/> and become an independent store, and the page logic is the reducer
  2. <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
  3. 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

  1. Click the Create Todo button to initiate an action with type = addTodo
  2. 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:

  1. Unify store and saga into a model , written in a js file
  2. Added a Subscriptions to collect actions from other sources
  3. 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'}) });
    },
  },
});

Afterward
621 声望62 粉丝

努力去做,对的坚持,静待结果