State Management for your React Apps - Simplified!
Dennys Jose M.
Software Developer | Senior Front-End (JavaScript ES6, React, Next, Redux, Angular, Node) | Jr CyberSecurity (Owasp, Ethical Hacking)
Elegant State Management Solution using Context API & React Hooks
Hi there fellow developers! ?? In this blog, I will show you how to implement a scalable state management solution for your React apps - without the need of any third party library or toolkit!
If you guys are unfamiliar with state management and why you might need it for your projects - there are countless (and I mean literally hundreds) of articles explaining the benefits of state management in frontend apps. I'll link to a great one?here?so you can read to your heart's content.
Now I am aware that there are many solutions for state management in React apps and you've probably heard the term?Redux?thrown around when someone mentions React state management.
Redux is a fantastic tool and is very popular in the React community - and rightly so. With its great documentation and robust set of developer tools it has become the gold standard for state management in frontend JavaScript apps.
However, did you know that you can achieve similar results using native tools baked right into React? That's what we will attempt to accomplish today using React's?Context API?and?Hooks.
Simply put Context API provides a way to pass data between React components without passing down props - something which can quickly become very cumbersome. Context API itself is cool and all - but combine it with React hooks and you're looking a pretty nice state management solution for your projects. And the?best?thing - no dependencies and no need to install any third party library!
I think I've taken way too long in explaining - now let's get to the fun part i.e. coding!
Let's write some code!
Now we can start to implement the Context API.
Create a new folder?which will contain all the files for state management we'll be needing for this project. Within this folder create a new file?which will serve as the basis for creating and exporting?context.?src/contextContext.js
Context.js
import { createContext } from "react";
export const context = createContext();
Create another file?which we will use to create the initial state for our project and also pass it to the?Provider?component.src/context/GlobalState.js
GlobalState.js
import React, { useReducer } from "react";
import { GlobalContext } from "./AppContext";
import AppReducer from "./AppReducer";
// Define initial state
const initialState = {
todos: [],
};
// Provider component
export const GlobalProvider = ({ children }) => {
const [state, dispatch] = useReducer(AppReducer, initialState);
// Actions
function deleteTodo(id) {
dispatch({
type: "DELETE_TODO",
payload: id,
});
}
function addTodo(todo) {
dispatch({
type: "ADD_TODO",
payload: todo,
});
}
return (
<GlobalContext.Provider
value={{
todos: state.todos,
deleteTodo,
addTodo,
}}
>
{children}
</GlobalContext.Provider>
);
};
Woah, that's a lot of code! Don't sweat - I'll explain in simple terms. Basically, Context API requires a?Provider?component and?initial state?to do its magic. We pass the state to the Provider and that state is accessible throughout our React app. With me so far? Good.
In this file we import the?Context?we exported earlier,?AppReducer?which we will create shortly and a React hook called?useReducer?which is an alternative to?useState. It allows you to pass an initial state and also define actions for manipulating the state. According to the React docs:
Woah, that's a lot of code! Don't sweat - I'll explain in simple terms. Basically, Context API requires a?Provider?component and?initial state?to do its magic. We pass the state to the Provider and that state is accessible throughout our React app. With me so far? Good.
In this file we import the?Context?we exported earlier,?AppReducer?which we will create shortly and a React hook called?useReducer?which is an alternative to?useState. It allows you to pass an initial state and also define actions for manipulating the state. According to the React docs:
Then we define the initial state which is an array of todos and create a Provider component to define our actions -?addTodo?and?deleteTodo. Actions are functions that are?dispatched?whenever we want to change the state - like adding a new todo or deleting an existing one.
If you are unfamiliar with this terminology, I would suggest reading up on the?Flux Pattern.
Each action includes a?payload?which is a value passed to it when the action is invoked. Think of it like calling a function by passing a value to it.
领英推荐
Finally, we pass the initial state and actions to the Provider and export it. The components which are wrapped inside the Provider will have access to the state and actions.
Now we will create the final piece of the puzzle - the?Reducer?which is responsible for changing the state and returning the updated state. Create a new file called?inside the?folder and add the following code:src/context/AppReducer.jscontext
AppReducer.js
export default (state, action) => {
switch (action.type) {
case "DELETE_TODO":
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
};
case "ADD_TODO":
return {
...state,
todos: [action.payload, ...state.todos],
};
default:
return state;
}
};
Here we use a simple?Switch Case?statement to update the state depending on the type of action invoked and return the updated state. Take care to?NOT?mutate the state - rather make a copy of the state using the?spread?operator first before changing it.
All that is left now is to wrap the?App?component with the Provider component so that the state is accessible to all components within the application.
import "./App.css";
import { GlobalProvider } from "./context/GlobalState";
function App() {
return (
<GlobalProvider>
<div className="App">
<h1>React Context Todo</h1>
</div>
</GlobalProvider>
);
}
export default App;
OK, now that you have created and exported the context and defined the actions, you can now begin using it in your components.
Finishing the App - Consuming Context
Now let's complete the todo app using Context API.
I won't go into the details of creating React components since that would take way too long. I am assuming you're familiar with the basics of React and JavaScript ES6.
Create a new folder?and add the following files into it:src/components
AddTodo.js
import { useState, useContext } from "react";
import { GlobalContext } from "../context/AppContext";
import { nanoid } from "nanoid";
export const AddTodo = () => {
const [text, setText] = useState("");
const { addTodo } = useContext(GlobalContext);
const onSubmit = (e) => {
e.preventDefault();
const newTodo = {
id: nanoid(),
text: text,
};
addTodo(newTodo);
setText("");
};
return (
<form onSubmit={onSubmit}>
<input
type="text"
placeholder="Add Todo..."
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add Todo</button>
</form>
);
};
To get the values from the Context, we import another React hook -?useContext?and pass it the?GlobalContext. We can then de-structure the required values from it. In the?AddTodo?component we have a simple input to get the text that the user types. For generating a unique id we are using the?nanoid?npm package.
TodoList.js
export const Todo = ({ todo, deleteTodo }) => {
return (
<div>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>x</button>
</div>
);
};
Here is the?App?component after the application is complete:
import "./App.css";
import { AddTodo } from "./components/AddTodo";
import { TodoList } from "./components/TodoList";
import { GlobalProvider } from "./context/GlobalState";
function App() {
return (
<GlobalProvider>
<div className="App">
<h1>React Context Todo</h1>
<AddTodo />
<TodoList />
</div>
</GlobalProvider>
);
}
export default App;
And here is the final working app:
I admit its not pretty but it gets the job done and demonstrates the point pretty nicely. Feel free to add more features or style the app using your favorite CSS framework to make it more exciting.