Hey React developers,
After working with dozens of developers and reviewing lots of React code, I’ve noticed a pattern. Many developers write code that works fine during development but leads to subtle bugs and frustrating user experiences in production.
These issues often show up when real users interact with the app, leading to bounce rates, lost user trust, or even lost revenue.
In this post, I’ll break down 10 common mistakes I see (and have made myself), plus practical ways to fix them. The goal: move from "it works on my machine" to "this feels great for everyone."
1. Not Using URL Query Parameters
The Mistake:
const [search, setSearch] = useState('');
State is stored in memory only.
Why It’s a Problem:
- Refreshing the page resets filters.
- You can’t share a filtered view.
- The URL doesn’t reflect the UI state.
How to Fix It:
Use useSearchParams
for simple cases:
const [searchParams, setSearchParams] = useSearchParams();
const search = searchParams.get('q') || '';
Use nuqs
+ zod
for validated, typed state:
import { useQueryState } from 'nuqs';
import { z } from 'zod';
const { q, setQuery } = useQueryState({ q: z.string().optional() });
const handleSearch = (value) => {
setQuery({ q: value });
};
Browsers treat URLs as the source of truth, so should your UI.
2. Avoiding the <form>
Element
The Mistake:
Using divs and onClick
handlers instead of native <form>
behavior.
Why It’s a Problem:
- Enter key doesn’t work.
- Screen readers miss semantic context.
- Browser validations are skipped.
How to Fix It:
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}>
<label htmlFor="name">Name:</label>
<input id="name" type="text" required />
<button type="submit">Submit</button>
</form>
Libraries like react-hook-form
still rely on <form>
.
3. Too Many Independent useState
Calls
The Mistake:
const [name, setName] = useState('');
const [email, setEmail] = useState('');
Why It’s a Problem:
It becomes hard to manage and update as the form grows.
How to Fix It:
Use an object:
const [form, setForm] = useState({ name: '', email: '' });
const updateField = (field, value) => {
setForm(prev => ({ ...prev, [field]: value }));
};
Or use useReducer
for complex cases:
const initialState = { name: '', email: '' };
function reducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
default:
return state;
}
}
const [form, dispatch] = useReducer(reducer, initialState);
4. Not Using Derived State
The Mistake:
const [birthDate, setBirthDate] = useState('');
const [age, setAge] = useState(calculateAge(birthDate));
Why It’s a Problem:
Duplicated state leads to stale or incorrect values.
How to Fix It:
const age = calculateAge(birthDate);
Use useMemo
only for expensive calculations:
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
5. Misusing useMemo
The Mistake:
const memoized = useMemo(() => doSomething(input), [valueThatChangesOnEveryRender]);
Why It’s a Problem:
It runs on every render; this is pointless.
How to Fix It:
Only use useMemo
when:
- The computation is expensive.
- Dependencies are stable.
const expensiveResult = useMemo(() => computeExpensiveValue(data), [data]);
6. No Loading, Error, or Empty States
The Mistake:
const { data } = useQuery(...);
return <List items={data} />;
Why It’s a Problem:
- No feedback while loading.
- Users see blank screens on failure.
- SEO crawlers might index empty pages.
How to Fix It:
const { data, isLoading, isError, error } = useQuery(...);
if (isLoading) return <SkeletonList />;
if (isError) return <p>Error: {error.message}</p>;
if (!data?.length) return <p>No items found.</p>;
return <List items={data} />;
Use @tanstack/react-query
to simplify handling these states.
7. Ignoring Accessibility
The Mistake:
Using <div>
instead of <button>
, skipping label
, or forgetting alt
text.
Why It’s a Problem:
- Breaks keyboard navigation.
- Screen readers can’t interpret the UI.
How to Fix It:
<label htmlFor="username">Username:</label>
<input id="username" type="text" />
<button aria-expanded={isOpen} onClick={toggle}>Toggle</button>
Add alt
text to images. Test with your keyboard. Use eslint-plugin-jsx-a11y
or axe.
8. Input Without Debounce
The Mistake:
<input value={query} onChange={e => setQuery(e.target.value)} />
Why It’s a Problem:
Triggers expensive updates on every keystroke.
How to Fix It:
Use a debounce hook:
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
Or use useDeferredValue
(React 18+) for smoother UI transitions.
9. Multi-Step Forms That Reset on Navigation
The Mistake:
Each step owns its own state.
Why It’s a Problem:
Going back resets user input.
How to Fix It:
Store state at the parent level:
const [formState, setFormState] = useState({ name: '', email: '' });
Pass props into each step. For large apps, use react-hook-form
or zustand
to manage form state.
10. No Skeletons or Placeholders
The Mistake:
return isLoading ? null : <ActualList items={data} />;
Why It’s a Problem:
Blank screen. Confusing for users. Bad perceived performance.
How to Fix It:
function SkeletonList() {
return (
<div>
{[...Array(3)].map((_, i) => (
<div key={i} style={{ height: '50px', background: '#e0e0e0', marginBottom: '8px' }} />
))}
</div>
);
}
Skeletons provide users with feedback and reduce layout shifts.
Tools That Solve Most of These
Start with React’s built-in hooks for small projects. For larger apps, reach for:
- @tanstack/react-query — handles fetching, caching, and background syncing.
- nuqs + zod — URL query param parsing and validation.
- zod / yup — validation schemas for user input.
- react-hook-form — performant, scalable form handling.
- zustand / valtio / jotai — global state without boilerplate.
- eslint-plugin-react-hooks / jsx-a11y — catch common bugs early.
Final Thoughts
Poor UX often begins with minor oversights in code. These mistakes might not show up in local dev, but they frustrate real users. Write code that survives refreshes, plays nice with URLs, and shows empathy for edge cases.
None of this is about being perfect. It’s about being thoughtful.
I’ve also made these mistakes. That’s how I learned.
What about you? Which of these have you made? Share your story or question, I’d love to hear from you.
Top comments (7)
Man, I’ve done half of these myself - love seeing this kinda breakdown, makes it way easier to spot my own mess ups.
Yes, me too!
Great article.
Points 6 could be improved with the new useActionState hook, as it can manage loading state and form submission.
Yup, for server actions.
Helpful and spot-on! Every React beginner should save this, understanding these mistakes early can save hours of debugging and frustration.
Thanks!
been cool seeing steady progress - it adds up. you think habits or just the grind keeps you pushing forward with stuff like this?
Some comments may only be visible to logged-in visitors. Sign in to view all comments.