When I started building with Next.js, I never imagined how quickly seemingly simple hooks like useState
could introduce subtle bugs. Because of the hybrid nature of Next.js—where pages can be rendered both on the server and client—using useState
incorrectly can lead to hydration mismatches, non-deterministic behavior, and performance bottlenecks. Over time, I encountered a few common traps with useState
that might seem trivial but can derail your development experience.
Let’s walk through some of the most frequent useState
issues I’ve dealt with while working on production-grade Next.js apps.
Hydration Mismatch Warnings
One of the first red flags I hit was the infamous React hydration warning. This happens when the server-rendered HTML doesn’t match what React tries to render on the client. In many of my early pages, I initialized state based on window
or other client-only values directly inside useState
, like this:
const [width, setWidth] = useState(window.innerWidth);
It worked fine in development, but on a production build, Next.js would throw hydration mismatch warnings. The server doesn’t have access to window
, so the initial render had different HTML. The fix was to either use useEffect
to set such client-only state or defer the render until after hydration. It’s a subtle mistake but one that’s easy to repeat, especially when building responsive components.
Relying on State for SSR Props
Another trap I fell into was fetching server-side props in getServerSideProps
or getStaticProps
, then initializing component state using useState(props.data)
and trying to keep them in sync. It seems like a logical thing to do, but once the component mounts, if the props change, the state doesn’t update because useState
only sets the initial value.
This led to bugs where my UI didn’t reflect the updated props unless I added useEffect
and some sync logic manually. I realized if I don’t plan to mutate the data locally, it’s better to just use the props directly instead of wrapping them inside state unnecessarily.
State Used Before It’s Set
I remember building a form page with conditional fields. Based on user interaction, I set a flag using useState
, then expected the new value to be available in the same function. But React state updates are asynchronous—so changes won’t reflect immediately.
Here’s what caused the confusion:
setIsAdvanced(true);
if (isAdvanced) {
// this doesn't run as expected
}
I learned that if I needed immediate reactions to state changes, I had to either refactor the logic using useEffect
or rely on the event that triggered the change rather than the new state value.
Too Many States When One Will Do
There were times when I overused useState
—splitting everything into its own state variable:
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
It quickly became hard to manage and test. Updating multiple fields at once or resetting them became verbose and repetitive. Eventually, I switched to a single object state using one useState
, which drastically improved readability and control.
const [formData, setFormData] = useState({ firstName: '', lastName: '', email: '' });
There’s also a detailed breakdown of how to handle form state in React with useActionState
if you’re using the latest server components approach.
Setting State in a Loop or Without Proper Cleanup
In one dashboard project, I used a loop with setTimeout
to create polling behavior, and each iteration updated state. Initially, it looked fine in development. But when I tested in production, multiple setTimeout
calls overlapped, leading to flickering UI and eventually memory leaks.
The issue? I didn’t wrap my polling logic inside useEffect
with the right cleanup. If you don’t cancel timeouts or intervals on unmount, especially in dynamic routes or layouts, you’ll create hard-to-track bugs. In Next.js, where navigation can shift between SSR and CSR seamlessly, these bugs get even harder to debug.
Ignoring Server-Client Rendering Differences
In one of my Next.js apps, I used useState
to control visibility for a theme toggle based on system preferences using window.matchMedia
. Again, it worked on the client but broke during SSR. The lesson here was to never rely on useState
alone for data that’s only available after the client has mounted.
In fact, I found a better approach using conditional rendering like:
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
return isMounted ? <ThemeToggle /> : null;
This also applies if you’re integrating third-party libraries or loading user settings conditionally. For cases like this, avoiding premature usage of useState
during the initial render saved me countless hours of debugging hydration issues.
Incorrect Default Values from Local Storage or Cookies
Another common source of bugs was setting default state from localStorage
:
const [token, setToken] = useState(localStorage.getItem('token'));
I kept getting errors on the server because localStorage
doesn’t exist there. In Next.js, you always need to ensure client-only APIs are accessed after the component mounts. I moved such logic into useEffect
, and initialized state with a fallback:
const [token, setToken] = useState(null);
useEffect(() => {
const stored = localStorage.getItem('token');
setToken(stored);
}, []);
In fact, a broader reflection on dynamic vs static rendering in Next.js helped me understand which side of the stack owns the responsibility for what.
Forgetting State Reset on Route Change
I once created a multi-step form in a Next.js project and didn’t handle route changes correctly. The useState
values from one step persisted across pages. I wrongly assumed that navigating to a new page would remount the component, resetting state. That’s not always true in Next.js, especially when using layout components or nested routes.
To fix this, I added cleanup logic or reset state explicitly using useEffect
when the route changed. It made me realize how important it is to treat state as scoped to the lifecycle, not just to the component.
Next.js amplifies the consequences of improper state usage more than traditional React apps. The server-client boundary isn’t just a runtime concern—it defines how your UI initializes, hydrates, and responds. If you don’t think carefully about how and when state is used, you’ll run into subtle but frustrating bugs.
Every time I hit a wall, it led me to better practices, whether that meant deferring logic to useEffect
, avoiding unnecessary state, or choosing server-aware alternatives like useActionState
. Understanding these nuances has made me not only a better Next.js developer but a better React developer overall.