• Scotland team
  • Author: Dao Kuan Peng

preface

Today we bring you a piece of REdux middleware: 👏 RC-Redux-Model, which provides you ✍️ with a more comfortable way to write data state management, so that you can develop with simplicity and elegance; Internal generated action, just remember an action, can change any state value, convenient and concise, thus releasing your ⌨️ CV key ~

Source repository address: RC-redux-model, if you feel good, ask ✨

background

Everyone knows about Redux and agrees with the way it streams data (you wouldn’t use it if you didn’t), but there are pros and cons to everything.

Redux doesn’t have any “pain points” per se, because redux only supports synchronous operations by default, leaving the user to choose to handle asynchrony and not asynchrony requests. It keeps itself pure, so to speak, leaving all the dirty work to others.

So my pain point was: how do I handle asynchronous requests, for which I used Redux-Saga to solve the asynchronous problem

But using Redux + Redux-Saga, I found that it made me do more [repetitive] work (working my way up to CV engineer) because of the verbose boilerplate code in our projects.

An 🌰 : To make an asynchronous request to get user information, I need to create sagas/user.js, reducers/user.js, actions/user.js, and to unify const management, I will also have a const/user.js and then switch back and forth between these files.

Should split files be the default specification?

// const/user.js
const FETCH_USER_INFO = 'FETCH_USER_INFO'
const FETCH_USER_INFO_SUCCESS = 'FETCH_USER_INFO_SUCCESS'
Copy the code
// actions/user.js
export function fetchUserInfo(params, callback) {
  return {
    type: FETCH_USER_INFO,
    params,
    callback,
  }
}
Copy the code
// sagas/user.js
function* fetchUserInfoSaga({ params, callback }) {
  const res = yield call(fetch.callAPI, {
    actionName: FETCH_USER_INFO,
    params,
  })
  if (res.code === 0) {
    yield put({
      type: FETCH_USER_INFO_SUCCESS,
      data: res.data,
    })
    callback && callback()
  } else {
    throw res.msg
  }
}
Copy the code
// reducers/user.js
function userReducer(state, action) {
  switch (action.type) {
    case FETCH_USER_INFO_SUCCESS:
      return Immutable.set(state, 'userInfo', action.data)
  }
}
Copy the code

Yes, such boilerplate code is simply CV operation, only need a copy, modify the name, personally, this will make me not focused enough, decentralized management of const, action, saga, reducer a set of processes, need to constantly jump ideas.

And the number of files will be more, I really do not like such a tedious process, is there a good framework to help me finish all these things?

dva

The following is the introduction of DVA official website:

Data flow solutions based on Redux and Redux-Saga let you write all your actions, states, effects, reducers, etc. in one Model file and then build in react-Router and FETCH to simplify the development experience.

It’s based on redux + Redux-Saga, but as you write it, you write it all in a model file, and it does some processing for you; Second, it is a framework, not a library. Does this mean that I need to determine whether the architecture of the project is DVA before the project starts? If I want to change to dVA in the middle of the development, does it not make sense to introduce DVA?

Or if I’m just doing demos and writing small personal projects, but I want to write dVA in the same way as the data state management model, would it be cumbersome to introduce DVA?

Looking back, my starting point is: Is to solve the tedious repetitive work, store files, state type and assign the wrong question, therefore, for people like me, provides a write state management a more comfortable way of writing, in most cases compatible with the original project, only need to install this package, will introduce a set of data management solutions, to write concise and comfortable, Happy happy masturbation code, not fragrant?

And here comes the RC-Redux-Model

rc-redux-model

To be clear: the RC-Redux-Model is middleware that provides a more concise and convenient way to manage data state.

Reference dVA data flow scheme, write all actions, reducer, state in a model file, read the source code of Redux-Thunk, internal implementation of a middleware, while providing the default action, Call this action to directly modify the state of any value, convenient and concise, let you cannot help but say WC

features

  • Lightweight and concise, write data management with writedvaSo comfortable
  • The asynchronous request is handled by the user and internally supports the Call method, which can be forwarded by calling the provided method, which returns a Promise
  • referenceredux-thunkInternal implementation of independent middleware, all actions are asynchronous actions
  • Provide default action, call this action, can modify any state value, solve your repetitive writing action, reducers problem
  • built-inseamless-immutable, just turn on the configuration and make your data immutable
  • Default detection for non-standard assignments and type errors, making your data more robust

The installation

npm install --save rc-redux-model
Copy the code

Related instructions

How to define a Model and automatically register actions and reducers?

Each model must contain namespace, state, and action and reducers without being written. To enable IMMUTABLE, openSeamlessImmutable = true is required

export default {
  namespace: '[your model.namespace]'.state: {
    testA: ' '.testB: false.testC: [].testD: {},}},Copy the code

Rc-redux-model will automatically register an action for each state field that changes the state, freeing the ⌨️ CV key on your keyboard, for example:

state: {
  userName: 'oldValue'
}
Copy the code

Set ${stateName}. If you set stateName to userName, you will automatically register the action setuserName

action: {
  setuserName: ({ dispatch, getState, commit, call, currentAction }) => {}
}
Copy the code

You can change state simply by calling this action in the component (📢 is not recommended, setStore is recommended).

this.props.dispatch({
  type: 'userModel/setuserName'.payload: {
    userName: 'newValue',}})Copy the code

The problem is that when there are many values in state (e.g., dozens of values), the user automatically registers dozens of actions. Does the user need to remember the corresponding action of each state? This must be extremely unreasonable, so the first step is to provide a default action that changes all the state values…

The problem with this is that if only one action is provided, then the action.type where all State values are changed will not be visible in the redux-devtools-extension (because it is the same action), and eventually, Again, a default action is provided, which is forwarded to the corresponding action according to the payload-key provided by the user.

✨ provides a unified default action for external users. Forwards real actions based on internal keys

this.props.dispatch({
  type: '[model.namespace]/setStore'.payload: {
    key: [model.state.key]  // The state key you want to change
    values: [your values] // The value you want to modify}})Copy the code

🌟 All actions that change state are sent via setStore. You don’t have to worry about finding them in Redux DevTools. This action will only forward the corresponding action based on your key

How do I send an action?

An action consists of type and payload. The name of type is [model.namespace/actionName].

Namespace = appModel, actionName = fetchUserList
const action = {
  type: 'appModel/fetchUserList',}// Initiate the action
this.props.dispatch(action)
Copy the code

Note that each action is a function, which is the same as an asynchronous action. If you don’t understand, go to 👉

Who handles asynchronous requests?

In model.action, each action is a function and its callback argument is:

  • Dispatch: Store provides an API that you can calldispatchContinue to distribute actions
  • GetState: API provided by the Store to get the latest state
  • CurrentAction: The current youthis.props.dispatchAction, you can get it heretypepayload
  • Call: forwards the request for you, using the Promise package, but you can write your own asynchronous logic
  • Commit: Receives an Action, which is used to dispatch the action to the reducers to modify the state value

You can handle asynchrony yourself and change the state value by calling the default [model.namespace/setStore] action

How do I get the state value in the component?

Note that the RC-Redux-Model is middleware and, for the most part, compatible with your existing projects, so get state the same way you would get state in components

In general, our projects will install the React-Redux library and use Connect to get the values on the state.

class appComponent extends React.Component {
  componentDidMount() {
    // Initiate action to change the loading state to true
    this.props.dispatch({
      type: 'appModel/fetchLoadingStatus'.payload: {
        loadingStatus: true,}})}render() {
    const { loadingStatus } = this.props.appModel
    console.log(loadingStatus) // true}}const mapStateToProps = (state) = > {
  return {
    appModel: state.appModel,
    reportTaskInfo: state.reportModel.taskInfo, // Other model values}}export default connect(mapStateToProps)(appComponent)
Copy the code

If, unfortunately, you don’t have a react-Redux installed in your project, you’ll have to import the store in each component and get the state value via store.getState()

The downside of this approach is that you need to make sure your state is up to date, meaning that after you change the state value, you need to retrieve the latest value from store.getState(), which can be a bit cumbersome

import store from '@your_folder/store' // This store is the store you generated using the Redux.createstore API

class appComponent extends React.Component {
  constructor() {
    this.appState = store.getState()['appModel']}}Copy the code

Is data Immutable?

In functional programming languages, data is immutable. Once all data is created, it cannot change its value. If it is changed, it can only create a new data. If you have read redux’s source code, you will know why it is necessary to return a new state every time. If you have not heard of redux’s source code, you can read the article 👉

The rC-redux-model configuration parameter openSeamlessImmutable is set to false if your state is immutable. If this configuration is not set in the Model, an error will be reported!!

/ / use seamless - immutable

import Immutable from 'seamless-immutable'

export default {
  namespace: 'appModel'.state: Immutable({
    username: ' ',}).openSeamlessImmutable: true.// This configuration must be enabled
}
Copy the code

Type correctness?

Inevitably, sometimes you define the type of a value in model.state, but change it to another type when you change it, for example:

export default {
  namespace: 'userModel'.state: {
    name: ' '.// Define name as string}},Copy the code

But when you modify the state value, you do pass a non-string value

this.props.dispatch({
  type: 'userModel/setStore'.payload: {
    key: 'name'.values: {}, // Where name becomes object}})Copy the code

The rC-redux-model compares the type of state[key] with the type of the payload. If the type is not the same, an error message is displayed

Change the value of state if the value is already defined in state

export default {
  namespace: 'userModel'.state: {
    name: ' '.// Only name exists in state}},Copy the code

Now you want to change the value of another property in state

this.props.dispatch({
  type: 'userModel/setStore'.payload: {
    key: 'age'.values: 18.// We want to change the age property}})Copy the code

Extremely unreasonable, since you did not declare this property in state, the RC-redux-model will do it for you by default

use

If in doubt, see the instructions below ~ and for how to use it in your project, 👉 can be clicked here

Provide default actions without additional action/reducers

Originally, we needed to define the Action in reducers if we wanted to change the state value, but now the RC-Redux-Model provides the default action for modification, so we only need to define the state value in the Model

export default {
  namespace: 'appModel'.state: {
    value1: ' ',}}Copy the code

In the page, you only need to call the default [Model.namespace /setStore] to modify the value of the state, delightfully, you do not need to write a lot of repeated modify state code in action and reducers

this.props.dispatch({
  type: 'appModel/setStore'.payload: {
    key: 'value1'.values: 'appModel_state_value1',}})Copy the code

Complex and realistic examples

  1. Create a new model folder and add userModel.js to this folder
// model/userModel.js
import adapter from '@common/adapter'

const userModel = {
  namespace: 'userModel'.openSeamlessImmutable: false.state: {
    classId: ' '.studentList: [].userInfo: {
      name: 'PDK',}},action: {
    // Demo: Initiate an asynchronous request and modify the loading state of global.model. After the asynchronous request, modify the reducers
    // This asynchronous logic can be handled by itself. If call is used, then it will be forwarded by a Promise wrapped layer
    fetchUserInfo: async ({ dispatch, call }) => {
      // Set Loading in globalModel to true before requesting
      dispatch({
        type: 'globalModel/changeLoadingStatus'.payload: true,})let res = await call(adapter.callAPI, params)
      if (res.code === 0) {
        dispatch({
          type: 'userModel/setStore'.payload: {
            key: 'userInfo'.values: res.data,
          },
        })
        // The request ends. Set Loading in globalModel to false
        dispatch({
          type: 'globalModel/changeLoadingStatus'.payload: false})},return res
    },
  },
}

export default userModel
Copy the code
  1. Aggregate all the Models, and notice that you are exporting an array
// model/index.js
import userModel from './userModel'

export default [userModel]
Copy the code
  1. Working with Models, registering middleware
// createStore.js
import { createStore, applyMiddleware, combineReducers } from 'redux'
import models from './models'
import RcReduxModel from 'rc-redux-model'

const reduxModel = new RcReduxModel(models)

const reducerList = combineReducers(reduxModel.reducers)
return createStore(reducerList, applyMiddleware(reduxModel.thunk))
Copy the code
  1. Called in a component
class MyComponents extends React.PureComponent {
  componentDidMount() {
    // Demo: Initiate an asynchronous request and modify the loading state of global.model. After the asynchronous request, modify the reducers
    // Specify the request in model.action, which supports the Promise
    this.props
      .dispatch({
        type: 'userModel/fetchUserInfo',
      })
      .then((res) = > {
        console.log(res)
      })
      .catch((err) = > {
        console.log(err)
      })

    // demo1: Call the automatically generated default action and directly modify the value of state.userInfo (recommended)
    this.props.dispatch({
      type: 'userModel/setStore'.payload: {
        key: 'userInfo'.values: {
          name: 'setStore_name',}}})// demo2: Call the automatically generated default action and directly modify the value of state.classId (recommended)
    this.props.dispatch({
      type: 'userModel/setStore'.payload: {
        key: 'classId'.values: 'sugarTeam2020',},})}}Copy the code

hooks ?

Rc-redux-model in hooks: “What, one is the React feature, one is the middleware for Redux, conflict?”

It is recommended that hooks do all the business logic, including asynchronous requests, etc., call Dispatch only to change state, so your model file is extremely clean, just write namespace and state, You don’t need to write the Actions and reducers, use the default provided [model.namespace/setStore]

API

Each model receives five attributes, as shown below

parameter instructions type The default value
namespace Must, and only string
state Data state, must object {}
action Action, not required object
reducers Reducer, not necessary object
openSeamlessImmutable Whether to enable Immutable is optional boolean false

The default Action provided

@desc Register to generate the default action @summary usagethis.props.dispatch({
   type: '[model.namespace]/setStore'.payload: {
     key: [model.state.key]
     values: [your values]
   }
 })
Copy the code

Related articles

  • rc-redux-model
  • why rc-redux-model and what’s rc-redux-model
  • Rc-redux-model from 0 to 1