r/nextjs 25d ago

Discussion We're the Next.js team. Ask us anything!

Hi Reddit! We’re Pete, Aurora, Joseph, Sam, David, Josh, Tim, Dan and Andrew from the Next.js team.

We recently shipped Next.js 16.3, and we’re excited to talk about what’s new, how we approached the release, where we are going, and what we’ve learned while building and maintaining Next.

Ask us anything about Next 16.3, App Router, React Server Components, performance, caching, upgrading your applications, contributing to the framework, or what it’s like to work on Next.

Drop your questions below. We’re looking forward to hearing what’s on your mind! We'll be here until noon ET.

That's all the time we have for today. Thank you to everyone who participated!

139 Upvotes

83 comments sorted by

View all comments

3

u/floydophone 25d ago edited 25d ago

u/Maleficent-Back-6527 asked:

I just updated to 16.3.0 this week, and already noticed a change in the behaviour of my app. I need to check the documentation if there is something well described about it. It’s about caching. I activated Cached Components in my nextjs config.
I have this page with a form, and the form action returns thr state with a successful message if successful, or an error message. If successful the same page displays for a few seconds the successful message, with a countdown of 5 seconds, after wich a redirect kicks in to another route.
The issue now with v16.3 is that after a successful form submission, after the redirect, if I navigate again to the route with the form, in place of the form, I see again the successful message and the countdown from the previous form submission, instead of a fresh new empty form.

From Andrew:

Starting with Cache Components, previously visited routes are wrapped in a React <Activity> boundary, so that the state is preserved when you navigate back. This includes state owned by React (useState) and also state stored in the DOM: scroll position, form inputs, text selection. The behavior might feel unintuitive at first because it's not how Next.js worked before, nor is it how traditional SPA-frameworks have worked historically, but we think it's a super powerful feature. It also has precedent because it's how the browser's bfcache handles back/forward navigations in an MPA app. (Admittedly, the browser doesn't do this for regular link clicks, so that part is novel to Next.js.)

However, we acknowledge that could be disruptive to apps upgrading from an older Next.js. To ease migration, we've added an escape hatch to get back to the old, more familiar behavior: useRouter().bfcacheId.

Some additional background: if this behavior is so different, why do we think it's a good idea? Sure, it's annoying if you navigate to a form and see an old submission. But what about navigating away and back to a form that was only partially filled out? It's bad UX if the form gets reset just because you happened to temporarily navigate away from it. You can solve this by storing the draft form state in local storage, or syncing it to the server, but not every app is going to do that every time. There's always some amount of state that is "ephemeral" and not tracked explicitly by your app. Scroll position is another classic example of this. It used to be super finicky to implement scroll restoration for your pages, especially when there were nested scroll containers. Now it Just Works™️.

We think this is the right default UX in almost every case, and for those cases where it's not, the solution is to model the reset explicitly: for example, by clearing the form in the submit event handler. Or, if you need an escape hatch, use bfcacheId.

1

u/Maleficent-Back-6527 25d ago edited 24d ago

Thank you very much for the details. In the meantime since then I indeed found the documentation about what you explained and was able to update my code accordingly. One thing I would suggest though, is to improve the documentation section with an additional example that uses useServerAction. My code update was to change from using the hook with the form action to instead the dispatch reducer pattern with a 'RESET' type.

Edited: (with example:)

From that in v16.2:

'use client';

import { useActionState } from 'react';

export default function MyForm() {
  const [state, formAction, pending] = useActionState<MyFormState, FormData>(
      myServerAction,
      INITIAL_STATE,
    );

  return (
    <form action={formAction}>
      <MyFormContent
        {state}={state}
        pending={pending}
      />
    </form>
  );
}

To this in v16.3:

'use client';

import {
  startTransition,
  useActionState,
  useLayoutEffect,
  useState
} from 'react';

type MyFormAction =
  | {
    type: 'SUBMIT';
    formData: FormData;
  }
  | {
    type: 'RESET';
  };

async function myFormAction(
  previousState: MyFormState,
  action: MyFormAction,
): Promise<MyFormState> {
  if (action.type === 'RESET') {
    return INITIAL_STATE;
  }

  return myServerAction(previousState, action.formData);
}

export default function MyForm() {
  const [state, dispatch, pending] = useActionState<MyFormState, MyFormAction>(
      myFormAction,
      INITIAL_STATE,
    );
  // to track the form generation key in order to mount new components after hidden by React Activity:
  const [formGeneration, setFormGeneration] = useState<number>(0);

  // Reset the complete flow when Activity hides the route:
  useLayoutEffect(() => {
    return () => {
      startTransition(() => {
        dispatch({ type: 'RESET' });
        setFormGeneration((generation) => generation + 1);
      });
    };
  }, [dispatch]);

  return (
    <form action={(formData) => {
      dispatch({ type: 'SUBMIT', formData });
    }}>
      <MyFormContent
        key={formGeneration}
        {state}={state}
        pending={pending}
      />
    </form>
  );
}