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:
- 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} />;
}