
ReactJS
Advanced state management with Context involves using `useReducer` for complex state transitions, implementing Redux-like patterns, and adding middleware for side effects. These patterns create scalable, maintainable state solutions.
`useReducer` is preferable to `useState` when state logic is complex or involves multiple related state updates:
import { useReducer, createContext, useContext, ReactNode, Dispatch } from "react";
// Define state shape
interface AppState {
user: { id: string; name: string } | null;
isLoading: boolean;
error: Error | null;
notifications: Array<{ id: string; message: string }>;
}
// Define action types
type AppAction =
| { type: "LOGIN_START" }
| { type: "LOGIN_SUCCESS"; payload: { id: string; name: string } }
| { type: "LOGIN_ERROR"; payload: Error }
| { type: "LOGOUT" }
| { type: "ADD_NOTIFICATION"; payload: { message: string } }
| { type: "REMOVE_NOTIFICATION"; payload: string }
| { type: "CLEAR_ERROR" };
// Reducer function
function appReducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case "LOGIN_START":
return { ...state, isLoading: true, error: null };
case "LOGIN_SUCCESS":
return {
...state,
user: action.payload,
isLoading: false,
error: null,
};
case "LOGIN_ERROR":
return { ...state, isLoading: false, error: action.payload };
case "LOGOUT":
return { ...state, user: null };
case "ADD_NOTIFICATION":
return {
...state,
notifications: [
...state.notifications,
{ id: Date.now().toString(), message: action.payload.message },
],
};
case "REMOVE_NOTIFICATION":
return {
...state,
notifications: state.notifications.filter(
(n) => n.id !== action.payload
),
};
case "CLEAR_ERROR":
return { ...state, error: null };
default:
return state;
}
}
// Context with state and dispatch
const AppContext = createContext<
| {
state: AppState;
dispatch: Dispatch<AppAction>;
}
| undefined
>(undefined);
function AppProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(appReducer, {
user: null,
isLoading: false,
error: null,
notifications: [],
});
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
// Consumer hook
function useAppState() {
const context = useContext(AppContext);
if (!context) {
throw new Error("useAppState must be within AppProvider");
}
return context;
}
// Component usage
function LoginForm() {
const { state, dispatch } = useAppState();
const handleLogin = async (email: string, password: string) => {
dispatch({ type: "LOGIN_START" });
try {
const user = await api.login(email, password);
dispatch({ type: "LOGIN_SUCCESS", payload: user });
dispatch({
type: "ADD_NOTIFICATION",
payload: { message: "Login successful!" },
});
} catch (error) {
dispatch({ type: "LOGIN_ERROR", payload: error as Error });
}
};
return (
<form onSubmit={(e) => handleLogin("user@example.com", "password")}>
{state.isLoading && <p>Loading...</p>}
{state.error && <p>Error: {state.error.message}</p>}
<button type="submit">Login</button>
</form>
);
}Implement a Redux-like pattern using Context for more predictable state management:
// Redux-style store
interface Store<S, A> {
getState: () => S;
dispatch: (action: A) => void;
subscribe: (listener: () => void) => () => void;
}
function createStore<S, A>(
reducer: (state: S, action: A) => S,
initialState: S
): Store<S, A> {
let state = initialState;
const listeners = new Set<() => void>();
return {
getState: () => state,
dispatch: (action: A) => {
const prevState = state;
state = reducer(state, action);
if (state !== prevState) {
listeners.forEach((listener) => listener());
}
},
subscribe: (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
// Redux store in Context
const StoreContext = createContext<Store<AppState, AppAction> | undefined>(
undefined
);
function StoreProvider({
children,
store,
}: {
children: ReactNode;
store: Store<AppState, AppAction>;
}) {
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
}
function useStore() {
const store = useContext(StoreContext);
if (!store) throw new Error("useStore outside provider");
return store;
}
// Selector hook for optimal re-renders
function useSelector<T,>(selector: (state: AppState) => T): T {
const store = useStore();
const [selected, setSelected] = useState(() => selector(store.getState()));
useEffect(() => {
const handleChange = () => {
const nextSelected = selector(store.getState());
setSelected(nextSelected);
};
const unsubscribe = store.subscribe(handleChange);
return unsubscribe;
}, [store, selector]);
return selected;
}
// Usage
function UserProfile() {
const user = useSelector((state) => state.user);
const { dispatch } = useStore();
return (
<div>
{user ? <h1>{user.name}</h1> : <p>Not logged in</p>}
<button onClick={() => dispatch({ type: "LOGOUT" })}>
Logout
</button>
</div>
);
}<details>
<summary>📚 More Examples</summary>
// Example 1: Middleware pattern with context
type Middleware<S, A> = (store: Store<S, A>) => (next: Dispatch<A>) => Dispatch<A>;
const loggerMiddleware: Middleware<AppState, AppAction> =
(store) => (next) => (action) => {
console.log("Dispatching:", action.type);
console.log("Previous state:", store.getState());
next(action);
console.log("Next state:", store.getState());
};
const persistenceMiddleware: Middleware<AppState, AppAction> =
(store) => (next) => (action) => {
next(action);
localStorage.setItem("appState", JSON.stringify(store.getState()));
};
// Example 2: AsyncThunk pattern
type AsyncAction<S, A, R = void> = (
dispatch: Dispatch<A>,
getState: () => S
) => Promise<R>;
function useAsyncDispatch() {
const store = useStore();
return (asyncAction: AsyncAction<AppState, AppAction>) => {
return asyncAction(
(action) => store.dispatch(action),
() => store.getState()
);
};
}
// Usage
const loginAsync: AsyncAction<AppState, AppAction> = async (dispatch, getState) => {
dispatch({ type: "LOGIN_START" });
try {
const user = await api.login();
dispatch({ type: "LOGIN_SUCCESS", payload: user });
} catch (error) {
dispatch({ type: "LOGIN_ERROR", payload: error as Error });
}
};
// Example 3: Thunk context
interface ThunkAction<S, A> {
(dispatch: Dispatch<A>, getState: () => S): Promise<void> | void;
}
const ThunkContext = createContext<{
dispatch: (action: AppAction | ThunkAction<AppState, AppAction>) => void;
getState: () => AppState;
} | undefined>(undefined);
function ThunkProvider({
children,
store,
}: {
children: ReactNode;
store: Store<AppState, AppAction>;
}) {
const dispatch = (action: AppAction | ThunkAction<AppState, AppAction>) => {
if (typeof action === "function") {
return action(store.dispatch, store.getState);
}
return store.dispatch(action);
};
return (
<ThunkContext.Provider value={{ dispatch, getState: store.getState }}>
{children}
</ThunkContext.Provider>
);
}</details>
Managing complex form state with validation and async submission:
// Form state machine
interface FormState {
values: Record<string, any>;
errors: Record<string, string>;
touched: Record<string, boolean>;
isSubmitting: boolean;
isDirty: boolean;
}
type FormAction =
| { type: "SET_FIELD"; payload: { name: string; value: any } }
| { type: "SET_ERROR"; payload: { name: string; error: string } }
| { type: "TOUCH_FIELD"; payload: string }
| { type: "START_SUBMIT" }
| { type: "SUBMIT_SUCCESS" }
| { type: "SUBMIT_ERROR"; payload: string }
| { type: "RESET" };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case "SET_FIELD":
return {
...state,
values: { ...state.values, [action.payload.name]: action.payload.value },
isDirty: true,
};
case "SET_ERROR":
return {
...state,
errors: { ...state.errors, [action.payload.name]: action.payload.error },
};
case "TOUCH_FIELD":
return {
...state,
touched: { ...state.touched, [action.payload]: true },
};
case "START_SUBMIT":
return { ...state, isSubmitting: true };
case "SUBMIT_SUCCESS":
return { ...state, isSubmitting: false, isDirty: false };
case "SUBMIT_ERROR":
return { ...state, isSubmitting: false };
case "RESET":
return {
values: {},
errors: {},
touched: {},
isSubmitting: false,
isDirty: false,
};
default:
return state;
}
}
const FormContext = createContext<
| {
state: FormState;
dispatch: Dispatch<FormAction>;
}
| undefined
>(undefined);
function useForm() {
const context = useContext(FormContext);
if (!context) throw new Error("useForm outside provider");
return context;
}
function FormProvider({
children,
onSubmit,
}: {
children: ReactNode;
onSubmit: (values: Record<string, any>) => Promise<void>;
}) {
const [state, dispatch] = useReducer(formReducer, {
values: {},
errors: {},
touched: {},
isSubmitting: false,
isDirty: false,
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
dispatch({ type: "START_SUBMIT" });
try {
await onSubmit(state.values);
dispatch({ type: "SUBMIT_SUCCESS" });
} catch (error) {
dispatch({
type: "SUBMIT_ERROR",
payload: (error as Error).message,
});
}
};
return (
<FormContext.Provider value={{ state, dispatch }}>
<form onSubmit={handleSubmit}>{children}</form>
</FormContext.Provider>
);
}
// Form field component
function FormField({
name,
label,
type = "text",
}: {
name: string;
label: string;
type?: string;
}) {
const { state, dispatch } = useForm();
const value = state.values[name] || "";
const error = state.errors[name];
const isTouched = state.touched[name];
return (
<div>
<label>{label}</label>
<input
type={type}
value={value}
onChange={(e) =>
dispatch({
type: "SET_FIELD",
payload: { name, value: e.target.value },
})
}
onBlur={() => dispatch({ type: "TOUCH_FIELD", payload: name })}
/>
{isTouched && error && <p>{error}</p>}
</div>
);
}| Pattern | Use Case | Complexity |
|---|---|---|
| useState | Simple state | Low |
| useReducer | Related updates | Medium |
| Redux-like | Large apps | High |
| MobX-like | Observable state | Very High |
Ready to practice? Challenges | Next: Performance Engineering
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward