Your app evolves during development. It’s a good idea to think about how your state is organized and how data flows between your components. This article will teach you how to structure states, share states between components, and maintain states in a React program.
Table of Contents
Input with State
You won’t be able to directly edit the UI with React. For example, you will not write commands such as “display success message” and so on. Instead, you’ll describe the UI you want to see for your component’s various visual states (“initial state,” “typing state,” “success state”), and then trigger state changes in response to user input.
You will be able to understand this by following this code
import React, { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<h2>Counter</h2>
<p>Current count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
In above attached code, the count state variable is set to zero. When we click the “Increment” or “Decrement” buttons, we use the ‘setCount’ function to update the count status. When the status changes, the component re-renders, and the updated count is shown on the screen.
Selecting a State Structure
The ability to structure state correctly can be the difference between a component that is easy to develop and debug and one that is a persistent source of issues. The most crucial idea is that no information should be repetitive or duplicated in the state. If there is an unneeded state, it is easy to lose track to update it and cause errors!
Here is example to demonstrate the usage of ‘useState’ hooks. This is a very basic form, in this form users will be able to input their age and calculate the year they were born.
import React, { useState } from 'react';
export default function AgeCalculator() {
const [age, setAge] = useState('');
const [birthYear, setBirthYear] = useState('');
function handleAgeChange(e) {
const enteredAge = parseInt(e.target.value, 10);
setAge(enteredAge);
const currentYear = new Date().getFullYear();
const calculatedYear = currentYear - enteredAge;
setBirthYear(calculatedYear);
}
return (
<>
<h2>Age Calculator</h2>
<label>
Enter your age:{' '}
<input
type="number"
value={age}
onChange={handleAgeChange}
/>
</label>
{age !== '' && (
<p>
You were born in the year: <b>{birthYear}</b>
</p>
)}
</>
);
}
Sharing the State between components
Sometimes, there will be scenarios that change the state of two components together. To achieve this, first, we have to remove the state from both and then transfer the state to the closest common parent and then send it down to them using props. We call that to “lifting state up”
import React, { useState } from 'react';
export default function App() {
const [activeTab, setActiveTab] = useState(0);
return (
<>
<h2>Tabs Example</h2>
<TabList activeTab={activeTab} onTabClick={setActiveTab} />
<Content activeTab={activeTab} />
</>
);
}
function TabList({ activeTab, onTabClick }) {
return (
<div className="tab-list">
<Tab title="Tab 1" isActive={activeTab === 0} onClick={() => onTabClick(0)} />
<Tab title="Tab 2" isActive={activeTab === 1} onClick={() => onTabClick(1)} />
</div>
);
}
function Tab({ title, isActive, onClick }) {
return (
<button className={`tab-button ${isActive ? 'active' : ''}`} onClick={onClick}>
{title}
</button>
);
}
function Content({ activeTab }) {
const tabContent = [
"Content for Tab 1",
"Content for Tab 2",
];
return (
<div className="tab-content">
{tabContent[activeTab]}
</div>
);
}
In above example code, the App component manages the ‘activeTab’ state. The ‘TabList’ component displays the tabs and their names. When a tab is selected , it uses the ‘onTabClick’ function to update the ‘activeTab’ state in the app component.
State Preserving & Restoring
When you re-render a component, React needs to decide which parts of the tree to keep (and update), and which parts to discard or re-create from scratch. React keeps the sections of the tree that “match up” with the previously displayed component tree by default.
Here you can see in below example, We have a list of colors, and when you choose one from the list, it keeps the color you chose. When you click a “Reset” button, the selection is reset to the default color.
import React, { useState } from 'react';
export default function ColorPicker() {
const [selectedColor, setSelectedColor] = useState('Default');
const colors = ['Red', 'Green', 'Blue', 'Yellow', 'Default'];
const handleColorSelect = (color) => {
setSelectedColor(color);
};
const handleReset = () => {
setSelectedColor('Default');
};
return (
<div>
<h2>Color Picker</h2>
<p>Selected Color: {selectedColor}</p>
<ul>
{colors.map((color, index) => (
<li key={index} onClick={() => handleColorSelect(color)}>
{color}
</li>
))}
</ul>
<button onClick={handleReset}>Reset</button>
</div>
);
}
The incorporation of state logic into a reducer
Components with many state changes split over several event handlers can be confusing. In these circumstances, you may combine all of the state update logic that exists outside of your component into a single function named “reducer.” Because they just specify the user’s “actions,” your event handlers become more succinct.
import React, { useReducer } from 'react';
export default function ItemListApp() {
const [items, dispatch] = useReducer(itemsReducer, initialItems);
function handleAddItem(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeItem(item) {
dispatch({
type: 'changed',
item: item,
});
}
function handleDeleteItem(itemId) {
dispatch({
type: 'deleted',
id: itemId,
});
}
return (
<>
<h1>Item List</h1>
<AddItem onAddItem={handleAddItem} />
<ItemList
items={items}
onChangeItem={handleChangeItem}
onDeleteItem={handleDeleteItem}
/>
</>
);
}
function itemsReducer(items, action) {
switch (action.type) {
case 'added': {
return [...items, { id: action.id, text: action.text, done: false }];
}
case 'changed': {
return items.map((item) =>
item.id === action.item.id ? action.item : item
);
}
case 'deleted': {
return items.filter((item) => item.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
let nextId = 3;
const initialItems = [
{ id: 0, text: 'Buy groceries', done: true },
{ id: 1, text: 'Go for a run', done: false },
{ id: 2, text: 'Read a book', done: false },
];
An ItemListApp component controls a list of items in this example. A itemsReducer function extracts the state logic for adding, modifying, and removing items. To control the status of the items, the useReducer hook is utilized. The rest of the code is identical to the previous example, including components for adding, displaying, and interacting with things.
Deeply Passing Data with Context
Props are often used to transmit data from a parent component to a child component. However, providing props might be problematic if you need to send a prop across many components or if multiple components want the same information.
Context allows the parent component to make certain information available to every component in the tree below it, regardless of how deep it is, without explicitly sending it through props.
import React, { createContext, useContext } from 'react';
// Create a context for the headings
const HeadingsContext = createContext();
// A custom hook to access the headings context
function useHeadings() {
return useContext(HeadingsContext);
}
// Heading component
function Heading({ children }) {
const headings = useHeadings();
return (
<h1>{children}</h1>
);
}
// Section component
function Section({ children }) {
return (
<div className="section">
{children}
</div>
);
}
export default function Page() {
return (
<HeadingsContext.Provider value={null}>
<Section>
<Heading>Title</Heading>
<Section>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Section>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Section>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
</Section>
</Section>
</Section>
</HeadingsContext.Provider>
);
}
To manage the heading data, we establish a context named ‘HeadingsContext’ in this example. Components can access the context using the useHeadings hook. This hook is used by the Heading component to render the specified heading text. The ‘Section’ component is a basic container.
The Page component wraps the complete structure in the HeadingsContext.Provider. This context provider can be highly nested, and the Heading components included inside it can access the context and display heading text as defined in your structure.
Scale using Context and Reducer
Reducers allow you to combine a component’s state update logic. Context allows you to pass data deep into other components. Reducers and context can be used in tandem to manage the state of a complicated screen. A parent component with complicated state controls it with a reducer in this method. Other components can read its status via context from anywhere in the tree. They can also send actions to that state to update it.
import React, { useReducer, createContext, useContext } from 'react';
// Create a context for tasks
const TasksContext = createContext();
// A custom hook to access the tasks context
function useTasks() {
return useContext(TasksContext);
}
// Reducer function for managing tasks
function tasksReducer(state, action) {
switch (action.type) {
case 'added':
return [...state, { id: action.id, text: action.text, done: false }];
case 'changed':
return state.map((task) =>
task.id === action.task.id ? action.task : task
);
case 'deleted':
return state.filter((task) => task.id !== action.id);
default:
throw new Error('Unknown action: ' + action.type);
}
}
// TasksProvider component to manage the state
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={{ tasks, dispatch }}>
{children}
</TasksContext.Provider>
);
}
// AddTask component
function AddTask() {
const { dispatch } = useTasks();
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
return (
<div>
<h2>Add a Task</h2>
<input type="text" placeholder="Task" />
<button onClick={() => handleAddTask("New Task")}>Add</button>
</div>
);
}
// TaskList component
function TaskList() {
const { tasks, dispatch } = useTasks();
function handleTaskChange(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleTaskDelete(id) {
dispatch({
type: 'deleted',
id: id,
});
}
return (
<div>
<h2>Task List</h2>
<ul>
{tasks.map((task) => (
<li key={task.id}>
<input
type="checkbox"
checked={task.done}
onChange={() => handleTaskChange({ ...task, done: !task.done })}
/>
<span>{task.text}</span>
<button onClick={() => handleTaskDelete(task.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Complete project', done: true },
{ id: 1, text: 'Go for a walk', done: false },
{ id: 2, text: 'Read a book', done: false },
];
export default TaskApp;
In this example, we used a reducer and context to control the status of tasks using a ‘TasksProvider‘ component. The ‘useTasks‘ hook enables the ‘AddTask‘ and ‘TaskList‘ components to retrieve and dispatch tasks. The reducer (tasksReducer) handles task creation, modification, and deletion. The remainder of the code structure is the same as in the previous example.
If you want you can refer Reacts Official Documentation for more info from here.
Learn about React Props simply and well organized way from here.
Ok Tech Geeks.. That’s it about React State. We will meet with next React tutorial!
Stay Tuned ! Comments your ideas and share the content with others