Frontend Components and React Basics
Build component based UIs with React, manage state, effects, and routing, and connect to Flask APIs.
Content
Hooks useState and useEffect
Versions:
Watch & Learn
AI-discovered learning video
Sign in to watch the learning video for this topic.
useState and useEffect — React Hooks That Actually Make Your Life Easier
"Hooks are like tiny assistants for your components: one holds your stuff (state), the other runs errands for you (side effects)."
You're already familiar with components & props and the old-school idea of state and setState. You also know how to fetch data asynchronously from APIs (hello, fetch/JSON). Now we level up: useState and useEffect let function components keep state and handle side effects cleanly — without classes, without lifecycle method spaghetti, and definitely without the console yelling at you at 2 AM.
Quick map before we dive
- useState: store mutable values that trigger re-renders — the new setState for function components. Think: your component's pockets.
- useEffect: perform side effects (data fetching, subscriptions, timers, DOM work). Think: the component's personal assistant who goes out and fetches coffee/data and cleans up after itself.
This builds directly on what you learned about state/setState and about asynchronous fetches. We'll show idiomatic patterns for combining them (you fetch inside an effect and store results in state).
useState — the pocket where your component keeps stuff
Micro explanation
useState(initialValue) returns a pair: [value, setValue]. On calling setValue, React re-renders the component with the new value.
Example: a counter
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // initial value 0
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
Pro tip: if new state depends on previous state, pass a function to setValue to avoid stale closures:
setCount(prev => prev + 1);
Why? Because React batches updates — using the functional form guarantees correct increments even with asynchronous updates.
useEffect — side effects, lifecycle, and a bit of magic
Micro explanation
useEffect(fn, deps) runs the effect fn after the render. The effect can optionally return a cleanup function.
- If
depsis[]: run once after the first render (like componentDidMount). - If
depshas values: run after the first render and whenever any dependency changes (like componentDidUpdate for specified props/state). - No deps provided: runs after every render (rarely ideal).
Example: fetch data when component mounts (ties to our earlier async JS topic)
import React, { useState, useEffect } from 'react';
function UsersList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function load() {
try {
const res = await fetch('/api/users'); // you learned fetch + JSON earlier
const data = await res.json();
if (!cancelled) setUsers(data);
} catch (err) {
if (!cancelled) console.error(err);
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => { cancelled = true; }; // cleanup: avoid setting state on unmounted component
}, []); // empty deps -> run once
if (loading) return <p>Loading...</p>;
return (
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
);
}
Better pattern (modern): use AbortController to cancel fetches.
useEffect(() => {
const controller = new AbortController();
fetch('/api/users', { signal: controller.signal })
.then(r => r.json())
.then(setUsers)
.catch(err => { if (err.name !== 'AbortError') console.error(err); });
return () => controller.abort();
}, []);
Common questions (and mistakes) — quick Q&A
Why do people keep misunderstanding the dependency array?
Because React captures variables in closures. If you omit a dependency, your effect might use stale values. If you include too many dependencies, you might trigger infinite loops. Use ESLint plugin (react-hooks/exhaustive-deps) to guide you.
Can I make an effect async directly?
No. The effect callback should not return a Promise. Instead, define an async function inside and call it, or use then/catch.
How do I avoid memory leaks when fetching?
Return a cleanup from useEffect (set a flag, or call AbortController.abort()). That prevents calling setState on unmounted components.
What about intervals, subscriptions, event listeners?
Register them in the effect and clean them up in the returned cleanup function:
useEffect(() => {
const id = setInterval(() => setTime(t => t + 1), 1000);
return () => clearInterval(id);
}, []);
Quick lifecycle cheat sheet (class vs hooks)
| Class lifecycle | Hook equivalent |
|---|---|
| componentDidMount | useEffect(..., []) |
| componentDidUpdate | useEffect(..., [deps]) |
| componentWillUnmount | return cleanup from useEffect |
A few patterns worth memorizing
- Data fetch on mount: useEffect(..., []) + setState for data
- Reacting to prop change: useEffect(() => { ... }, [prop])
- Cleanup subscriptions: return () => unsubscribe()
- Derived state avoidance: prefer computing values in render from existing state/props rather than duplicating state
Why this matters — final emotional pitch
If components are actors on a stage, useState is the prop they keep in their pocket, and useEffect is the stage manager who runs the background tasks and cleans up the props at the end of the scene. Together they let function components be expressive, composable, and easier to reason about than class-based lifecycle spaghetti.
"This is the moment where the concept finally clicks: hooks let you separate what your component renders (state/props) from what it does (effects/side-effects) — and that separation is why modern React apps don't feel like fragile dominos."
Key takeaways
- useState stores local state in function components. Use functional updates when state depends on previous values.
- useEffect runs side effects after render. Always think about the dependency array and cleanups.
- When fetching data (you already know fetch/JSON), do it in useEffect, guard against unmounts, and avoid updating state after unmount.
- Follow the Rules of Hooks: call hooks at the top level and only from React function components or custom hooks.
Go build something! Try converting a class-based component you wrote earlier to use hooks — the payoff is immediate: cleaner code, fewer bugs, and a newfound respect for sanity.
If you want, I can now:
- show a step-by-step conversion of a class component with state & lifecycle to hooks, or
- create a small demo combining useState + useEffect + fetch (with AbortController) that you can paste into CodeSandbox.
Which one sounds more fun? 😈
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!