Top 5 React mistakes junior developers make (and how to fix them fast)
Search terms like React mistakes beginners, React errors junior developers, and common React bugs all point to the same problem: React feels simple until state, effects, and component boundaries start working against you.
Most junior developers do not need more theory. They need to see the bug, understand why React behaves that way, and make the smallest fix that gets them unstuck. The five mistakes below show up constantly in code reviews and pair-programming sessions because they create subtle bugs, stale UI, and unnecessary re-renders.
Each section includes bad code, a fast fix, and the reason the fix works so you can stop patching symptoms and start recognizing the pattern.
1. Not understanding the dependency array in useEffect
The dependency array tells React when an effect should re-run. Junior developers often leave it empty because they want the warning to disappear, but that usually creates stale data or effects that never react to new props.
Bad code
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch("/api/users/" + userId)
.then((res) => res.json())
.then(setUser);
}, []);
return <div>{user?.name}</div>;
}Fast fix
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch("/api/users/" + userId)
.then((res) => res.json())
.then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}Why this works
With [], the effect runs only once on mount. IfuserId changes later, the component still shows the old user. Adding userId tells React to sync the effect with the value it actually depends on. When the linter complains about missing dependencies, assume it is protecting you from a real bug until proven otherwise.
2. Mutating state directly instead of using setState
React state should be treated as immutable. If you change an array or object in place, React may not detect a meaningful change because the reference stayed the same.
Bad code
function TodoList() {
const [todos, setTodos] = useState(["Ship post"]);
function addTodo() {
todos.push("Review PR");
setTodos(todos);
}
return <button onClick={addTodo}>{todos.length} todos</button>;
}Fast fix
function TodoList() {
const [todos, setTodos] = useState(["Ship post"]);
function addTodo() {
setTodos((currentTodos) => [...currentTodos, "Review PR"]);
}
return <button onClick={addTodo}>{todos.length} todos</button>;
}Why this works
The fixed version creates a new array instead of mutating the old one. That gives React a new reference to compare, so the re-render is reliable. This also avoids side effects where other code still holds a reference to the mutated object and sees unexpected changes.
3. Overusing useEffect when derived state is simpler
A lot of common React bugs come from storing values in state that can be calculated during render. If a value can be derived from props or existing state, adding an effect usually makes the code harder to follow and easier to break.
Bad code
function ProductList({ products, searchTerm }) {
const [filteredProducts, setFilteredProducts] = useState([]);
useEffect(() => {
setFilteredProducts(
products.filter((product) =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
)
);
}, [products, searchTerm]);
return <Results products={filteredProducts} />;
}Fast fix
function ProductList({ products, searchTerm }) {
const filteredProducts = products.filter((product) =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return <Results products={filteredProducts} />;
}Why this works
There is no separate source of truth here. filteredProducts always comes from products and searchTerm, so storing it in state creates duplication. The render-based version removes an extra state update, avoids effect timing issues, and stays in sync automatically. If the calculation becomes expensive, that is the time to consider useMemo, not an effect.
4. Prop drilling too deep without context
Passing the same prop through three or four components that do not use it makes components noisy and tightly coupled. It is one of the most common React errors junior developers make when an app starts growing.
Bad code
function App() {
return <Dashboard theme="dark" />;
}
function Dashboard({ theme }) {
return <Sidebar theme={theme} />;
}
function Sidebar({ theme }) {
return <UserMenu theme={theme} />;
}
function UserMenu({ theme }) {
return <Avatar theme={theme} />;
}Fast fix
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext value="dark">
<Dashboard />
</ThemeContext>
);
}
function Avatar() {
const theme = useContext(ThemeContext);
return <div className={theme}>Profile</div>;
}Why this works
Context lets the component that needs the value read it directly instead of forcing every middle layer to pass it along. Use it for app-wide or branch-wide concerns like theme, authenticated user, or feature flags. If only one child needs the data, restructuring the component tree can still be cleaner than adding context.
5. Not memoizing expensive renders
Memoization is not something to sprinkle everywhere, but ignoring it completely can make a UI feel sluggish. When an expensive calculation runs on every render, or when a child depends on a stable callback prop, useMemo and useCallback can remove unnecessary work.
Bad code
function ReportTable({ rows, filter }) {
const visibleRows = rows
.filter((row) => row.status === filter)
.sort((a, b) => b.score - a.score);
const handleExport = () => exportRows(visibleRows);
return <Toolbar onExport={handleExport} rows={visibleRows} />;
}Fast fix
function ReportTable({ rows, filter }) {
const visibleRows = useMemo(() => {
return rows
.filter((row) => row.status === filter)
.sort((a, b) => b.score - a.score);
}, [rows, filter]);
const handleExport = useCallback(() => {
exportRows(visibleRows);
}, [visibleRows]);
return <Toolbar onExport={handleExport} rows={visibleRows} />;
}Why this works
Now the filtering and sorting logic only reruns whenrows or filter change, and the export callback stays stable between unrelated renders. That matters when the child is memoized or when the calculation is genuinely expensive. The rule is simple: memoize measured bottlenecks, not everything by default.
How to fix these React mistakes faster next time
The pattern behind most React mistakes beginners make is the same: state is stored in the wrong place, effects are doing too much, or components are sharing data inefficiently. When you get stuck, ask three questions: is this value derived, is this effect truly syncing with something external, and does this component really need this prop?
That quick mental checklist catches a surprising number of common React bugs before they turn into a late-night debugging session.