Edit 1:
I've wrapped the useContext() with a custom useUserStore() 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.
Stack Exchange network consists of 183 Q&A communities including Stack Overflow, the largest, most trusted online community for developers to learn, share their knowledge, and build their careers.
Visit Stack ExchangeStack Internal
Knowledge at work
Bring the best of human thought and AI automation together at your work.
Explore Stack InternalEdit 1:
I've wrapped the useContext() with a custom useUserStore() 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.
Edit 1:
I've wrapped the useContext() with a custom useUserStore() 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.
I've been experimenting with Hooks lately and looking more into how can I replace Redux with useContext and useReduer. For me, VuexVuex 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 custom useContextuseStore() hookshook should return a store with the following parts:
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 the useContext() hook with a custom one:
import { useContext } from 'react' import { UserContext } from './UserContext' const useUserStore = () => { return useContext(UserContext) } export default useUserStore const { actions, getters, mutations, state } = useContextuseUserStore(UserContext) useEffect(() => { actions.dispatch({ type: 'load-users' }) }, []) Are there any optimizations I can do? What are the biggest cons when comparing to redux? Here is the repo and sandbox, any feedback is appreciated.
I've been experimenting with Hooks lately and looking more into how can I replace Redux with useContext and useReduer. 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 useContext() hooks should return a store with the following parts:
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> ) } const { actions, getters, mutations, state } = useContext(UserContext) useEffect(() => { actions.dispatch({ type: 'load-users' }) }, []) Are there any optimizations I can do? What are the biggest cons when comparing to redux? Here is the repo and sandbox, any feedback is appreciated.
I've been experimenting with Hooks lately and looking more into how can I replace Redux with useContext and useReduer. 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 custom useStore() hook should return a store with the following parts:
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 the useContext() hook with a custom one:
import { useContext } from 'react' import { UserContext } from './UserContext' const useUserStore = () => { return useContext(UserContext) } export default useUserStore 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 the repo, any feedback is appreciated.
I've been experimenting with Hooks lately and looking more into how can I replace Redux with useContext and useReduer. 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 useContext() hooks should return a store with the following parts:
{ state, mutations, actions, getters }
Components can then dispatch actions with actions.dispatch({type: 'my-action', payload}) (actions commit mutations) or directly commit mutations with mutations.commit({ type: 'my-mutation', payload}). Mutations then mutate the state (using useReducer), which finally causes a rerender.
For my example, I have two entities inside ./models. User (context/store provided globally) and Post(context/store provided on it's page):
// User.ts export interface User { id: number username: string website: string } // Post.ts export 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> ) } After providing the context, the store can be used as such:
const { actions, getters, mutations, state } = useContext(UserContext) useEffect(() => { actions.dispatch({ type: 'load-users' }) }, []) Are there any optimizations I can do? What are the biggest cons when comparing to redux? Here is the repo and sandbox, any feedback is appreciated.