How to Fix: [Feature] be able to hide the next version

5 min read

The production bug happens because the app treats the next version window as always visible once version metadata is available, but there is no durable hide state persisted across a production build and reload. In development, this can look fine due to hot reload and local state behavior. After pnpm run build and a production start, the banner reappears because the visibility logic is recalculated without a stored dismissal rule.

Understanding the Root Cause

This issue usually appears in apps that display a release notice, upgrade modal, or version banner based on a comparison between the current app version and a target next version. The feature request to “be able to hide the next version” means the UI needs more than render logic; it also needs a reliable dismissal mechanism.

Technically, the problem comes from one or more of these patterns:

  • The component stores visibility only in React local state, so the dismissed state is lost on refresh.
  • The app computes visibility during render but does not check a persisted value such as localStorage, cookies, or server-backed user preferences.
  • The production build executes under stricter SSR/CSR boundaries, and browser-only APIs are not accessed safely.
  • The hide logic is not tied to a specific version, so dismissing one version may incorrectly hide all future versions or fail to hide the intended one.

The correct behavior is version-aware dismissal: when a user hides version X.Y.Z, the app should persist that decision and suppress the window only for that exact version. When a newer version appears, the window should show again.

Step-by-Step Solution

The safest implementation is to persist a hidden key scoped to the announced version. A common pattern is dismissed-next-version:<version>.

1. Define a version-specific storage key

export function getNextVersionDismissKey(version: string) {
  return `dismissed-next-version:${version}`;
}

2. Read the dismissal state only on the client

import { useEffect, useMemo, useState } from 'react';

function NextVersionWindow({ nextVersion }: { nextVersion: string | null }) {
  const [isDismissed, setIsDismissed] = useState(false);
  const [isHydrated, setIsHydrated] = useState(false);

  const storageKey = useMemo(() => {
    if (!nextVersion) return null;
    return `dismissed-next-version:${nextVersion}`;
  }, [nextVersion]);

  useEffect(() => {
    setIsHydrated(true);

    if (!storageKey) return;

    const dismissed = window.localStorage.getItem(storageKey) === 'true';
    setIsDismissed(dismissed);
  }, [storageKey]);

  if (!isHydrated || !nextVersion || isDismissed) {
    return null;
  }

  return (
    <div>
      <p>A new version is available: {nextVersion}</p>
      <button
        onClick={() => {
          if (storageKey) {
            window.localStorage.setItem(storageKey, 'true');
          }
          setIsDismissed(true);
        }}
      >
        Hide
      </button>
    </div>
  );
}

export default NextVersionWindow;

3. If using Next.js App Router, mark the component as client-side

'use client';

This is required if the component touches localStorage or uses browser event handlers.

4. If the banner is driven by config, compare versions explicitly

type Props = {
  currentVersion: string;
  nextVersion: string | null;
};

function shouldShowNextVersion(currentVersion: string, nextVersion: string | null) {
  if (!nextVersion) return false;
  return currentVersion !== nextVersion;
}

This avoids showing the window when the deployed app already matches the announced version.

5. Support resetting dismissal for testing

export function clearNextVersionDismissal(version: string) {
  window.localStorage.removeItem(`dismissed-next-version:${version}`);
}

This is especially useful when reproducing production behavior locally.

6. Optional: use cookies when dismissal must survive across subdomains or SSR-aware flows

export function hideNextVersionWithCookie(version: string) {
  document.cookie = `dismissed-next-version:${version}=true; path=/; max-age=31536000`;
}

If the application renders differently between server and client, cookies can be a better fit than localStorage because they can participate in request-time logic.

7. Validate in a production build

pnpm install
pnpm run build
pnpm run start

Then test this flow:

  1. Open the app and confirm the next version window is visible.
  2. Click Hide.
  3. Refresh the page.
  4. Confirm the window stays hidden for that specific version.
  5. Change the announced version and confirm the window appears again.

Common Edge Cases

  • Hydration mismatch: If the server renders the banner but the client immediately hides it after reading localStorage, you may see flicker. Guard rendering until hydration completes.
  • Server Components: Accessing window or localStorage in a server component will fail. Move the logic into a client component.
  • Global dismissal bug: If the storage key is just dismissed-next-version without the version suffix, one hide action may suppress all future announcements.
  • Invalid version source: If nextVersion is null, undefined, or stale at build time, the component may behave inconsistently between environments.
  • Private browsing or restricted storage: Some environments can block local persistence. Wrap storage access in try/catch if your app targets stricter browsers or embedded webviews.
  • Multi-tab behavior: If a user hides the banner in one tab, another open tab may still show it until reload unless you also listen for the storage event.
useEffect(() => {
  function onStorage(event: StorageEvent) {
    if (event.key === storageKey && event.newValue === 'true') {
      setIsDismissed(true);
    }
  }

  window.addEventListener('storage', onStorage);
  return () => window.removeEventListener('storage', onStorage);
}, [storageKey]);

FAQ

Why does the window reappear only after building for production?

Because production exposes the real render lifecycle more clearly. If visibility depends on non-persisted state, the app resets after refresh or a fresh server render. Development mode can mask this with hot reload behavior.

Should I use localStorage or cookies to hide the next version?

Use localStorage for a simple client-only dismissal. Use cookies if the hidden state must influence server-rendered output or be available earlier in the request lifecycle.

How do I ensure hiding one version does not hide future releases?

Scope the dismissal key to the announced version, such as dismissed-next-version:1.4.0. That way, each release has its own independent hidden state.

The core fix is simple: make the hide action persistent, make it version-specific, and make sure browser storage is only read inside the client runtime. Once those three rules are in place, the next version window can be hidden reliably in both local and production environments.

Leave a Reply

Your email address will not be published. Required fields are marked *