preface

In the middle of the year, the company started a new project and needed to build a micro front-end architecture. After multiple investigations, the technical solutions of Qiankun, UMI and DVA were determined. However, a big problem was encountered in the first month. First, DVA is a contracted form and cannot be flexibly configured. Second, the universe can not fully meet the business needs, need to change a lot of source code, such as master communication, brother communication and so on. After some choice, the plan was abandoned. After that, based on single-SPA, a set of micro front-end architecture was built. At the same time, templates were generated by commands, similar to create-React-app, using technology stacks React and Redux. I was used to the operation method of DVA before, so it is cumbersome to use REdux. Since the new project is relatively large, MOBx is not recommended. After researching various options, I chose redux author Dan Abramov’s toolkit @ReduxJS/Toolkit (RTK), which was published in March this year.

Introduction to the

RTK is designed to help solve three problems with Redux:

  • Configuring Redux storage is too complex;
  • Many packages must be added to get Redux to do what is expected;
  • Redux requires too much boilerplate code;

Simply speaking, the process of Redux storage configuration is too complicated, requiring actionTypes, Actions, Reducer, store and connect through connect. With RTK, all you need is a reducer, provided that the component is in the form of hooks.

directory

  1. configureStore
  2. createAction
  3. createReducer
  4. createSlice
  5. createAsyncThunk
  6. createEntityAdapter
  7. Unit test of some difficult codes

configureStore

ConfigureStore is an abstract wrapper around the standard Redux createStore function, adding default values for a better development experience. Traditional Redux requires configuration of reducer, middleware, devTools, enhancers, and so on, and uses configureStore to directly encapsulate these defaults. The code is as follows:

import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

// The store already integrates with redux-thunk and Redux DevTools
const store = configureStore({ reducer: rootReducer })
Copy the code

Redux is much simpler than native Redux, but the details of how to configure Redux will not be covered here.

CreateAction, createReducer

Function createAction(type, prepareAction?)

  1. Type: actionTypes in Redux
  2. PrepareAction: Actions in Redux

As follows:

const INCREMENT = 'counter/increment'

function increment(amount: number) {
  return {
    type: INCREMENT,
    payload: amount,
  }
}
const action = increment(3) // { type: 'counter/increment', payload: 3 }
Copy the code

CreateReducer simplifies the Redux Reducer function creation process, integrates immer internally, simplifies immutable update logic by writing variable code in reducer, and allows specific operation types to be mapped directly to case Reducer functions, which schedule update status. Different from the way Redux Reducer uses switch cases, createReducer simplifies this way and supports two different forms:

  1. builder callback
  2. map object

The first way is as follows:

import { createAction, createReducer } from '@reduxjs/toolkit'

interface CounterState {
  value: number
}

/ / create the actions
const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')
const incrementByAmount = createAction<number>('counter/incrementByAmount')

const initialState: CounterState = { value: 0 }

/ / create a reducer
const counterReducer = createReducer(initialState, (builder) = > {
  builder
    .addCase(increment, (state, action) = > {
      // immer is used, so there is no need to use the original way: return {... state, value: state.value + 1}
      state.value++
    })
    .addCase(decrement, (state, action) = > {
      state.value--
    })
    .addCase(incrementByAmount, (state, action) = > {
      state.value += action.payload
    })
})
Copy the code

It looks better than Redux actions and Reducer. We will not talk about map object, but we will talk about createSlice and createAsyncThunk.

Builder provides three methods

  1. AddCase: Add a Reducer case operation based on the action.
  2. AddMatcher: Filter using matcher function before calling actions
  3. AddDefaultCase: default value, equivalent to the default case of the switch.

createSlice

CreateSlice encapsulates actions and Reducer. It is a function that accepts Initial State, Reducer, Action Creator, and action types. This is standard writing using RTK. Use createAction and createReducer internally, and integrate immer.

// initial state interface
export interface InitialStateTypes {
  loading: boolean;
  visible: boolean;
  isEditMode: boolean;
  formValue: CustomerTypes;
  customerList: CustomerTypes[];
  fetchParams: ParamsTypes;
}

// initial state
const initialState: InitialStateTypes = {
  loading: false.visible: false.isEditMode: false.formValue: {},
  customerList: [].fetchParams: {}};// Create a slice
const customerSlice = createSlice({
  name: namespaces, // Namespace
  initialState, / / initial value
  // Each method in reducers is a combination of action and Reducer, and immer is integrated
  reducers: {
    changeLoading: (state: InitialStateTypes, action: PayloadAction<boolean>) = > {
      state.loading = action.payload;
    },
    changeCustomerModel: (state: InitialStateTypes, action: PayloadAction<IndexProps>) = > {
      const { isOpen, value } = action.payload;
      state.visible = isOpen;
      if (value) {
        state.isEditMode = true;
        state.formValue = value;
      } else {
        state.isEditMode = false; }}},// Additional reducer to handle the reducer of asynchronous actions
  extraReducers: (builder: ActionReducerMapBuilder<InitialStateTypes>) = > {
    builder.addCase(fetchCustomer.fulfilled, (state: InitialStateTypes, { payload }) = > {
      const{ content, pageInfo } = payload; state.customerList = content; state.fetchParams.pageInfo = pageInfo; }); }});Copy the code

UseDispatch and useSelector are used in this article. Therefore, class is not supported. To use class, use connect.

import { useDispatch, useSelector } from 'react-redux';
import {
  fetchCustomer,
  changeCustomerModel,
  saveCustomer,
  delCustomer,
} from '@root/store/reducer/customer';

export default() = > {const dispatch = useDispatch();
  / / value
  const { loading, visible, isEditMode, formValue, customerList, fetchParams } = useSelector(
    (state: ReducerTypes) = > state.customer,
  );

  useEffect(() = > {
    // dispatch
    dispatch(fetchCustomer(fetchParams));
  }, [dispatch, fetchParams]);  
}
Copy the code

Without connect, the code is elegant.

createAsyncThunk

Here’s the RTK integration thunk, which wants to use redux-Saga’s own configuration in the same way. CreateAsyncThunk takes the Redux Action Type string and returns a Promise callback. It generates the Promise’s action type life cycle based on the action type prefix passed in and returns a Thunk Action Creator. It does not track status or how to handle return functions, which should be handled in the Reducer. Usage:

export const fetchCustomer = createAsyncThunk(
  `${namespaces}/fetchCustomer`.async (params: ParamsTypes, { dispatch }) => {
    const { changeLoading } = customerSlice.actions;
    dispatch(changeLoading(true));
    const res = await server.fetchCustomer(params);
    dispatch(changeLoading(false));

    if (res.status === 0) {
      return res.data;
    } else{ message.error(res.message); }});Copy the code

CreateAsyncThunk takes three arguments

  1. TypePrefix: action types
  2. PayloadCreator: {dispatch, getState, extra, requestId… }, the normal development only needs to know about dispatch and getState, note: getState here can get the state of the entire store
  3. Options: optional, {condition, dispatchConditionRejection}, condition, can carry out, before the content to create successful cancel said return false to cancel.

When talking about createReducer, there are two ways to express it, one is builder callback, namely build.addCase(), the other is map object. The following is explained in this way. When createAsyncThunk is created successfully, the value returned by the createAsyncThunk will be received in extraReducers.

  1. Pending: ‘fetchCustomer/requestStatus/pending, running;
  2. Fulfilled: ‘fetchCustomer/requestStatus/fulfilled, complete;
  3. Rejected: ‘fetchCustomer/requestStatus/rejected, refused to;

The code is as follows:

const customerSlice = createSlice({
  name: namespaces, // Namespace
  initialState, / / initial value
  // Each method in reducers is a combination of action and Reducer, and immer is integrated
  reducers: {
    changeLoading: (state: InitialStateTypes, action: PayloadAction<boolean>) = > {
      state.loading = action.payload;
    },
    changeCustomerModel: (state: InitialStateTypes, action: PayloadAction<IndexProps>) = > {
      const { isOpen, value } = action.payload;
      state.visible = isOpen;
      if (value) {
        state.isEditMode = true;
        state.formValue = value;
      } else {
        state.isEditMode = false; }}},// Additional reducer to handle the reducer of asynchronous actions
  extraReducers: {
    // padding
    [fetchCustomer.padding]: (state: InitialStateTypes, action: PayloadAction<IndexProps>) = > {},
    // fulfilled
    [fetchCustomer.fulfilled]: (state: InitialStateTypes, action: PayloadAction<IndexProps>) = > {},
    // rejected
    [fetchCustomer.rejected]: (state: InitialStateTypes, action: PayloadAction<IndexProps>) = >{},}});Copy the code

The corresponding builder.addCase method:

  extraReducers: (builder: ActionReducerMapBuilder<InitialStateTypes>) = > {
    builder.addCase(fetchCustomer.padding, (state: InitialStateTypes, { payload }) = > {});
    builder.addCase(fetchCustomer.fulfilled, (state: InitialStateTypes, { payload }) = > {});
    builder.addCase(fetchCustomer.rejected, (state: InitialStateTypes, { payload }) = > {});
  },
Copy the code

createEntityAdapter

The literal creation of an entity adapter is intended to generate a set of pre-built reductor and selector functions that perform CRUD operations on objects containing specific types, which can be passed as case reducers to createReducer and createSlice, or as helper functions. The createEntityAdapter is ported from @ngrx/ Entity with a lot of modifications. Its role is to implement the idea of state formalization. Entity indicates the uniqueness of a data object. Id is generally used as the key value. The entity State structure generated by the createEntityAdapter method looks like this:

{
  // The unique ID of each object must be string or number
  ids: []
  Key = id,value = value of the object where the id is;
  entities: {}}Copy the code

Create a createEntityAdapter:

type Book = {
  bookId: string;
  title: string;
};

export const booksAdapter = createEntityAdapter<Book>({
  selectId: (book) = > book.bookId,
  sortComparer: (a, b) = > a.title.localeCompare(b.title),
});

const bookSlice = createSlice({
  name: 'books'.initialState: booksAdapter.getInitialState(),
  reducers: {
    // Add a book entity
    bookAdd: booksAdapter.addOne,
    // Accept all books entities
    booksReceived(state, action){ booksAdapter.setAll(state, action.payload.books); ,}}});export const { bookAdd, booksReceived } = bookSlice.actions;
export default bookSlice.reducer;
Copy the code

Values in components:

  import React, { useEffect } from 'react';
  import { useDispatch, useSelector } from 'react-redux';
  
  const dispatch = useDispatch();
  const entityAdapter = useSelector((state: ReducerTypes) = > state);
  const books = booksAdapter.getSelectors((state: ReducerTypes) = > state.entityAdapter);

  console.log(entityAdapter);
  // { ids: ['a001', 'a002'], entities: { a001: { bookId: 'a001', title: 'book1' }, a002: { bookId: 'a002', title: 'book2' } } }

  console.log(books.selectById(entityAdapter, 'a001'));
  // { bookId: 'a001', title: 'book1' }

  console.log(books.selectIds(entityAdapter));
  // ['a001', 'a002']

  console.log(books.selectAll(entityAdapter));
  // [{ bookId: 'a001', title: 'book1' }, { bookId: 'a002', title: 'book2' }]

  useEffect(() = > {
    dispatch(bookAdd({ bookId: 'a001'.title: 'book1' }));
    dispatch(bookAdd({ bookId: 'a002'.title: 'book2'})); } []);Copy the code

From the provided method, the original array value can be obtained. After the normalization, the key-value mode can obtain the array IDS to store the key, which is the state normalization.

unit test

Public part:

  const dispatch = jest.fn();
  const getState = jest.fn(() = > ({
    dispatch: jest.fn(),
  }));
  const condition = jest.fn(() = > false);
Copy the code
  1. Methods in Reducers, Actions unit tests:
const action = changeCustomerModel({
      isOpen: true,
      value,
    });
    expect(action.payload).toEqual({
      isOpen: true,
      value,
    });
Copy the code
  1. Thunk Actions (createAsyncThunk) unit test
    const mockData = {
      status: 0.data: {
        content: [{id: '001'.code: 'table001'.name: 'Joe'.phoneNumber: '15928797333'.address: 'Chengdu Tianfu New District',},],},}// server.fetchCustomer method mock data
    server.fetchCustomer.mockResolvedValue(mockData);
    // Execute the thunk action asynchronous method
    const result = await fetchCustomer(params)(dispatch, getState, { condition });
    // Request interface data to assert whether it is mock data
    expect(await server.fetchCustomer(params)).toEqual(mockData);
    // dispatch Sets the loading state to true
    dispatch(changeLoading(true));
    // Declare thunk action successful
    expect(fetchCustomer.fulfilled.match(result)).toBe(true);
    
    // Perform extraReducers' fetchCustomer. Depressing
    customerReducer(
      initState,
      fetchCustomer.fulfilled(
        {
          payload: {
            content: [value],
            pageInfo: initState.fetchParams.pageInfo,
          },
        },
        ' ',
        initState.fetchParams,
      ),
    );

    // Assert that the first dispatch set loading to true
    expect(dispatch.mock.calls[1] [0]).toEqual({
      payload: true.type: 'customer/changeLoading'});// The request succeeded. Set loading to false for the second dispatch
    expect(dispatch.mock.calls[2] [0]).toEqual({
      payload: false.type: 'customer/changeLoading'});// thunk action return to extraReducers
    expect(dispatch.mock.calls[3] [0].payload).toEqual(mockData.data);
Copy the code

Afterword.

Write a little messy, is as notes to record, have written wrong place not stingy comment.

reference

  1. Redux-toolkit.js.org/introductio…
  2. Redux.js.org/recipes/str…