Skip to main content
Notice removed Authoritative reference needed by CommunityBot
Bounty Ended with no winning answer by CommunityBot
Tweeted twitter.com/StackCodeReview/status/1242556934986829828
Notice added Authoritative reference needed by htmn
Bounty Started worth 50 reputation by htmn
Explain edit
Source Link
htmn
  • 208
  • 1
  • 8

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.

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.

Update code, add graph
Source Link
htmn
  • 208
  • 1
  • 8

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:

enter image description here

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.

Edit new

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:

enter image description here

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.

Edit new

Source Link
htmn
  • 208
  • 1
  • 8

React Context & Hooks custom Vuex like store

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.