I've been experimenting with Hooks lately and looking more into how can I replace Redux withuseContext anduseReduer. For me,Vuex was more intuitive when I first got into stores and I prefer their state management pattern, so I tried to build something close to that using the React Context.
I aim to have one store/context for each page in my app or to have the ability to pull the store/context up, so it's available globally if needed. The final customuseStore() hook should return a store with the following parts:
{ state, mutations, actions, getters }
Components can then dispatch actions withactions.dispatch({type: 'my-action', payload}) (actions commit mutations) or directly commit mutations withmutations.commit({ type: 'my-mutation', payload}). Mutations then mutate the state (usinguseReducer), which finally causes a rerender.
For my example, I have two entities inside./models.User (context/store provided globally) andPost(context/store provided on it's page):
// User.tsexport interface User { id: number username: string website: string}// Post.tsexport interface Post { id: number userId: number title: string}I then create the reducers./store/{entity}/structure/reducer.ts:
import { UserState } from './types'import { UserMutations } from './types';export function userReducer(state: UserState, mutation: UserMutations): UserState { switch (mutation.type) { // ... case 'set-users': return { ...state, users: [...state.users, ...mutation.users] } // ... }}Switch through mutations from./store/{entity}/structure/mutations.ts
import { User } from '../../../models/User';import { AxiosError } from 'axios';export const setUsers = (users: User[]) => ({ type: 'set-users', users} as const);To get the state./store/{entity}/structure/types/index.ts:
export interface UserState { isLoading: boolean error: AxiosError users: User[]}Any heavier work (fetching data, etc.) before committing a mutation is located inside actions./store/{entity}/structure/actions.ts:
import { UserMutations, UserActions } from "./types";import axios, { AxiosResponse } from 'axios';import { GET_USERS_URL, User } from "../../../models/User";import { API_BASE_URL } from "../../../util/utils";export const loadUsers = () => ({ type: 'load-users'} as const);export const initActions = (commit: React.Dispatch<UserMutations>) => { const dispatch: React.Dispatch<UserActions> = async (action) => { switch (action.type) { case 'load-users': try { commit({ type: 'set-loading', isLoading: true }) const res: AxiosResponse<User[]> = await axios.get(`${API_BASE_URL}${GET_USERS_URL}`) if (res.status === 200) { const users: User[] = res.data.map((apiUser) => ({ id: apiUser.id, username: apiUser.username, website: apiUser.website })) commit({ type: 'set-users', users }) } } catch (error) { commit({ type: 'set-error', error }) } finally { commit({ type: 'set-loading', isLoading: false }) } break; default: break; } } return dispatch}Additionally, a new derived state can be computed based on store state using getters./store/{entity}/structure/getters.ts:
import { UserState, UserGetters } from "./types"export const getters = (state: Readonly<UserState>): UserGetters => { return { usersReversed: [...state.users].reverse() }}Finally, everything is initialized and glued together inside./store/{entity}/Context.tsx:
import React, { createContext, useReducer } from 'react'import { UserStore, UserState } from './structure/types'import { userReducer } from './structure/reducer'import { getters } from './structure/getters'import { initActions } from './structure/actions'import { AxiosError } from 'axios'const initialStore: UserStore = { state: { isLoading: false, error: {} as AxiosError, users: [] } as UserState, getters: { usersReversed: [] }, mutations: { commit: () => {} }, actions: { dispatch: () => {} }}export const UserContext = createContext<UserStore>(initialStore)export const UserContextProvider: React.FC = (props) => { const [state, commit] = useReducer(userReducer, initialStore.state) const store: UserStore = { state, getters: getters(state), actions: { dispatch: initActions(commit) }, mutations: { commit } } return ( <UserContext.Provider value={store}> {props.children} </UserContext.Provider> )}For a syntactic sugar, I wrap theuseContext() hook with a custom one:
import { useContext } from 'react'import { UserContext } from './UserContext'const useUserStore = () => { return useContext(UserContext)}export default useUserStoreAfter providing the context, the store can be used as such:
const { actions, getters, mutations, state } = useUserStore()useEffect(() => { actions.dispatch({ type: 'load-users' })}, [])Are there any optimizations I can do? What are the biggest cons when comparing to redux? Here is therepo, any feedback is appreciated.
Edit 1:
I've wrapped theuseContext() with a customuseUserStore() hook, so it can be used as
const { actions, getters, mutations, state } = useUserStore()and so the store/context terms are unified when using the store.
You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.

