You Might Not Need Redux. —— Dan Abramov

But we can use useReducer and useContext ~

Previous words:

UseContext implements state sharing, and useReducer implements functions like Redux state manager Dispatch. Thus, we can implement a simple state manager using these two hooks. If ts is added, we can think of many types defined by ourselves, and the pleasure presented in front of us by TS and editor support

In a project, we might have multiple states to share, we might asynchronously request user information after login, and then use that user information on other pages… .

Let’s use these two hooks for example: (If you don’t know about these two hooks, you can read the article introduction and click on me.)

Achieve asynchronous access to user information related files

userInfo/index.declare.ts

export interface IState { id? : string; name? : string; isFetching? : boolean; failure? : boolean; message? : string; } type TType = | "ASYNC_SET_USER_INFO" | "FETCHING_START" | "FETCHING_DONE" | "FETCHING_FAILURE"; export interface IAction { type: TType; payload? : IState; }Copy the code

This file is extracted here, declaring the basic constraints of state, type, and action. The parameters are received as payload

userInfo/index.tsx

import React, { useReducer, createContext, useContext } from "react"; import { IState, IAction } from "./index.declare"; Const initialState: IState = {id: "", name: "", isFetching: false, failure: false, message: ""}; // Create a context and initialize the value const context: react. context <{state: IState; dispatch? : React.Dispatch<IAction>; }> = createContext({ state: initialState }); // reducer const reducer: React.Reducer<IState, IAction> = ( state, { type, payload } ): IState => { switch (type) { case "ASYNC_SET_USER_INFO": { const { id, name, message } = payload! ; return { ... state, id, name, message }; } case "FETCHING_START": { return { ... state, failure: false, isFetching: true }; } case "FETCHING_DONE": { return { ... state, isFetching: false }; } case "FETCHING_FAILURE": { return { id: "", name: "", failure: true, message: payload? .message }; } default: throw new Error(); }}; /** * const request = (id: string): Promise<any> => { return new Promise((resolve, reject) => { setTimeout(() => { if (id === "998") { resolve({ id: "998", name: "liming", message: "user acquired successfully"}); } else {reject(' could not find user with id ${id} '); }}, 1000); }); }; /** * dispatch */ const dispatchHO = (dispatch: React.Dispatch<IAction>) => { return async ({ type, payload }: IAction) => { if (type.indexOf("ASYNC") ! == -1) { dispatch({ type: "FETCHING_START" }); try { const { id, name, message } = await request(payload! .id!) ; dispatch({ type, payload: { id, name, message } }); } catch (err) { dispatch({ type: "FETCHING_FAILURE", payload: { message: err } }); } dispatch({ type: "FETCHING_DONE" }); } else { dispatch({ type, payload }); }}; }; /** */ export const ProviderHOC = (WrappedComponent: react.fc) => {const Comp: React.FC = (props) => { const [state, dispatch] = useReducer(reducer, initialState); return ( <context.Provider value={{ state, dispatch: dispatchHO(dispatch) }}> <WrappedComponent {... props} /> </context.Provider> ); }; return Comp; }; /** * export const useContextAlias = () => {const {state, dispatch} = useContext(context); return [state, dispatch] as [IState, React.Dispatch<IAction>]; };Copy the code

Explanation:

  • The Request method is just a mock interface request wait.
  • In a real business scenario, we would request user information asynchronously, so the core code for implementing an asynchronous action is in the dispatchHO method, which is a higher-order function with Dispatch as a parameter. When we make an asynchronous request, we need to change some state, such as before the request, the request succeeds, the request fails… We need to encapsulate it in a big dispatch. This particular “big dispatch” is triggered only if the convention type has “ASYNC”.
  • ProviderHOC is a higher-order component, which is usually written in this way to share state, such as:
<context.Provider value={obj}>
  <App />
</context.Provider>;
Copy the code

But here we use higher-order components to give us more flexibility when wrapping root components, so keep reading.

  • The useContextAlias method is a reencapsulation of useContext. Here I convert it to the familiar useReducer notation, such as:
const [state, dispatch] = useContext();
Copy the code

A file directory structure in a project might look like this

We can see that reducers are specifically used to put some Reducer modules, userInfo, userList…

Reducers /index.ts as a main file, let’s take a look at the implementation inside:

import React from "react"; import { ProviderHOC as ProviderHOCUserList, useContextAlias as useContextUserList } from "./userList"; import { ProviderHOC as ProviderHOCUserInfo, useContextAlias as useContextUserInfo } from "./userInfo"; /** * const compose = (... providers: any[]) => (root: any) => providers.reverse().reduce((prev, next) => next(prev), root); const arr = [ProviderHOCUserList, ProviderHOCUserInfo]; const providers = (root: React.FC) => compose(... arr)(root); export { useContextUserList, useContextUserInfo }; export default providers;Copy the code

Explanation:

  • The compose method is the core method for combining various providers. We introduce ProviderHOC exposed by each module and then combine them. This allows us to flexibly add more providers without having to manually wrap the root component. In the App we can do this, for example:

App.tsx

import React from "react";
import "./styles.css";
import providers from "./reducers";
import UseReducerDemo from "./userReducer.demo";

const App = () => {
  return (
    <div className="App">
      <UseReducerDemo />
    </div>
  );
};
export default providers(App);
Copy the code
  • We import useContextUserList, useContextUserInfo, and aliases from useContextUserInfo. In other pages, we import the context we want to use, for example:

userReducer.demo.tsx

import React from "react"; import { useContextUserInfo, useContextUserList } from "./reducers"; const Index: React.FC = () => { const [userInfo, dispatchUserInfo] = useContextUserInfo(); const [userList, dispatchUserList] = useContextUserList(); Return (<div className="demo"> userInfo: <p> Status: {userinfo.isfetching? "Loading..." : "Loaded"} < / p > < p > id: {the userInfo. Id} < / p > < p > name: {the userInfo. Name} < / p > < p > the message: {userInfo.message}</p> <button disabled={userInfo.isFetching} onClick={() => { dispatchUserInfo({ type: "ASYNC_SET_USER_INFO", payload: { id: "998" } }); }} > Fetch user information id="998" </button> <button disabled={userinfo.isfetching} onClick={() => {dispatchUserInfo({type: dispatchUserInfo) "ASYNC_SET_USER_INFO", payload: { id: "1" } }); Id ="1" </button>); }; export default Index;Copy the code

conclusion

When we are working on a project, these two hooks can be used to make very lightweight Redux, and we can also implement asynchronous actions ourselves. In addition, TS allows us to write dispatches in other pages with a stable feeling. The compose method is used to combine various higher-order components, enabling us to share various states more flexibly.

So click on me to see the full example