5
\$\begingroup\$

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 }

enter image description here

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 useUserStore

After 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 new

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.

askedMar 22, 2020 at 19:06
htmn's user avatar
\$\endgroup\$

0

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.