form-atoms / form-atoms

Atomic form primitives for Jotai

Home Page:https://codesandbox.io/s/getting-started-with-form-atoms-v2-ddhgq2?file=/src/App.tsx

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Hydration with `useFieldInitialValue` in Next.js renders twice - first empty, then filled input.

MiroslavPetrik opened this issue Β· comments

I believe that currently the useFieldInitialValue is not optimized for SSR, as it fails to render a filled input on the first paint.
I suspect that the useEffect might be the cause.
So far I've switched to useHydrateAtoms from jotai/utils which fixed the issue. form-atoms/field@9264c90#diff-57ff5d92fdd79e6888455c22013fd0924199b51e87f1be3075f7c8b9cc848c47R5

The hydrateAtoms will set the atoms only once, while the useFieldInitialValue was recently updated to permit multiple updates. #52

The multiple updates I believe are also vital for Next.js. My use case is that after a form is saved, I call router.refresh(), where the router is the useRouter() from nextjs/navigation. This successfully re-initialized my form with the changes from #52, but it fails to work (expected) with the useHydrateAtoms.

Potential solutions

  • use the useHydrateAtoms in the useFieldInitialValue
  • update useFieldInitialValue to set the first value outside of the useEffect?

Beware that the useHydrateAtoms might not be straighforward - with my linked implementation for useHydrateField I'm experiencing HTML content mismatch, as multiple listAtom items result to multiple DOM nodes. So after the router.refresh() when item in a list was added/removed, the FE jotai store is stalled (since useHydrateAtoms is permited only once) and the BE renders different amount of items, so a mismatched HTML. Here I can clear the jotai store, but likely that won't help as the internal hydrateMap preventing the duplicate hydration is not resetable from outside... https://github.com/pmndrs/jotai/blob/2d760920f5ebea05abaa8e072cee578fa125ca9b/src/react/utils/useHydrateAtoms.ts#L22C7-L22C18
So maybe even that could be made resetable in the jotai lib, but that would require manual user cache invalidation ,e.g. just after the router.refresh() call in my case.

I believe I originally used useHydrateAtoms and moved away from it because it sucked to work with. Tbh I don't really care about Next.js and how well the form populates on the server because this is inherently a client side library

Like if you want server forms then use native forms.

Yeah this strikes me as a "you" problem as a user. "You" chose to make your life difficult so "you" have to deal with the consequences. Not the library author πŸ˜ƒ That is, the answer to Next.js's over-engineered mess isn't more over-engineering.

Also can't this be solved in "you" land by creating your form atom inside your component (eg in react usesState) rather than outside?

You are right, its my problem, I also see this more like 'opt-in' so can be done in user land. But maybe we can make a change so even the client-side only version improves.

Let's take next.js out of the equation. Simply I'm revisiting the hydration and how to do it with jotai. I've discovered that several users have trouble with that.

I believe I originally used useHydrateAtoms .... sucked to work with ...

What exactly sucked? It's a simple hook to me. I've created field specific version.
In tandem with useFieldInitialValue it works great. It succeeds to do the first paint on the server side, while the useFIV with useEffect fails to do so. So this is excellent, as the useHydrateFields can be used opt-in only when needed.

So likely there is no more work to do, & I can only document this as SSR docs page on my side. in the fields repo.
I'm not sure if using useHydrateAtoms has some benefit from client-only perspective? Can the first render be somehow better without useEffect?

If the worst thing about the current hook is that inputs are empty on the first paint, I'm very tempted to close this issue rather than add complexity.

Yeah, I think I have it solved for now, as the 2 hooks together work for me.
No need to add the hook here yet, maybe when more users ask for it.

Still I think this is the place where to put it, as if you look at it as a layered architecture, the feature calls the bottom layer (jotai), so there is likelihood that multiple people will want it. E.g. just taking myself as an example, I've arrived to the code you had previously.

Maybe we can mention the hook in the readme here?

Maybe it does work without useEffect or useHydrateAtoms at all?

export function useFieldInitialValue<Value>(
  fieldAtom: FieldAtom<Value>,
  initialValue?: Value | typeof RESET,
  options?: UseAtomOptions
): UseFieldInitialValue {
  const field = useAtomValue(fieldAtom, options);
  const store = useStore(options);

  if (initialValue === undefined) {
    return;
  }

  if (!store.get(field.dirty) && initialValue !== store.get(field.value)) {
    store.set(field.value, initialValue);
  }

  if (initialValue !== store.get(field._initialValue)) {
    store.set(field._initialValue, initialValue);
  }
}

I'd really appreciate a solution without useEffect since in my case, it causes an empty state to flash and then it will be replaced by the initial value when the useEffect runs.
I tried PR #66 and also got the "Warning: Cannot update a component".

using both useFieldInitialValue and useHydrateAtoms from @MiroslavPetrik works well in my purely client-side app.

Too bad? It's a client library. You're getting the flash, dawg lol. If you want a form that works with server rendering, use the form element and uncontrolled inputs. I'm not over-engineering for a use case that doesn't fit project goals. It's your problem, not the library's.

I am not using server rendering, it's a pure client rendered SPA.

Then you're doing something wrong πŸ€·πŸΌβ€β™‚οΈ read the docs. I have never seen a flash.

Like if there's a flash here, it's imperceptible.

Screen.Recording.2024-01-27.at.2.50.30.PM.mov

Then I must be doing something wrong

screenrec.mov

πŸŽ‰ This issue has been resolved in version 3.2.3 πŸŽ‰

The release is available on:

Your semantic-release bot πŸ“¦πŸš€

hard to say without seeing the form code, but maybe the layout effect in #66 will help

Just tried v3.2.3 but unfortunately it does not change it.

I think my problem is this:

The field atom is nullable:

const selectedProductAtom = fieldAtom<SelectedProduct | null>({value: null})

And the custom form component renders like this:

export interface SelectProductButtonProps {
  initialValue?: SelectedProduct | null;
}
export function SelectProductButton({
  initialValue,
}: SelectProductButtonProps) {

  const selectedProductField = useField(selectedProductAtom);
  useFieldInitialValue(selectedProductAtom, initialValue);
  if (selectedProductField.state.value === null) {
      return (
        <Button onClick={showPicker} id={id} fullWidth={true}>
          select
        </Button>
      );
     else {
      return (/* different display style in case value is set */)
    }
}

In the first render, selectedProductField.state.value is null, and thus the first branch is taken.
Then the initial value is set and the second branch is taken.

Yeah that isn't something the layout effect would fix as its beyond paint, in that the tree itself changes. One option (which may be annoying but is never the less a sound React and Jotai pattern) is to create your form atom within your form component e.g.

const [form] = React.useState(() => {
  return formAtom({
    selectedProduct:  fieldAtom({value: initialSelectedProductValue}),
  })
})

Apologies for being prickly earlier 😏, but I don't understand how useHydrateAtom is going to affect existing users/use cases so I'm reluctant to release what is potentially a breaking change including it. In the meantime I imagine it'd look something like this if you wanted to try it instead:

export function useFieldInitialValue<Value>(
  fieldAtom: FieldAtom<Value>,
  initialValue?: Value | typeof RESET,
  options?: UseFieldInitialValueOptions<Value>
): UseFieldInitialValue {
  const field = useAtomValue(fieldAtom, options);
  const store = useStore(options);
  useHydrateAtoms(
    initialValue
      ? [
          [field._initialValue, initialValue],
          [field.value, initialValue],
        ]
      : [],
    options
  );

  React.useLayoutEffect(() => {
    const areEqual = (options && options.areEqual) || defaultValuesAreEqual;

    if (initialValue === undefined) {
      return;
    }

    if (
      !store.get(field.dirty) &&
      !areEqual(initialValue, store.get(field.value))
    ) {
      store.set(field.value, initialValue);
    }

    if (!areEqual(initialValue, store.get(field._initialValue))) {
      store.set(field._initialValue, initialValue);
    }
  });
}

This is basically what I use, the combination of both hooks and which fixes the flash:

export function useInitialValue<Value>(
  fieldAtom: FieldAtom<Value>,
  initialValue?: Value | typeof RESET,
  options?: UseAtomOptions,
) {
  useHydrateField(fieldAtom, initialValue, options);
  useFieldInitialValue(fieldAtom, initialValue, options);
}