Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tracking] SSR & RSC Usage #480

Closed
juliendelort opened this issue Oct 18, 2023 · 8 comments
Closed

[Tracking] SSR & RSC Usage #480

juliendelort opened this issue Oct 18, 2023 · 8 comments
Assignees

Comments

@juliendelort
Copy link
Contributor

An increasing number of people use SSR & frameworks like NextJS (page router or app router) and Remix, so we should have a guide section covering that.

Some topics:

  • How do I use this with NextJs (we probably need separate sections for the pages router and the app router). Can I do SSR? In the case of RSC’s, do I need a client component? Can I use server actions?
  • How do I use this with Remix? Remix has [a dedicated Form element]((https://remix.run/docs/en/main/components/form), are we compatible with that?
@crutchcorn
Copy link
Member

Love this. Right now we have a blocking bug via #470, but this is a good addition

@crutchcorn crutchcorn self-assigned this Oct 30, 2023
@crutchcorn crutchcorn removed the good first issue Good for newcomers label Oct 30, 2023
@crutchcorn
Copy link
Member

Removing the good first issue flag and assigning this work to myself. I spoke with @tannerlinsley about a potential API we're investigating that looks something like this:

import { useFormState } from "react-dom";
import {
    createFormFactory,
    // Proposed API
    useTransform,
    // Proposed API
    mergeForm
} from "@tanstack/react-form";

const {
  useForm,
  // Proposed API
  validateFormData,
  // Proposed API
  // This allows us to ensure that the types of `useFormState`'s `state`
  // is the same as returned from onServerValidate without having to force the
  // user to manually replicate
  initialFormState,
} = createFormFactory({
  defaultValues: {
    name: "",
    age: 0,
  },
  // Proposed API
  onServerValidate: async (values) => {
    if (values.name.includes("server_error")) {
      return {
        name: "This is a server error",
      };
    }
  },
});

async function submitForm(formData) {
  "use server";
  const results = await validateFormData({ formData });
  if (results) return results;
}

export default function CreatePerson() {
  const [state, dispatch] = useFormState(submitForm, initialFormState);
  const form = useForm({
    /**
     * Proposed API
     *
     * Transforms under-the-hood to a non-framework agnostic:
     * { fn: formBase => mergeForm(formBase, state),
     * deps: [state] }
     *
     * Which would allow us to watch the deps changes in `form-core` and run the relevant functions ourselves
     *
     * But is a custom hook because it allows us to have auto-fixed deps without
     * writing our own ESLint plugin:
     *
     * @see https://www.npmjs.com/package/eslint-plugin-react-hooks#advanced-configuration
     */
    transform: useTransform((formBase) => mergeForm(formBase, state), [state]),
  });

  return (
    <form.Provider>
      <form action={dispatch}>
        <form.Field
          name="name"
          onChange={(val) =>
            val.includes("client_error") ? "This is a client error" : ""
          }
        >
          {(field) => (
            <>
              <label htmlFor={field.name}>First Name:</label>
              <input
                name={field.name}
                value={field.state.value}
                onBlur={field.handleBlur}
                onChange={(e) => field.handleChange(e.target.value)}
              />
              {field.state.meta.errors ? (
                <>
                  {field.state.meta.errors.map((error) => (
                    // Merge client errors and form errors
                    <div key={error}>{error}</div>
                  ))}
                </>
              ) : null}
            </>
          )}
        </form.Field>
        <button type="submit">Submit</button>
      </form>
    </form.Provider>
  );
}

Using this API, we believe we're able to support:

  • NextJS' server actions & useFormState
  • Remix server validation (I might need help with this, reach out to me or comment here please)
  • Nuxt/non-React SSR/SSG usage

And support progressively enhanced (disable JS, still see form errors via page refresh), isomorphic (client and server validation support using the same API) form valdiation.

I'd love to hear anyone's thoughts on this API or our approach.

@crutchcorn crutchcorn changed the title [Docs] Guide about SSR, RSCs, NextJS, Remix [Tracking] SSR & RSC Usage Oct 30, 2023
@crutchcorn
Copy link
Member

Just got off a call with @Fredkiss3 (Server actions superstar). Turns out we have a few problems with our potential API:

  1. You cannot declare a "use server" function in a "use client" component, despite the inline "use server" syntax
  2. useFormState requires "use client"

This means that we can change our usable API slightly to something like so, where we first define our shared state:

// form-base.ts
import {
    createFormFactory,
} from "@tanstack/react-form";

const {
  useForm,
  validateFormData,
  initialFormState,
} = createFormFactory({
  defaultValues: {
    name: "",
    age: 0,
  },
  // Proposed API
  onServerValidate: async (values) => {
    if (values.name.includes("server_error")) {
      return {
        name: "This is a server error",
      };
    }
  },
});

export {
  useForm,
  validateFormData,
  initialFormState
}

Then migrate our action to a "use server file:

"use server"

// form-action.ts
import  {
  validateFormData,
} from "./form-base";

export default async function submitForm(formData) {
  const results = await validateFormData({ formData });
  if (results) return results;
}

And finally we can use this in our client comp:

"use client"
import { useFormState } from "react-dom";
import {
    useTransform,
    mergeForm
} from "@tanstack/react-form";
import {useForm, initialFormState} from "./form-base";
import { submitForm } from "./form-action";

export default function CreatePerson() {
  const [state, dispatch] = useFormState(submitForm, initialFormState);
  const form = useForm({
    transform: useTransform((formBase) => mergeForm(formBase, state), [state]),
  });

  return (
    <form.Provider>
      <form action={dispatch}>
        <form.Field
          name="name"
          onChange={(val) =>
            val.includes("client_error") ? "This is a client error" : ""
          }
        >
          {(field) => (
            <>
              <label htmlFor={field.name}>First Name:</label>
              <input
                name={field.name}
                value={field.state.value}
                onBlur={field.handleBlur}
                onChange={(e) => field.handleChange(e.target.value)}
              />
              {field.state.meta.errors ? (
                <>
                  {field.state.meta.errors.map((error) => (
                    // Merge client errors and form errors
                    <div key={error}>{error}</div>
                  ))}
                </>
              ) : null}
            </>
          )}
        </form.Field>
        <button type="submit">Submit</button>
      </form>
    </form.Provider>
  );
}

However, there's a third issue here as well, that I'll outline in the next comment.

@crutchcorn
Copy link
Member

@Fredkiss3 then informed me a major problem with this bit of our POC code:

const {
  // This will throw an error in a `"use server"` usage
  useForm,
  validateFormData,
  initialFormState,
} = createFormFactory({
   // ...
});

This is because when you have a setup that imports from useState (even lazily), like so:

"use client"

import { useFormState } from "react-dom"
import someAction from "./action";

export const ClientComp = () => {
  const [data, action] = useFormState(someAction, "Hello client");

  return <form action={action}>
    <p>{data}</p>
    <button type={"submit"}>Update data</button>
  </form>
}
"use server"
// action.ts

import {data} from "./shared-code";

export default async function someAction() {
  return "Hello " + data.name;
}
// shared-code.ts
import {useState} from "react";

export const data = {
  useForm: <T>(val: T) => {
      useState(val)
  },
  name: "server"
}

You'll be presented with the following error:

./src/app/shared-code.ts
ReactServerComponentsError:

You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
Learn more: https://nextjs.org/docs/getting-started/react-essentials

   ╭─[/src/app/shared-code.ts:1:1]
 1 │ import {useState} from "react";
   ·         ────────
 2 │ 
 3 │ export const data = {
 3 │   useForm: <T>(val: T) => {
   ╰────

Maybe one of these should be marked as a client entry with "use client":
./src/app/shared-code.ts
./src/app/action.ts

This is because NextJS statistically analyzes usage of useState to ensure it's not being utilized in a useState.

This has been written about here:

https://phryneas.de/react-server-components-controversy

And documented in this issue:

apollographql/apollo-client#10974

It seems like there may be a workaround here:

apollographql/apollo-client#10974 (comment)

But it's unclear if that's still needed per:

vercel/next.js#56501

I'll reach out to some folks and see if there's anything else I can learn prior to prototyping and shipping

@phryneas
Copy link

phryneas commented Nov 5, 2023

The "safe" workaround currently suggested by the React team is

if (Object(React).useState) {

because the Object(React) will work around all static analysis right now.

To be honest, I don't feel comfortable with that workaround (another future bundler could still detect it and we'd end up in an endless cat-and-mouse game), so Apollo Client will likely use rehackt as a wrapper around React.

Small warning about that package: It has not been fully reviewed yet - reviews welcome :)


All that said: the "official" solution would be to put an exports field in your package.json and have a separate import condition for RSC where all of that code has been stripped out.

crutchcorn added a commit that referenced this issue Dec 31, 2023
* docs: add initial NextJS RSC code sample

* chore: replace react imports with rehackt imports

#480 (comment)

* chore: initial work to add server action support to React Form

* feat: got initial demo of NextJS server action error to work

* feat: add useTransform and mergeForm APIs

* chore: WIP

* feat: add transform array checking

* fix: issues with canSubmit state issues when using server validation

* fix: remove error when Field component is first ran

* fix: correct failing tests

* chore: fix ci/cd

* chore: fix next server action typings

* chore: fix various issues with templates and CI/CD

* chore: upgrade node version

* chore: change from tsup to manual rollup

* Revert "chore: change from tsup to manual rollup"

This reverts commit 994c85c.

* chore: attempt to fix tsup issues

* Revert "chore: attempt to fix tsup issues"

This reverts commit b9b1f07.

* chore: migrate form-core to use Vite

* chore: migrate Vue package to Vite

* docs: migrate form adapters to vite

* chore: migrate solid and react packages to use vite

* chore: refactor to single config generator

* chore: remove typescript 4.8

* chore: fix issues with test ci

* chore: fix PR CI

* chore: fix clean script

* Merge main into nextjs-server-actions (#545)

* chore: Update CI versions of node and pnpm (#538)

* Update node and pnpm for CI

* Update concurrency and run conditions

* docs(CONTRIBUTING.md): add instructions for previewing the docs locally (#537)

* chore: Update to Nx v17 (#539)

* Update CI run condition

* Update to Nx v17

* Attempt to fix scripts

* Fully utilise Nx for PR workflow

* chore: Use updated `publish.js` script (#540)

* Initial rename and copy

* Update relevant packages

* Remove ts-node

* Mark root as ESM

* Move getTsupConfig

* Remove eslint-plugin-compat

* Make codesandbox run Node 18

* chore: Add missing command to CI workflow (#541)

* chore: Enable Nx distributed caching (#542)

* chore: Update prettier config (#543)

* Update prettier config

* Run format

* Update gitignore

---------

Co-authored-by: fuko <43729152+fulopkovacs@users.noreply.github.com>

* Fix lockfile

---------

Co-authored-by: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com>
Co-authored-by: fuko <43729152+fulopkovacs@users.noreply.github.com>
@crutchcorn
Copy link
Member

Closing, as we have documented and working support for SSR/Server Actions

https://tanstack.com/form/latest/docs/framework/react/guides/ssr

@cannap
Copy link

cannap commented Apr 23, 2024

hi, thanks for you awesome work
is for vue this the same ? beacuse there is no onServerValidate prop
image

@crutchcorn
Copy link
Member

@cannap, there is no Vue support for SSR TanStack Usage at this time. It should be a relatively light lift, but I just don't have experience in Nuxt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants