Writing reason
There is a view on the web that you can use React Hooks to completely replace Redux for state management. In my opinion, for some relatively large front-end projects, there will be a considerable number of states that need to be shared in each module for data and operation sharing. Because Redux’s Dispatch action procedure invokes related methods by matching type strings, avoiding importing methods and types, it has some advantages in this case. But for data where the scope of state is small, or for projects that are not so large, especially with Typescript, use Hooks instead of Redux, both in terms of the overall amount of code and how easy it is to handle business logic as requirements change. The Hooks scheme has some advantages.
In view of the fact that most of the articles on the web are limited to using Hooks to handle state and do not deal with asynchronous requests, this article will be based on my study of related blogs and my own experience in development. This paper presents an implementation of using React Hooks instead of Redux for state management and asynchronous requests.
PS: Refer to the GitHub repository for the code in this article
Technical route
The author’s technical route provides a Context for using React Context, and works with useReducer for state management. The code for asynchronous requests and obtaining state is encapsulated in custom hooks and exposed to the view component for use. The view component simply gets and changes state by using state and methods exposed by custom hooks. The definition of custom Hooks references the state management tool Easy Peasy.
Create a data.d.ts file to define the interface to use
export interface IListItem {
id: number;
name: string;
};
export interface IStoreState {
byId: { [key innumber]? : IListItem }; allIds: number[]; }Copy the code
PS: The reason for using byId and allIds can be found in the official Redux documentation
Create a store. TSX file for writing business logic
First you need to define the context to create a state management context
const initialState: IStoreState = { byId: {}, allIds: []};const StoreContext = createContext<{ state: IStoreState; dispatch? : Dispatch<Action> }>({state: initialState,
});
Copy the code
Secondly, define a Reducer function to control state changes in the useReducer
type Action =
| { type: 'saveState'.payload: IStoreState }
| { type: 'addItem'.payload: IListItem }
| { type: 'removeItem'.payload: Pick<IListItem, 'id'>};const reducer = (state: IStoreState, action: Action) = > {
const { byId, allIds } = state;
switch (action.type) {
case 'saveState':
return action.payload;
case 'addItem':
byId[action.payload.id] = action.payload;
return { byId, allIds: [action.payload.id, ...allIds] };
case 'removeItem':
return { ...state, allIds: allIds.filter(i= >i ! == action.payload.id) };default:
returnstate; }};Copy the code
There are three actions defined: save the initial state, add an Item, and remove an Item.
We then define a React component: Store component that defines the state management context in the parent component. This is the default exported function component.
const Store: React.FC = ({ children }) = > {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StoreContext.Provider value={{ state.dispatch}} >{children}</StoreContext.Provider>
);
};
Copy the code
This is used in the parent component as follows
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import Store from './Store';
ReactDOM.render(
<React.StrictMode>
<Store>
<App />
</Store>
</React.StrictMode>.document.getElementById('root'));Copy the code
This context then needs to be used in a child component. The user can of course choose to use useContext directly to get the context and use it in the child component, but this will confuse the business code with the view component, making it difficult for later maintenance and requirements iteration. Therefore, it is best to encapsulate the business code in the Store, exposing only the data and methods that the view needs.
First state, note that since the data layer uses byId and allIds to manage state, and the view component requires an array of objects, you can encapsulate this logic in custom Hooks useStoreState to expose the List directly
export const useStoreState = () = > {
const { state } = useContext(StoreContext);
const list = useMemo(() = > {
const { byId, allIds } = state;
return allIds.map(id= > byId[id]).filter(i= > i) as NonNullable<IListItem[]>;
}, [state]);
return { list };
}
Copy the code
The second is the method used by the view component. Once the business logic is wrapped with useStoreActions, only the method is exposed to the view component, which does not need to know its implementation details
export const useStoreActions = () = > {
const { dispatch } = useContext(StoreContext);
if (dispatch === undefined) {
throw new Error('error information');
}
const onAdd = useCallback(async (item: IListItem) => {
// send async request;
dispatch({ type: 'addItem'.payload: item });
}, [dispatch]);
const onRemove = useCallback(async (id: number) => {
// send async request
dispatch({ type: 'removeItem'.payload: { id } });
}, [dispatch]);
return { onAdd, onRemove };
};
Copy the code
Only useStoreState and useStoreActions are exposed as the only outlets to get Store state and change state functions.
It is used in view components as follows
function App() {
const countRef = useRef(200);
const { list } = useStoreState();
const { onAdd, onRemove } = useStoreActions();
const handleAdd = () = > {
const id = countRef.current++;
onAdd({ id, name: `item ${id}` });
};
return (
<div className="App">
<div className="button-block">
<button onClick={handleAdd}>Add</button>
</div>
<div>
<ul className="list">
{list.map(item => (
<li key={item.id}>
<span>{item.id}</span>
<span>{item.name}</span>
<button onClick={()= > onRemove(item.id)}>remove</button>
</li>
))}
</ul>
</div>
</div>
);
}
Copy the code
When initial data needs to be requested
When you need to request initial data, you can use useEffect inside the Store component
const Store: React.FC = ({ children }) = > {
const [state, dispatch] = useReducer(reducer, initialState);
const [loading, setLoading] = useState(false);
useEffect(() = > {
let didCancel = false;
(async () => {
setLoading(true);
const res = await queryList();
if(res && ! didCancel) { dispatch({type: 'saveState'.payload: res });
setLoading(false);
}
})();
return () = > {
didCancel = true; }; } []);return (
<StoreContext.Provider value={{ state.loading.dispatch}} >{children}</StoreContext.Provider>
);
};
Copy the code
PS: How to fetch data with React Hooks?
The loading state enables the page to be spun when back-end data is requested, which is also exposed to the view component through useStoreState
export const useStoreState = () = > {
const { state, loading } = useContext(StoreContext);
const list = useMemo(() = > {
const { byId, allIds } = state;
return allIds.map(id= > byId[id]).filter(i= > i) as NonNullable<IListItem[]>;
}, [state]);
return { list, loading };
}
Copy the code
References:
- How to use hooks (useContext, useReducer) to replace redux
- Replacing React’s Redux library with the useReducer Hook
- Redux Advanced Series 2: How to design Redux State properly