Redux is a library for managing the global application state for web apps. It is widely used with React but, Redux adds to the overall size of your app. For small apps, we can use Context API with useReducer to manage the global state.
What is Context API?
In react we often come to a point when we have to pass props to components. But, the problem occurs when we have to pass props to nested components. We may have to pass props to a component that doesn't need to use that data. This is also called prop drilling. Here, Context API comes to the rescue. It helps us to pass props across all components.
According to React docs, “Context provides a way to pass data through the component tree without having to pass props down manually at every level.” Context API gives us a clear and easy way to share states between different components without passing down the props all the time.
Context API
- Provider provide state to children components. It passes the value of context and components which are consuming this value will re-render when value change.
- Consumer are components that consume that state.
- createContext() create new context using createContext()
Let's Create Context
// data-context.js
import { createContext, useContext } from "react";
// creating context
const DataContext = createContext();
export const DataProvider = ({children}) => {
<DataContext.Provider value={{some_value}}>
{children}
</DataContext.Provider>
}
// exporting context as "useData" custom hook
export const useData = () => useContext(DataContext);
What is useReducer?
useReducer hook is a better alternative to useState when you have complex logic and state updates. Base logic of hook is similar to javascript Array.prototype.reduce()
.
const [state, dispatch] = useReducer(reducer, initialState)
useReducer accepts two argument's: reducer function and initialState, and return two values as current value of the state and dispatch function.
function reducer(state, action)
reducer function takes state and action. It updates the value of the state and action determines how the value of the state will be updated.
dispatch is used to pass the action to the reducer function. It dispatches an action that is used to update the state.
dispatch
dispatches an object in the format:
{ type: "ACTION_TYPE", payload: "value" }
Implement Context API and useReducer.
Let's build a simple create, read and delete operation of the Todo app.
1. Create InitialState
const initialState = {
todos: []
};
So here we create initialState for our context
. todos
is an empty array in which a new todo task will be added.
2. Create Context
As shown at the start of the article we create our context.
import { createContext, useContext, useReducer } from "react";
const initialState = {
todos: []
};
// reducer-function
// creating context
const DataContext = createContext();
export const DataProvider = ({ children }) => {
// useReducer here
return (
<DataContext.Provider value={{some_value}}>
{children}
</DataContext.Provider>
);
};
// exporting context as "useData"
export const useData = () => useContext(DataContext);
Now let's add the reducer function and pass values to context.
3. reducer function
const reducer = (state, action) => {
switch (action.type) {
case "ADD_TODO":
return { ...state, todos: state.todos.concat(action.payload) };
case "DELETE_TODO":
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload)
};
default:
return state;
}
};
We create a reducer function. Depending on action.type
we perform updates on the state. action.payload
contains a value that will help the reducer to perform updates.
For example:
For ADD_TODO
, action.payload will contain new todo which will be added to the state.
4. Adding useReducer and passing value to Context
// state is destructured here
const [{ todos }, dispatch] = useReducer(reducer, initialState);
Now we pass value to Context. In Code, it looks like this:
import { createContext, useContext, useReducer } from "react";
// initialState
const initialState = {
todos: []
};
// reducer function
const reducer= (state, action) => {
switch (action.type) {
case "ADD_TODO":
return { ...state, todos: state.todos.concat(action.payload) };
case "DELETE_TODO":
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload)
};
default:
return state;
}
};
// creating context
const DataContext = createContext();
export const DataProvider = ({ children }) => {
// Initialized useReducer and destructure state
const [{ todos }, dispatch] = useReducer(reducer, initialState);
return (
// passing value to context
<DataContext.Provider value={{ todos, dispatch }}>
{children}
</DataContext.Provider>
);
};
// exporting context as "useData" custom hook
export const useData = () => useContext(DataContext);
Now Context + useReducer is completed.
In App.js
import { useState } from "react";
import { useData } from "./context/data-context";
import { v4 } from "uuid";
import "./styles.css";
export default function App() {
// get dispatch and state i.e todos from context
const { todos, dispatch } = useData();
const [newTask, setTask] = useState("");
const addNewTodo = () => {
dispatch({ type: "ADD_TODO", payload: { id: v4(), todo: newTask } });
};
const deleteTask = (todoId) => {
dispatch({ type: "DELETE_TODO", payload: todoId });
};
return (
<div className="App">
<div>
<input
type="text"
placeholder="Add todo"
onChange={(e) => setTask(e.target.value)}
/>
<button onClick={() => addNewTodo()}>ADD</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<div>{todo.todo}</div>
<button onClick={() => deleteTask(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
What's happening here?
- We import
useData
from context.const { todos, dispatch } = useData();
extract whatever value we need from the context. addNewTodo
dispatches action ADD_TODO with payload of newTask and iddeleteTask
dispatcher action DELETE_TODO with a payload of taskId, which we use to remove a task from the state.
import { v4 } from "uuid";
is random id generator
Here CodeSandBox
Conclusion
- Context API helps us to pass data directly to components, helps us to avoid prop drilling.
- Using useReducer we create our store and use Context to pass the store.
- For more complex apps you should use Redux