Why you may not need global state management as much as you think
How to minimalize state management in a modern React application with libraries, hooks, and Context API
Following this guide, you will reevaluate the introduction of the global state management library in your application. Most of the state can be delegated to specialized libraries, and the rest will be negligible to introduce a global state management library.
The reactJS ecosystem is very active and full of ideas and libraries. There are many opinions on state management, there is no right or wrong. Some developer groups are too convinced that the chosen library or approach is the ultimate solution. Overusing libraries to solve problems that were not meant to solve is common practice.
This article will explain the concepts and provide a guide for problems you may face. There is not right or wrong approach. Your application may have specific requirements, and choosing your library mix may be influenced by them.
📀 What is the application state in React application?
Anything that can be changed during the application run and the application should react to it.
On every state change, the view would react to the given change by rendering an updated view.
✍️ What does setState
do?
setState()
schedules an update to a component’s state
object. When the state changes, the component responds by re-rendering.
♻️ Reasons for the component to re-render
- Update in State
- Re-rendering of the parent component only for children without memoization techniques (
React.memo
,useMemo
) - Update in prop of the component with memoization techniques (
React.memo
,useMemo
) - Context changes — if a component uses context consumer, it will be re-rendered every time the context provider’s value is changed.
- Hooks changes — if a state or context provider’s value is changed in the hook or nested hook (regardless of whether this state is returned in the hook value and memoized or not)
If you want to read more about react re-rendering follow this article https://www.developerway.com/posts/react-re-renders-guide
🎬 What actions are a common trigger to a state change?
- User input — text provided in the input, other events by keyboard or mouse interactions
- State from the server — the state change can be triggered when a request to a server is resolved or rejected
- Environment context —listening to user environment events, when the URL changes, online status, size of the device screen, orientation, and many more
🧑💻 Why do you want to use state management?
State management is solving the problem of accessing and setting the application’s state. It may also provide a set of rules that make reasoning about the application data flow clear and predictable.
The other benefit is development experience many libraries include dev tools in the form of browser extensions.
Popularization of Redux
Since previous versions of React did not have public API for context and people were unfamiliar with Javascript development, the rules of the flax pattern were welcome.
I am not mentioning other alternatives such as MobX that was popular at the time before hooks and public context API
Reason for the decline of Redux popularity
Redux was overused when providing the same state to sibling components was impossible with React it was the first proposed way to solve this problem in tutorials.
- React made context API public
- A lot of boilerplate code
Common miss use of Redux
Since Redux was one of the first things, people switching to React were learning. It becomes a way to interact with Router State, Handle Request logic, and do not forget forms with Redux Form. Using redux for everything has some benefits, like you had every action in one tool and debugger.
- using redux was making the whole app and sometimes simple components dependent on the redux. Tests had to mock redux
- redux code was too specific, and it is hard to structure code in redux to encourage sharing features between project
- redux uses performance optimizations but comparing, and recreating large state objects are costly in Javascript. Some wanted to solve this by immutable data structures, but performance benefits do not always exceed the costs and added complexity
Overusing libraries connected to the Redux state
- router state can be managed by React Router and query parameters with use-query-params without connecting router to redux
- 🚨 managing request state in redux seems like boilerplate overkill, but recently, you can use createAsyncThunk from Redux Toolkit to reduce boilerplate. You will not have any of the features of a React Query, SWR, or other libraries out of the box, like resolving requests from cache, auto-reload, on-tab focus, or many others.
There is another tool by Redux Toolkit team RTK Query that provides features of React Query or other libraries. See the comparision.
- form state stored in Redux Form was causing unnecessary rerenders on every input in the form. Libraries such as Formik improved and later introduced a more performant React Hook Form, reduced the rerenders, and made large dynamic forms not problematic again.
What is good about Redux
- flux pattern with actions and reducers
- optimized subscription to the state using state selectors
- documentation and ecosystem
- debugger browser extension with time traveling, and inspections
You should consider your project needs and redux, in some cases, solves project needs more than alternatives
Recoil as alternative state management to Redux
Use Recoil when you have rapidly changing UI, deeply nested data structures, and performance is important.
Support using complex data structures (trees, graphs, you name it). Each piece of shared state is called an atom, and atoms can be combined with selectors that only recompute whenever an atom changes. Asynchronicity is built in.
The documentation is great, and the API is starting to become stable. If you require a performant shared state in your application, this is a great tool to reach for.
React with hooks and Context API
Context can provide value to children components when a child component consumes the context, it will subscribe to changes.
🚀 Advantages
- provided by React works out of the box
- hooks encourage composition and reusability
- hooks can remove logic from component render functions
- hooks and context is easy to test when you extract the business logic into separate functions
- context does not have to cover the whole application, (theming, UI, and animation states)
- use hooks from the npm repository. No duplication or synchronization
🚨 Disadvantages
- performance implications, if not used properly with memoization
💡 Leave specialized tasks to specialized npm modules
- If the user can share the URL with query parameters that influence render, then store the state in React Router and query parameters with use-query-params
- fetch, cache, and updating data should be outsourced to a well-documented maintained external library without touching any “global state”. (React Query, SWR)
- leave handling forms to the external library with performant, flexible API, and mutate server data only when the form is submitted. (React Hook Form)
What state we will manage in the application?
When we follow these recommendations. The state will be negligible to introduce a global state management library.
Example
Initialize react-query
import { QueryClient, QueryClientProvider } from "react-query";
// Create a client
const queryClient = new QueryClient();
export default () => {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Page /> /* Or <Routers />*/
</QueryClientProvider>
);
}
Create src/api/userApi.ts (for this case, the axios request is mocked).
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
const mock = new MockAdapter(axios);
export enum Role {
admin = "admin",
user = "user",
}
mock.onGet("/user").reply((config) => [
200,
{
user: {
userId: config.params.userId ?? 1,
name: "John Smith",
role: Role.admin,
},
},
]);
export type User = {userId: number; name: string; role: Role};
export const userApi = {
getUser: ({userId}: {userId?: number} = {}) =>
axios.get<{user: User}>("/user", {params: {userId}}),
};
Use react-query for request
import { UserProperties } from "../components/UserProperties";
import { AxiosError } from "axios";
import { useQuery } from "react-query";
import { userApi } from "../api/userApi";
export const UserDetail = ({id = 2}) => {
const query = useQuery(`user-${id}`, () => userApi.getUser({ id }));
if (query.isLoading) {
return <p>Loading...</p>;
}
const user = query?.data?.data?.user;
if (query.isError) {
return (
<p>
Error loading user:{" "}
{(query?.error as AxiosError)?.message ?? "unknown error"}
</p>
);
}
return (
<>
<h1>User detail</h1>
{user && <UserProperties {...user} />}
</>
);
};
Refactor to separate hook (query key creation encapsulated, query key is used as cache key)
import { useQuery } from "react-query";
import { userApi } from "../api/userApi";
export const useUserQuery = ({ userId }: { userId?: number } = {}) => {
return useQuery(`user-${userId}`, () => userApi.getUser({ userId }));
};
useUserQuery hook usage, you can provide hooks as props or use default props
import {UserProperties} from "../components/UserProperties";
import {AxiosError} from "axios";
import {useUserQuery} from "../hooks/useUserQuery";
const defaultProps = {
userId: 2,
useUserQueryHook: useUserQuery,
};
export const UserDetail = ({userId, useUserQueryHook}: typeof defaultProps) => {
const query = useUserQueryHook({userId});
if (query.isLoading) {
return <p>Loading...</p>;
}
if (query.isError) {
return (
<p>
Error loading user:{" "}
{(query?.error as AxiosError)?.message ?? "unknown error"}
</p>
);
}
const user = query?.data?.data?.user;
return <>{user && <UserProperties {...user} />}</>;
};
UserDetail.defaultProps = defaultProps;
Code sandbox interactive example
Look at the option to provide a hook as a prop to the component.
Usage of context to <CurrentUserRole /> and implementation of useCurrentUserQuery hook.
import {UserDetail} from "../containers/UserDetail";
import {CurrentUserRole} from "../components/CurrentUserRole";
import {useCurrentUserQuery} from "../hooks/useCurrentUserQuery";
export const Page = () => (
<>
<CurrentUserRole />
<h1>Current User detail</h1>
<UserDetail useUserQueryHook={useCurrentUserQuery} />
<h1>User detail</h1>
<UserDetail />
</>
);
👏Clap, 👂follow for more awesome 💟#Javascript and ⚛️#React content.