Redux is a library that helps to manage the state of your application.
Redux should be used in medium to large single-page applications with complex data flow. Redux can add unnecessary complexity to your application if your application is really small with a simple data flow. On the other hand, in v16.3 react team introduced Context which also helps to manage the state of a react application.
I’m Ashraful, working as a frontend developer in Anymind Group. Today I’m going to share how we are managing a part of our global application state in AnyTag/AnyCreator with react context instead of using redux.
Why We Need State Management Tool?
For frontend applications, we can think of the state as an internal database for the application that contains all the necessary pieces of information that we need. There’s no hard and fast rule for the structure of the state. That totally depends on the requirements.
If we have multiple components that share the same piece of data and they don’t have any parent-child relationship then redux comes to solve the problem. In redux, we have a single piece of identical data that is in sync with all of the components that are using that piece of data. In redux, we call it store.
One alternate solution for sharing the same piece of data can be using event raising in Angular or using props in react. Later in a large codebase, this solution will turn into event spaghetti, and in react application we will face the problem of prop drilling. From all over the codebase we will start getting events and it will be hard to keep track of all of them. So the data flow becomes unpredictable.
Facebook faced this problem back in 2014 that’s why they introduced Flux Architecture. Redux is the simplified and lightweight implementation of Flux Architecture. With react context, we can solve some of the above-mentioned problems like sharing app state in different components with much simple and a few lines of codes. But that also comes with an extra cost. We will see that later.
There’s a lot of different approach for using context in react application. But we found implementing it with the useReducer hook and redux pattern is more elegant and maintainable. Before using redux, let’s have a look at the main concepts of redux, and that will help us to use context in a redux pattern.
Main Concept of Redux
- Store – A single JS object that contains the state of the application.
- Actions – A plain JS object that represents something has happened. Similar to events.
- Reducer – A function that defines how the state changes in response to an action. It does not modify the state. It always returns a new state.
In Redux, we don’t directly update the state. First we will dispatch an action. That will go to the store. The store passes the action to the root reducer. Based on the action the reducer returns a new state and the store updates internally.
Using Context in React Application
For the demonstration, the idea is to create a Header component that will be responsible for showing the Login or Logout button based on the status from the store. We will dispatch the LOGIN and LOGOUT action from the Header component.
We will do everything with 5 simple steps.
- Create Actions
- Create a State
- Create Reducer
- Create Context
- Create Header component
1. Creating Actions
action.ts
export enum Action {
LOGIN = "LOGIN",
LOGOUT = "LOGOUT"
}
2. Creating State
Now we have to determine what kind of data we want to persist in our context store. So we will create an interface for that and also the initial value for the state. In our case, we will just store the username.
state.ts
export interface StateType {
username: string | null;
}
export const initialState: StateType = {
username: null,
};
3. Creating Reducer
Next, we will create the reducer. Reducer is a pure function. It is a function with two arguments. First is the default state which is typed StateType and second is an object with type
and payload
properties. type
property defines the action type and payload
contains optional data from the dispatcher.
Pure Function — If we give the same input and always get the same output from a function then the function is a pure function. So there will be no side effects. It gives us easy testability and easy way of implementing undo/redo features.
reducer.ts
import { StateType, initialState } from "./state";
import { Action } from "./action";
export type ActionType = {
type: Action,
payload?: StateType,
};
const reducer = (state: StateType, { type, payload }: ActionType) => {
switch (type) {
case Action.LOGIN:
return { ...state, ...payload };
case Action.LOGOUT:
return initialState;
default:
return state;
}
};
export default reducer;
For LOGIN action we will pass the username
as a payload when we will dispatch the action. For LOGOUT we don’t need any kind of data as a payload. We will just reset the state with the initial state value.
4. Creating Context
Now we will create the context. First, we will define the ContextType
and then we will create a Store. After that, we will create the StoreProvider
.
context.tsx
import React, { createContext, Dispatch, useReducer } from "react";
import reducer, { ActionType } from "./reducer";
import { StateType, initialState } from "./state";
export interface ContextType {
state: StateType;
dispatch: Dispatch<ActionType>;
}
export const RootStore = createContext({} as ContextType);
export const RootStoreProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer<React.Reducer<StateType, ActionType>>(
reducer,
initialState
);
const value = { state, dispatch };
return <RootStore.Provider value={value}>{children}</RootStore.Provider>;
};
Now we will wrap our root component with this RootStoreProvider to access the store globally.
index.tsx
const App = () => {
return <RootStoreProvider>....</RootStoreProvider>;
};
Now we are all set. We will create a simple Header component from where we will use RootStore to get the user information and also to update the information.
5. Creating Header Component
Header/index.tsx
import React from "react";
const Header = () => {
const username = null;
const toggleLoginLogoutHandler = () => {};
return (
<div
style={{
backgroundColor: "#dee5ec",
padding: 10,
display: "flex",
justifyContent: "space-between",
}}
>
<div>
<p>Header</p>
{username && <p>Logged in as {username}</p>}
</div>
<button onClick={toggleLoginLogoutHandler}>
{username ? "Logout" : "Login"}
</button>
</div>
);
};
export default Header;
Now let’s use our RootStore to get the username and also dispatch the login and logout action.
import React, { useContext } from "react";
import { RootStore } from "../../context";
import { Action } from "../../action";
const Header = () => {
const {
state: { username },
dispatch,
} = useContext(RootStore);
const toggleLoginLogoutHandler = () => {
if (username) {
dispatch({ type: Action.LOGOUT });
} else {
dispatch({ type: Action.LOGIN, payload: { username: "Ashraful" } });
}
};
// ....
};
Finally, update the root component.
import Header from "./src/components/Header";
const App = () => {
return (
<RootStoreProvider>
<Header />
</RootStoreProvider>
);
};
That’s all. We have implemented the react context with all of the concepts of redux components.
Finally, Should You Replace Redux with Context?
No, not at all. Redux is not replaceable with context at the same time redux is not a silver bullet for all kinds of state management. It totally depends on the problem we are trying to solve.
The main downside for context is with any kind of state update, all the components consuming that context will be rerendered. It will be the worst decision to use context in such a case where you will change the state very frequently and a lot of components are dependent on that state.
On the other hand, using redux we will not face that re-rendering issue. From the reducer we never mutate the state, we always return to a new state. As redux using immutable data structures internally so react itself takes very little time to perform its Reconciliation process. But every good thing comes with a cost and for redux, we need to write a lot of additional codes and in return, we will get a predictable data flow in the whole application with great debugging tools and testability.
In AnyTag/AnyCreator we have a relatively very small state with a few properties that we share across the whole application and that data gets updated only once after the authentication process. Using redux in this situation will be overkill for the whole application. That’s why we choose context over redux.