The Problem: Adding State to a Functional Component
We have a React functional component called UseState that currently doesn't manage any state. Our task is to enhance this component to:
- Switch between light and dark themes when buttons are clicked
- Display a counter that can be incremented and decremented
- Add a bonus feature to toggle the theme with a single button
Each of these features requires maintaining state across component renders, which is the perfect use case for React's useState hook.
Understanding Our Project Structure
Let's first understand the files we're working with:
App.jsx: The main App component that renders our UseState componentUseState.jsx: The component we need to modify to implement state managementUseState.css: Contains styles for our component, including classes for light and dark themesindex.js: Exports our UseState component- Test files:
01-theme.test.jsx,02-counter.test.jsx, and03-toggle-theme.test.jsx
Planning Our Solution
- Import the useState hook from React
- Create a state variable for the theme with an initial value of 'light'
- Add the theme state as a class to the container div
- Implement the Dark and Light buttons to change the theme
- Create a state variable for the counter with an initial value of 0
- Display the counter state in the h2 element
- Implement the Increment and Decrement buttons
- Use callback functions in the state updates for best practices
- Add a Toggle Theme button for the bonus feature
Step-by-Step Implementation
Step 1: Import useState and CSS
We need to import the useState hook from React and our CSS file to use in our component.
import { useState } from 'react';
import './UseState.css';
const UseState = () => {
// Component code will go here
return (
// JSX will go here
);
};
export default UseState;
Step 2: Create a State Variable for the Theme
The useState hook returns an array with two elements: the current state value and a function to update it. We'll destructure these values and set an initial state of 'light'.
import { useState } from 'react';
import './UseState.css';
const UseState = () => {
// Create state for the theme
const [theme, setTheme] = useState('light');
return (
// JSX will go here
);
};
export default UseState;
Here, theme is our state variable that will hold either 'light' or 'dark', and setTheme is the function we'll use to update it.
Step 3: Add the Theme State as a Class
Now, we need to apply our theme state as a class to the container div. This will allow the CSS to style the component based on whether the theme is 'light' or 'dark'.
import { useState } from 'react';
import './UseState.css';
const UseState = () => {
const [theme, setTheme] = useState('light');
return (
<div className={`state ${theme}`}>
<h1>UseState Component</h1>
<button>Dark</button>
<button>Light</button>
<h2>DISPLAY COUNT HERE</h2>
<button>Increment</button>
<button>Decrement</button>
</div>
);
};
export default UseState;
We're using a template literal with backticks (`) to combine the fixed 'state' class with our dynamic theme class. This creates a string like "state light" or "state dark".
Step 4: Implement Theme-Switching Buttons
Next, we'll add click handlers to our Dark and Light buttons to update the theme state.
import { useState } from 'react';
import './UseState.css';
const UseState = () => {
const [theme, setTheme] = useState('light');
return (
<div className={`state ${theme}`}>
<h1>UseState Component</h1>
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('light')}>Light</button>
<h2>DISPLAY COUNT HERE</h2>
<button>Increment</button>
<button>Decrement</button>
</div>
);
};
export default UseState;
When a button is clicked, the onClick handler calls setTheme with the appropriate value, updating our state and causing React to re-render the component with the new theme.
Step 5: Create a State Variable for the Counter
Now let's add another state variable to manage our counter. We'll use useState again with an initial value of 0.
import { useState } from 'react';
import './UseState.css';
const UseState = () => {
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
return (
<div className={`state ${theme}`}>
<h1>UseState Component</h1>
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('light')}>Light</button>
<h2>DISPLAY COUNT HERE</h2>
<button>Increment</button>
<button>Decrement</button>
</div>
);
};
export default UseState;
This demonstrates how we can use multiple useState hooks in a single component to manage different pieces of state independently.
Step 6: Display the Counter State
Let's replace the placeholder text with our actual count state.
import { useState } from 'react';
import './UseState.css';
const UseState = () => {
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
return (
<div className={`state ${theme}`}>
<h1>UseState Component</h1>
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('light')}>Light</button>
<h2>{count}</h2>
<button>Increment</button>
<button>Decrement</button>
</div>
);
};
export default UseState;
Now our h2 element will display the current value of the count state, which is initially 0.
Step 7: Implement Increment and Decrement Buttons
Similar to the theme buttons, we'll add onClick handlers to our Increment and Decrement buttons to update the count.
import { useState } from 'react';
import './UseState.css';
const UseState = () => {
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
return (
<div className={`state ${theme}`}>
<h1>UseState Component</h1>
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('light')}>Light</button>
<h2>{count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
};
export default UseState;
Now when the Increment button is clicked, we add 1 to the current count, and when the Decrement button is clicked, we subtract 1 from the current count. React will re-render the component with the updated count displayed in the h2.
Step 8: Use Callback Functions in State Updates
When we update state based on the previous state, it's best practice to use a callback function. This ensures we always have the most up-to-date value when making changes.
import { useState } from 'react';
import './UseState.css';
const UseState = () => {
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
return (
<div className={`state ${theme}`}>
<h1>UseState Component</h1>
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('light')}>Light</button>
<h2>{count}</h2>
<button onClick={() => setCount(prevCount => prevCount + 1)}>Increment</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>Decrement</button>
</div>
);
};
export default UseState;
Instead of directly using count + 1, we're now using a callback function that receives the previous count as an argument. This is safer when multiple state updates might happen in quick succession, as React can batch updates.
Step 9: Add a Toggle Theme Button (Bonus)
Finally, let's add a bonus button to toggle between light and dark themes with a single click. This will use a callback function with the previous theme to determine the new theme.
import { useState } from 'react';
import './UseState.css';
const UseState = () => {
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
return (
<div className={`state ${theme}`}>
<h1>UseState Component</h1>
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('light')}>Light</button>
<button onClick={() => setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
<h2>{count}</h2>
<button onClick={() => setCount(prevCount => prevCount + 1)}>Increment</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>Decrement</button>
</div>
);
};
export default UseState;
The Toggle Theme button uses a conditional (ternary) expression to set the theme to 'dark' if it's currently 'light', and vice versa.
Complete Solution
Here's the final version of our UseState.jsx file with all features implemented:
import { useState } from 'react';
import './UseState.css';
const UseState = () => {
// Theme state (light/dark)
const [theme, setTheme] = useState('light');
// Counter state
const [count, setCount] = useState(0);
return (
<div className={`state ${theme}`}>
<h1>UseState Component</h1>
{/* Theme buttons */}
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('light')}>Light</button>
<button onClick={() => setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
{/* Counter display and buttons */}
<h2>{count}</h2>
<button onClick={() => setCount(prevCount => prevCount + 1)}>Increment</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>Decrement</button>
</div>
);
};
export default UseState;
This component now successfully:
- Maintains a theme state (light or dark)
- Applies the theme class dynamically to change the appearance
- Allows switching between themes with dedicated buttons
- Maintains a count state
- Displays the current count
- Allows incrementing and decrementing the count
- Includes a bonus button to toggle between themes
Understanding useState: A Deeper Look
What Exactly Is useState?
The useState hook is a function provided by React that lets you add state to functional components. Before hooks were introduced in React 16.8, you could only use state in class components, which made functional components less powerful. With useState, functional components can now maintain their own state across renders.
Anatomy of useState
Let's break down how useState works:
- Initialization: When you call
useState(initialValue), you're telling React to create a new state variable with the provided initial value. - Return Value: useState returns an array with exactly two elements:
- The current state value (initially set to the initialValue you provided)
- A function that lets you update the state
- Destructuring: Typically, we use array destructuring to give names to these two elements, like
const [count, setCount] = useState(0).
A Mental Model for useState
Think of useState like a special box with a label (the state variable name) and a special key (the setter function):
- When your component first renders, React creates a box labeled with your state variable name (e.g., "count")
- Inside the box, React puts the initial value you specified (e.g., 0)
- React also gives you a special key (the setter function) that's the only way to change what's in the box
- Whenever you use the key to change the value, React notices and re-renders your component with the new value
Why Use Callback Functions in State Updates?
Let's look at the difference between these two approaches:
// Approach 1: Direct update
setCount(count + 1);
// Approach 2: Callback function
setCount(prevCount => prevCount + 1);
Approach 1 captures the value of count at the time the function is defined. If count is 0 when the function is defined, it will always try to set the count to 1 (0 + 1), even if the actual state has changed.
Approach 2, on the other hand, uses a callback that receives the most up-to-date value of the state just before the update is applied. This ensures that multiple updates in succession will work correctly.
Imagine you're in a bakery with a number ticket system. If you tell your friend "My number is 42, so watch for 43 to be called," that's like Approach 1. If the bakery skips some numbers, your instruction won't work. But if you say "Watch for the number after mine to be called," that's like Approach 2 - it will work correctly regardless of what your specific number is.
Multiple State Variables vs. One Object
In our example, we used two separate useState calls:
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
We could have combined them into a single state object:
const [state, setState] = useState({ theme: 'light', count: 0 });
// And then update like this:
setState(prevState => ({ ...prevState, theme: 'dark' }));
However, using separate state variables is often cleaner and easier to work with, especially for unrelated pieces of state. It allows each piece of state to be updated independently without having to worry about preserving other values in the state object.
Real-World Applications
The concepts we've learned with useState form the foundation for state management in React applications. Here are some real-world examples of how these concepts are applied:
Theme Switching
Many websites and applications now offer light and dark modes to improve user experience. The theme-switching functionality we implemented is a simplified version of what you'd find in production applications. In a real-world scenario, you might also:
- Store user preference in localStorage to persist across sessions
- Detect system preferences using media queries
- Apply theme changes to more complex UI elements
Counters and Numeric Controls
Our counter implementation is a basic example of numeric state management, which has numerous applications:
- Shopping cart item quantities
- Pagination controls
- Rating systems
- Timer or stopwatch functionality
Form Input Management
The useState hook is commonly used to manage form inputs:
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// Then in the JSX:
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
Conditional Rendering
State variables are often used to control what gets rendered:
const [isLoggedIn, setIsLoggedIn] = useState(false);
// Then in the JSX:
{isLoggedIn ? <UserDashboard /> : <LoginForm />}
Feature Toggles and Modal Controls
Boolean state variables can control the visibility of components:
const [isModalOpen, setIsModalOpen] = useState(false);
// Then in the JSX:
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
{isModalOpen && <Modal onClose={() => setIsModalOpen(false)} />}
Common Pitfalls and Best Practices
Pitfall: Direct State Mutation
Never modify state directly. Always use the setter function provided by useState.
// WRONG
count = count + 1;
// RIGHT
setCount(count + 1);
Pitfall: Stale State in Closures
Be cautious of accessing state in closures, as it might be stale. Use the callback pattern when updating state based on its previous value.
// Potentially problematic if the state changes between the time
// the function is defined and when it's called
setTimeout(() => setCount(count + 1), 1000);
// Safer approach
setTimeout(() => setCount(prevCount => prevCount + 1), 1000);
Best Practice: Keep State Minimal
Only store values in state that actually need to cause re-renders when they change. Derived values can be calculated during rendering.
Best Practice: Split State Logically
Separate unrelated state variables into multiple useState calls rather than combining them into a single object.
Best Practice: Use Functional Updates
When updating state based on its previous value, use the functional form:
setCount(prevCount => prevCount + 1);
Best Practice: Lift State Up
If multiple components need to share state, move it up to their closest common ancestor in the component tree.
Beyond useState: Next Steps in React State Management
As your applications grow in complexity, you might outgrow the basic useState hook. Here are some next steps in your journey with React state management:
useReducer
The useReducer hook provides a more structured way to manage complex state logic, especially when state transitions depend on previous state or when state updates need to follow specific patterns.
Context API
React's Context API, combined with hooks like useContext, allows you to share state across your component tree without prop drilling. This is particularly useful for global state like user authentication or theme preferences.
Custom Hooks
As you gain experience, you'll likely start creating custom hooks to encapsulate and reuse stateful logic across components. For example, you might create a useTheme hook that wraps all the theme-switching functionality we implemented.
State Management Libraries
For larger applications, dedicated state management libraries like Redux, MobX, or Zustand provide more powerful tools for managing complex application state.