[Tracking] SSR & RSC Usage
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?
Love this. Right now we have a blocking bug via #470, but this is a good addition
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.
Just got off a call with @Fredkiss3 (Server actions superstar). Turns out we have a few problems with our potential API:
- You cannot declare a
"use server"function in a"use client"component, despite the inline"use server"syntax -
useFormStaterequires"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.
@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:
https://github.com/apollographql/apollo-client/issues/10974
It seems like there may be a workaround here:
https://github.com/apollographql/apollo-client/issues/10974#issuecomment-1594746276
But it's unclear if that's still needed per:
https://github.com/vercel/next.js/pull/56501
I'll reach out to some folks and see if there's anything else I can learn prior to prototyping and shipping
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.
Closing, as we have documented and working support for SSR/Server Actions
https://tanstack.com/form/latest/docs/framework/react/guides/ssr
hi, thanks for you awesome work
is for vue this the same ? beacuse there is no onServerValidate prop
@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