Redux-Toolkit, the latest practical guide to Redux

foreword

redux-toolkit is currently the official redux-recommended method for writing redux logic. It is optimized for the problems of cumbersome redux store creation, too much boilerplate code, and dependence on external libraries. The official summary of the four features is simple/thoughtful / Powerful/efficient , in summary, it is more convenient and simple .

According to the official github records, the project started in 2018. From the update of the official document, it can be seen that redux has officially started to be promoted. Its document content combines a large number of redux-toolkit examples. For details, please refer to Redux Chinese official website or Redux official website , the latest redux4.2.0 version also sets redux-toolkit as the best way to use redux

And the original createStore method has been marked as deprecated.

This article mainly introduces redux-toolkit, hereinafter referred to as RTK. If you are not clear about the concept of redux, please understand it before learning. See https://github.com/storepage/redux-toolkit-demo for the sample code in this article

Introduction

Installation package dependencies

yarn add @reduxjs/toolkit react-redux

No need to install redux separately anymore

react configuration

import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
import './index.css';

const container = document.getElementById('root')!;
const root = createRoot(container);

root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

There is basically no difference between this and the previous react-redux. The root component of Provider is still provided, and the store tree is passed in as a parameter, but the definition of store is slightly different. See the next section for details. (The root component rendering here uses the wording of react 18)

create store

/* app/store.ts */
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todoReducer from '../features/todo/todoSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    todo: todoReducer,
  },
});

Call the configureStore method of rtk, which is equivalent to the createStore, combineReducers, middleware, enhancers that integrates redux and redux-redux, and supports the extension tool Redux DevTools by default.

The parameter option is an object containing the following contents

Parameter key value illustrate
reducer Create a reducer and pass it to combineReducers for use
middleware Middleware, passed to applyMiddleware to use
devTools extension tool, default is true
preloadedState Initial state value, passed to createStore
enhancers Enhanced store, passed to createStore

Data acquisition and initiation of action

Let's first look at how to use it in the component. This step can be considered as the content of react-redux

import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import {
  decrement,
  selectCount
} from './counterSlice';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useAppDispatch();

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
      </div>
    </div>
  );
}

The example is a simple counter. Since react supports the writing of hooks, react-redux also provides custom hooks for obtaining state and dispatching an action, which eliminates the need to use the connect method to bind props.

For the above two hooks, the official also gave new suggestions on how to use redux data

  • Any component can read any data from the Redux Store
  • Any component can trigger state updates through dispatch actions

At present, the previous concept of pressenational and container component cannot be found in the official documents (translation is not needed, there are called UI components and container components, and there are also called smart components and puppet components) (this concept is the division recommended by the author of redux in 2015, Those who like to be elegant can read Presentational and Container Components )

 ——The above screenshot is from the traditional version of the document I found so far, Read Me | Redux

Redux officially no longer recommends the division of the above components. On the one hand, the reason should be that it focuses more on redux itself. As for how the user splits the components, it is up to the user to think about it. On the other hand, it may be that the previous connect method will affect performance more.

Define slice Slice

rtk introduces a new definition slice, which is the integration of action and reducer logic in the application, and is created through the createSlice method

import { createSlice, PayloadAction } from '@reduxjs/toolkit';


export interface CounterState {
  value: number;
  status: 'idle' | 'loading' | 'failed';
}

const initialState: CounterState = {
  value: 0,
  status: 'idle',
};


export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state: CounterState) => {
      state.value += 1;
    },
    decrement: (state: CounterState) => {
      state.value -= 1;
    },
    // Use the PayloadAction type to declare the contents of `action.payload`
    incrementByAmount: (state: CounterState, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export default counterSlice.reducer;


The reducers parameter here not only defines the reducers, but also creates the associated action. This method finally returns an object containing actions and reducers.

It can be seen that the state has been directly modified here. On the surface, it seems to violate the principle that redux prohibits modification of the state. In fact, because the library of immer is referenced , it always returns a safe and immutable update value, which greatly simplifies the reducer. The way of writing. Note that this method can only be written in createSlice and createReducer.

As you can see from the redux extension tool, the type of {name}/{reducers.key} is quite generated here

Note that if a parameter is to be passed in the action of the reducer here, it can only be one payload. If there are multiple parameters, it needs to be encapsulated into a payload object.

In fact, data may need to be processed during use. In addition to supporting a method, the definition of the reducer can also provide a preprocessing method, because the reducer must be a pure function, and other variable parameters can be placed in the preprocessing function. Preprocessing The function must return an object with a payload field


import { createSlice, PayloadAction, nanoid } from '@reduxjs/toolkit';

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: {
      reducer(state: TodoState[], action: PayloadAction<TodoState>) {
        const { id, text } = action.payload;
        state.push({ id, text, completed: false });
      },
      prepare(text: string) {
        /* nanoid 随机一个字符串 */
        return { payload: { text, id: nanoid() } };
      },
    },
    toggleTodo(state: TodoState[], action: PayloadAction<string>) {
      const todo = state.find((todo: { id: any }) => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
  },
});

 rtk also provides a nanoid method for generating a fixed-length random string, similar to the uuid function.

You can print the result of dispatch(addTodo(text)) to see that an action with a payload field is returned

Side Effects

rtk integrates redux-thunk to handle asynchronous events, so you can write asynchronous requests according to the previous thunk writing method

// 外部的 thunk creator 函数
const fetchSomething = params => {
  // 内部的 thunk 函数
  return async (dispatch, getState) => {
    try {
      // thunk 内发起异步数据请求
      const res = await someApi(params)
      // 但数据响应完成后 dispatch 一个 action
      dispatch(someAction(res));
      return res;
    } catch (err) {
      // 如果过程出错,在这里处理
    }
  }
}
/* 组件中发起action */
dispatch(fetchSomething(params))

 rtk provides a new method createAsyncThunkto support thunk, which is also recommended by redux

export const incrementAsync = createAsyncThunk('counter/fetchCount', async (amount: number) => {
  const response = await fetchCount(amount);
  return response.data;
});

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
      })
      .addCase(incrementAsync.rejected, (state) => {
        state.status = 'failed';
      });
  },
});

createAsyncThunk can be written in any extraReducers of a slice, it receives 2 parameters,

  • Generate the type value of the action, where the type needs to be defined by yourself, unlike createSlice that automatically generates the type, this requires attention to avoid naming conflicts (if createSlice defines a corresponding name and method, it will also conflict)
  • The promise that includes data processing will first dispatch an action type of 'counter/fetchCount/pending', when the asynchronous request is completed, according to the success or failure of the result, the dispatched action type is determined to be 'counter/fetchCount/fulfilled' or' counter/fetchCount/rejected', these three actions can be processed in the extraReducers of the slice. This promise also only accepts 2 parameters, which are the payload and the thunkAPI object including dispatch and getState. Therefore, in addition to processing in the extraReducers of the slice, createAsyncThunk can also call any action, which is very similar to the original thunk. no, not recommended
export const incrementAsync = createAsyncThunk('counter/fetchCount', 
  async (amount: number, thunkApi) => {
    const response = await fetchCount(amount);
    thunkApi.dispatch(counterSlice.actions.incrementByAmount(response.data));
    return response.data;
  }
);

  In addition, in addition to the above chain method, extraReducers can also be defined as an object, with a structure of {[type]: function(state, action)}, which is a legacy syntax. Although it is supported, it is not officially accepted. recommend.

  extraReducers: {
    [incrementAsync.pending.type]: (state) => {
      state.status = 'loading';
    },
    [incrementAsync.fulfilled.type]: (state, action) => {
      state.status = 'idle';
      state.value += action.payload;
    },
    [incrementAsync.rejected.type]: (state) => {
      state.status = 'failed';
    },
  },

 The action that dispatched createAsyncThunk will catch all exceptions and finally return an action

If you want to get the data returned in the promise of createAsyncThunk and handle the try/catch yourself, you can call the unwrap method

async () => {
  try {
    const payload = await dispatch(incrementAsync(incrementValue)).unwrap();
    console.log(payload);
  } catch (error) {
    // deal with error
  }
}

advanced use

Selector uses cache

When the useSelector method involves complex logic operations and returns an object, a new reference value is returned every time it runs, which will cause the component to re-render even if the returned data content has not changed, as follows with filtered todoList shown

const list = useSelector((state: RootState) => {
    const { todo, visibilityFilter } = state;
    switch (visibilityFilter) {
      case VisibilityFilters.SHOW_ALL:
        return todo;
      case VisibilityFilters.SHOW_COMPLETED:
        return todo.filter((t: TodoState) => t.completed);
      case VisibilityFilters.SHOW_ACTIVE:
        return todo.filter((t: TodoState) => !t.completed);
      default:
        throw new Error('Unknown filter: ' + visibilityFilter);
    }
  });

In order to solve this problem, you can use the Reselect library, which is a library that creates a memorized selector. The result is recalculated only when the input changes. rtk integrates this library and exports it as the createSelector function . The above The selector can be rewritten as follows

const selectTodos = (state: RootState) => state.todo;
const selectFilter = (state: RootState) => state.visibilityFilter;

// 创建记忆化selector
const selectList = createSelector(selectTodos, selectFilter, (todo, filter) => {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todo;
    case VisibilityFilters.SHOW_COMPLETED:
      return todo.filter((t: TodoState) => t.completed);
    case VisibilityFilters.SHOW_ACTIVE:
      return todo.filter((t: TodoState) => !t.completed);
    default:
      throw new Error('Unknown filter: ' + filter);
  }
});

// 使用记忆化selector
const list = useSelector((state: RootState) => selectList(state));

createSelector receives one or more selector input functions (passed in one by one or as an array) and a selector output function, where the output parameters of each input function will be used as the input parameters of the last output function.

In addition, the above problem can also be solved by passing in the second parameter comparison function through useSelector.

normalized state structure

"Normalized state" means:

  • We have only one copy of each particular piece of data in our state, no duplication.
  • Normalized data is held in a lookup table, where the item ID is the key and the item itself is the value.
  • There may also be a specific item that holds the array of all IDs.

According to the above definition, its structural form is determined as

{
  ids: ["user1", "user2", "user3"],
  entities: {
    "user1": {id: "user1", firstName, lastName},
    "user2": {id: "user2", firstName, lastName},
    "user3": {id: "user3", firstName, lastName},
  } 
}

Such a structure is similar to a dictionary, which is convenient for adding, deleting, modifying and searching functions. rtk provides the createEntityAdapter api, which performs a series of standardized operations on the storage of normalized structures.

import { 
  createSlice, 
  PayloadAction, 
  createEntityAdapter, 
  nanoid, 
  EntityState 
} from '@reduxjs/toolkit';
import { RootState } from '../../app/store';

export interface TodoPayload {
  todoId: string;
  text: string;
  completed?: boolean;
  createdTimestamp: number;
}

/* 创建EntityAdapter */
const todoAdapter = createEntityAdapter<TodoPayload>({
  /* 默认值为id */
  selectId: (todo) => todo.todoId,
  /* 对ids进行排序,方法与Array.sort相同,如果不提供,不能保证ids顺序 */
  sortComparer: (a, b) => a.createdTimestamp - b.createdTimestamp,
});

const todosSlice = createSlice({
  name: 'todosEntity',
  initialState: todoAdapter.getInitialState(),
  reducers: {
    /* 增 */
    addTodo: {
      reducer(state: EntityState<TodoPayload>, action: PayloadAction<TodoPayload>) {
        todoAdapter.addOne(state, action.payload);
      },
      prepare(text: string) {
        return {
          payload: {
            text,
            todoId: nanoid(),
            createdTimestamp: Date.now(),
          },
        };
      },
    },
    /* 删 */
    removeTodo(state: EntityState<TodoPayload>, action: PayloadAction<string>) {
      todoAdapter.removeOne(state, action.payload);
    },
    /* 改 */
    toggleTodo(state: EntityState<TodoPayload>, action: PayloadAction<string>) {
      const todo = state.entities[action.payload];
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
  },
});

/* 查 */
export const { selectAll: selectAllTodos } = todoAdapter.getSelectors((state: RootState) => state.todoEntity);

/* action */
export const { actions: todoActions } = todosSlice;
/* reducer */
export default todosSlice.reducer;

Mastering the structure mainly includes three main points

  1. Create an EntityAdapter with a specific id and support for sorting
  2. Get the initial value of state getInitialState, in addition to the default ids and entities, you can also add custom fields
  3. A series of adding, deleting, modifying and checking methods, most of which need to pass in the current state and the data to be operated

Using Redux-Persist

If you want to use Redux-Persist, you need to specifically ignore all action types it dispatches. Refer to Question: How to use this with redux-persist? #121

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import {
  persistStore,
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'

import App from './App'
import rootReducer from './reducers'

const persistConfig = {
  key: 'root',
  version: 1,
  storage
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

const store = configureStore({
  reducer: persistedReducer,
  middleware: getDefaultMiddleware({
    serializableCheck: {
      ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
    }
  })
})

let persistor = persistStore(store)

ReactDOM.render(
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
      <App />
    </PersistGate>
  </Provider>,
  document.getElementById('root')
)

The above rootReducer is written in the same way as the previous reducer, and needs to be integrated using combineReducers

import { combineReducers } from 'redux';

import counterReducer from '../features/counter/counterSlice';
import todoReducer from '../features/todos/todoSlice';
import todoEntityReducer from '../features/todosEntity/todoSlice';
import visibilityFilterReducer from '../features/filters/filtersSlice';

export default combineReducers({
  counter: counterReducer,
  todo: todoReducer,
  todoEntity: todoEntityReducer,
  visibilityFilter: visibilityFilterReducer,
});

Guess you like

Origin blog.csdn.net/cscj2010/article/details/125705530