Skip to main content

Using the Context API and useReducer hook

  • for state management we try to avoid external libraries and try to use vanilla React solution using the Context API and the useReducer hook

  • the main idea is tied to the very popular article by Kent. C. Dodds which provides a couple of ideas for dealing with state management:

    • keep state as local as possible
    • use component composition and the children prop to lift state up
    • separate state into different logical pieces instead of creating one huge global store
    • introduce the context only when prop drilling becomes a problem
    • creating domain specific contexts is okay, since not all state needs to be globaly acessible (maybe some state neeeds to be shared accross a small portion of our app, and not globally)
  • as far as the concrete implementation of state management within our project is concerned we use the redux pattern with the built-in tools which React provides

  • redux pattern visualisation:

Redux Pattern

  • a helper function which creates our context provider and exposes hooks for acessing the data within the context and the dispatch function:
  • reasoning behind storing data and dispatch function in separate contexts can be explored in this article
export default function makeContextStore<Action, State>(
reducer: Reducer<State, Action>,
initialState: State
): [React.FC, () => State, () => Dispatch<Action>] {
const StoreContext = createContext<State>({} as State);
const DispatchContext = createContext<Dispatch<Action>>(
{} as Dispatch<Action>
);

const StoreProvider: React.FC = ({ children }) => {
const [store, dispatch] = useReducer(reducer, initialState);

return (
<DispatchContext.Provider value={dispatch}>
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
</DispatchContext.Provider>
);
};

function useStore() {
return useContext(StoreContext);
}

function useDispatch() {
return useContext(DispatchContext);
}

return [StoreProvider, useStore, useDispatch];
}
  • defining necessary types, actions, and reducers for the context:
// App.types.ts
export interface AppState {
workspaces: Workspace[];
apps: App[];
selectedWorkspace?: Workspace;
selectedApp?: App;
}

export enum AppContextActionType {
SET_WORKSPACES = "SET_WORKSPACES",
SET_APPS = "SET_APPS",
SET_SELECTED_WORKSPACE = "SET_SELECTED_WORKSPACE",
SET_SELECTED_APP = "SET_SELECTED_APP",
}

export interface SetWorkspaces {
type: AppContextActionType.SET_WORKSPACES;
payload: Workspace[];
}

export interface SetApps {
type: AppContextActionType.SET_APPS;
payload: App[];
}

export interface SetSelectedWorkspace {
type: AppContextActionType.SET_SELECTED_WORKSPACE;
payload?: Workspace;
}

export interface SetSelectedApp {
type: AppContextActionType.SET_SELECTED_APP;
payload?: App;
}

export type AppContextAction =
| SetWorkspaces
| SetApps
| SetSelectedWorkspace
| SetSelectedApp;
// App.actions.ts
export const setWorkspacesAction = (payload: Workspace[]): SetWorkspaces => ({
type: AppContextActionType.SET_WORKSPACES,
payload,
});

export const setAppsAction = (payload: App[]): SetApps => ({
type: AppContextActionType.SET_APPS,
payload,
});

export const setSelectedWorkspaceAction = (
payload?: Workspace
): SetSelectedWorkspace => ({
type: AppContextActionType.SET_SELECTED_WORKSPACE,
payload,
});

export const setSelectedAppAction = (payload?: App): SetSelectedApp => ({
type: AppContextActionType.SET_SELECTED_APP,
payload,
});
// App.reducer.ts
export const AppReducer: Reducer<AppState, AppContextAction> = (
state,
action
) => {
switch (action.type) {
case AppContextActionType.SET_WORKSPACES:
return { ...state, workspaces: action.payload };
case AppContextActionType.SET_APPS:
return { ...state, apps: action.payload };
case AppContextActionType.SET_SELECTED_WORKSPACE:
return { ...state, selectedWorkspace: action.payload };
case AppContextActionType.SET_SELECTED_APP:
return { ...state, selectedApp: action.payload };
}
};
  • creating and exposing the context:
// App.context.tsx
const initialState: AppState = {
workspaces: [],
apps: [],
selectedWorkspace: undefined,
selectedApp: undefined,
};

const [AppContextProvider, useAppContext, useAppDispatch] = makeContextStore(
AppReducer,
initialState
);

export { AppContextProvider, useAppContext, useAppDispatch };
  • context usage examples:
// App.tsx
export default function App() {
return (
<AppContextProvider>
<App />
</AppContextProvider>
);
}
// Workspaces.tsx
export default function Workspaces() {
const { workspaces } = useAppContext();

return <WorkspaceList workspaces={workspaces} />;
}
// SelectWorkspace.tsx
export default function SelectWorkspace() {
const dispatch = useAppDispatch();

const handleSelectWorkspace = (workspace: Workspace) => {
// other logic ...

dispatch(setSelectedWorkspaceAction(workspace));
};

return <WorkspaceButton onClick={handleSelectWorkspace} />;
}