State management is a key aspect of building robust and scalable React applications. In React, state refers to the data that determines how a component is rendered and behaves. The state can be thought of as the internal memory of a component, which is used to store and update data over time.
We have state management in React to manage the state of our application. The state is an important part of React components and represents the current state of the component. When the state changes, React re-renders the component with the updated state, which updates the UI accordingly.
However, as the application grows larger, it becomes difficult to manage the state of the entire application. In a complex application with many components, it's not practical to store all the states in the individual components, especially if the state needs to be shared between multiple components.
This is where state management libraries such as Redux, MobX, and React Query come in. They provide a centralized location to store and manage the state of the application, making it easier to manage and update the state of the application. With these libraries, we can share the state between components, make it easier to test and debug our application and improve the performance of our application.
State management libraries also provide features such as time-travel debugging, where we can go back in time and inspect the state of our application at a certain point in time, making it easier to identify and fix bugs.
Now! Let's dive into some examples of state management in React.
Local State
A local state refers to a state that is managed within a component. This is useful for a state that is only used within that component and doesn't need to be shared with other components.
Here's an example of a local state in a simple counter component:
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={handleClick}>Increment</button>
</div>
);
}
export default Counter;
In this example, we define a count
state variable using the useState
hook. We also define a handleClick
function that increments the count when the button is clicked.
When the button is clicked, the count
state is updated with the new value, and the component is re-rendered with the updated count.
Context API
The Context API is a way to manage a state that can be accessed by multiple components, without the need to pass props down through the component tree.
Here's an example of using the Context API to manage a theme in a React application:
import React, { useState, useContext } from "react";
const ThemeContext = React.createContext();
function App() {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div className={theme}>
<h1>Theme: {theme}</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
<ChildComponent />
</div>
</ThemeContext.Provider>
);
}
function ChildComponent() {
const { theme } = useContext(ThemeContext);
return (
<div>
<h2>Child Component</h2>
<p>Current Theme: {theme}</p>
</div>
);
}
export default App;
In this example, we define a ThemeContext
using the React.createContext
method. We then use the useState
hook to define a theme
state variable and a toggleTheme
function that toggles the theme between "light" and "dark".
We wrap our application in a ThemeContext.Provider
, passing in the theme
state and toggleTheme
function as the context value. We then render a child component (ChildComponent
) that uses the useContext
hook to access the theme value from the context.
When the toggleTheme
function is called, the theme
state is updated with the new value, and the component tree is re-rendered with the updated theme.
Redux
Redux is a popular state management library for React that provides a centralized store for managing the state predictably and efficiently.
Here's an example of using Redux to manage a counter in a React application:
import React from "react";
import { createStore } from "redux";
import { Provider, connect } from "react-redux";
// Define the initial state and reducer function
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
default:
return state;
}
}
// Create the Redux store
const store = createStore(counterReducer);
// Define the Counter component
function Counter(props) {
const { count, dispatch } = props;
const handleIncrement = () => {
dispatch({ type: "INCREMENT" });
};
const handleDecrement = () => {
dispatch({ type: "DECREMENT" });
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</div>
);
}
// Connect the Counter component to the Redux store
const ConnectedCounter = connect((state) => ({ count: state.count }))(Counter);
// Render the app
function App() {
return (
<Provider store={store}>
<ConnectedCounter />
</Provider>
);
}
export default App;
In this example, we define an initial state and a reducer function that updates the state based on different actions. We then create a Redux store using the createStore
method from the redux
library.
We define a Counter
component that displays the current count and provides buttons to increment and decrements the count. We use the connect
method from the react-redux
library to connect the Counter
component to the Redux store, allowing it to dispatch actions and access the current count value.
Finally, we wrap the ConnectedCounter
component in a Provider
component, passing in the Redux store as a prop.
MobX
As mentioned earlier, MobX is a state management library that uses observable objects and reactions to manage the state more reactively and intuitively.
Here's an example of using MobX to manage a list of tasks in a React application:
import React from "react";
import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";
// Define the Task class with observable state
class Task {
id;
title;
completed;
constructor({ id, title, completed }) {
this.id = id;
this.title = title;
this.completed = completed;
makeAutoObservable(this);
}
toggle() {
this.completed = !this.completed;
}
}
// Define the TaskList class with observable state
class TaskList {
tasks = [];
constructor(tasks) {
tasks.forEach((task) => this.tasks.push(new Task(task)));
makeAutoObservable(this);
}
get completedTasks() {
return this.tasks.filter((task) => task.completed);
}
get incompleteTasks() {
return this.tasks.filter((task) => !task.completed);
}
}
// Define the TaskList component with observer
const TaskListComponent = observer(({ taskList }) => {
return (
<div>
<h1>Tasks</h1>
<ul>
{taskList.incompleteTasks.map((task) => (
<li key={task.id} onClick={() => task.toggle()}>
{task.title}
</li>
))}
</ul>
<h2>Completed Tasks</h2>
<ul>
{taskList.completedTasks.map((task) => (
<li key={task.id} onClick={() => task.toggle()}>
{task.title}
</li>
))}
</ul>
</div>
);
});
// Create a new TaskList instance
const taskList = new TaskList([
{ id: 1, title: "Learn React", completed: true },
{ id: 2, title: "Build an App", completed: false },
{ id: 3, title: "Deploy to Production", completed: false },
]);
// Render the app
function App() {
return <TaskListComponent taskList={taskList} />;
}
export default App;
In this example, we define a Task
class with the observable state for the id
, title
, and completed
properties. We also define a TaskList
class with the observable state for the tasks
array.
We then create a TaskListComponent
component that displays the incomplete and completed tasks and allows the user to toggle the completed state of each task. We use the observer
function from the mobx-react-lite
library to ensure that the component re-renders when the state changes.
Finally, we create a new TaskList
instance and render the TaskListComponent
with the taskList
prop.
React Query
React Query is a state management library that provides a powerful and flexible way to manage remote data in a React application.
Here's an example of using React Query to fetch and display a list of todos from an API:
import React from "react";
import { useQuery } from "react-query";
// Define the TodoList component
function TodoList() {
const { isLoading, error, data } = useQuery("todos", () =>
fetch("https://jsonplaceholder.typicode.com/todos").then((res) =>
res.json()
)
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>Todos</h1>
<ul>
{data.map((todo) => (
<li key={todo.id}>
{todo.completed ? "✅" : "❌"} {todo.title}
</li>
))}
</ul>
</div>
);
}
// Render the app
function App() {
return <TodoList />;
}
export default App;
In this example, we define a TodoList
component that uses the useQuery
hook from React Query to fetch the todos data from the API. The useQuery
hook takes two arguments: a key to identify the query (in this case, "todos"), and a function that returns the data.
While the data is being fetched, the isLoading
flag is true, and we display a "Loading..." message. If there's an error fetching the data, the error
object will contain an error message that we can display.
Once the data is fetched, we display the list of todos with a checkmark emoji for completed todos and an "X" emoji for incomplete todos.
React Query provides many other features, such as caching, pagination, and optimistic updates, which make it a powerful tool for managing remote data in a React application.
In summary, state management in React is important because it makes it easier to manage and update the state of the application, share the state between components, and improve the performance and maintainability of our application.