Skip to content

Commit

Permalink
fix!: Field re-renders properly when using React Compiler
Browse files Browse the repository at this point in the history
* chore: add compiler demo

# Conflicts:
#	pnpm-lock.yaml

* chore: add React compiler ESLint rules to React package

* chore!: remove useField and useStore APIs returned from useForm

* fix: Field rerenders properly when using React Compiler

* chore: fix issues with deprecated APIs in tests

* chore: fix build and type issues

* docs: remove reference to form.useField and form.useStore

* ci: apply automated fixes and generate docs

* chore: fix ci

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
crutchcorn and autofix-ci[bot] authored Nov 29, 2024
1 parent 3567b07 commit 1a88b6f
Show file tree
Hide file tree
Showing 51 changed files with 594 additions and 240 deletions.
6 changes: 3 additions & 3 deletions docs/framework/react/guides/basic-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,12 @@ import { z } from 'zod'

## Reactivity

`@tanstack/react-form` offers various ways to subscribe to form and field state changes, most notably the `form.useStore` hook and the `form.Subscribe` component. These methods allow you to optimize your form's rendering performance by only updating components when necessary.
`@tanstack/react-form` offers various ways to subscribe to form and field state changes, most notably the `useStore(form.store)` hook and the `form.Subscribe` component. These methods allow you to optimize your form's rendering performance by only updating components when necessary.

Example:

```tsx
const firstName = form.useStore((state) => state.values.firstName)
const firstName = useStore(form.store, (state) => state.values.firstName)
//...
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
Expand All @@ -193,7 +193,7 @@ const firstName = form.useStore((state) => state.values.firstName)
/>
```

Note: The usage of the `form.useField` hook to achieve reactivity is discouraged since it is designed to be used thoughtfully within the `form.Field` component. You might want to use `form.useStore` instead.
Note: The usage of the `useField` hook to achieve reactivity is discouraged since it is designed to be used thoughtfully within the `form.Field` component. You might want to use `useStore(form.store)` instead.

## Listeners

Expand Down
10 changes: 5 additions & 5 deletions docs/framework/react/guides/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function Home() {
transform: useTransform((baseForm) => mergeForm(baseForm, state), [state]),
})

const formErrors = form.useStore((formState) => formState.errors)
const formErrors = useStore(form.store, (formState) => formState.errors)

return (
<form action={handleForm.url} method="post" encType={'multipart/form-data'}>
Expand Down Expand Up @@ -249,7 +249,7 @@ export const ClientComp = () => {
transform: useTransform((baseForm) => mergeForm(baseForm, state!), [state]),
})

const formErrors = form.useStore((formState) => formState.errors)
const formErrors = useStore(form.store, (formState) => formState.errors)

return (
<form action={action as never} onSubmit={() => form.handleSubmit()}>
Expand Down Expand Up @@ -410,8 +410,8 @@ export default function Index() {
[actionData],
),
})
const formErrors = form.useStore((formState) => formState.errors)

const formErrors = useStore(form.store, (formState) => formState.errors)

return (
<Form method="post" onSubmit={() => form.handleSubmit()}>
Expand Down Expand Up @@ -456,4 +456,4 @@ export default function Index() {
}
```

Here, we're using [Remix's `useActionData` hook](https://remix.run/docs/en/main/hooks/use-action-data) and TanStack Form's `useTransform` hook to merge state returned from the server action with the form state.
Here, we're using [Remix's `useActionData` hook](https://remix.run/docs/en/main/hooks/use-action-data) and TanStack Form's `useTransform` hook to merge state returned from the server action with the form state.
2 changes: 1 addition & 1 deletion docs/framework/react/guides/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export default function App() {

// Subscribe to the form's error map so that updates to it will render
// alternately, you can use `form.Subscribe`
const formErrorMap = form.useStore((state) => state.errorMap)
const formErrorMap = useStore(form.store, (state) => state.errorMap)

return (
<div>
Expand Down
2 changes: 1 addition & 1 deletion docs/framework/react/reference/functions/useform.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ This API encapsulates all the necessary functionalities related to the form. It

## Defined in

[useForm.tsx:54](https://github.com/TanStack/form/blob/main/packages/react-form/src/useForm.tsx#L54)
[useForm.tsx:67](https://github.com/TanStack/form/blob/main/packages/react-form/src/useForm.tsx#L67)
2 changes: 1 addition & 1 deletion docs/reference/classes/formapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ The current state of the form.
**Note:**
Do not use `state` directly, as it is not reactive.
Please use form.useStore() utility to subscribe to state
Please use useStore(form.store) utility to subscribe to state
#### Defined in
Expand Down
2 changes: 1 addition & 1 deletion examples/lit/simple/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"lit": "^3.2.1"
},
"devDependencies": {
"vite": "^5.4.10"
"vite": "^5.4.11"
},
"browserslist": {
"production": [
Expand Down
2 changes: 1 addition & 1 deletion examples/lit/ui-libraries/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"lit": "^3.2.1"
},
"devDependencies": {
"vite": "^5.4.10"
"vite": "^5.4.11"
},
"browserslist": {
"production": [
Expand Down
2 changes: 1 addition & 1 deletion examples/react/array/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.3",
"vite": "^5.4.10"
"vite": "^5.4.11"
},
"browserslist": {
"production": [
Expand Down
16 changes: 16 additions & 0 deletions examples/react/compiler/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @ts-check
const reactCompiler = require('eslint-plugin-react-compiler')

/** @type {import('eslint').Linter.Config} */
const config = {
plugins: {
'react-compiler': reactCompiler,
},
extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
rules: {
'react/no-children-prop': 'off',
'react-compiler/react-compiler': 'error',
},
}

module.exports = config
27 changes: 27 additions & 0 deletions examples/react/compiler/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

pnpm-lock.yaml
yarn.lock
package-lock.json

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
6 changes: 6 additions & 0 deletions examples/react/compiler/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install`
- `npm run dev`
16 changes: 16 additions & 0 deletions examples/react/compiler/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />

<title>TanStack Form React Simple Example App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
40 changes: 40 additions & 0 deletions examples/react/compiler/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@tanstack/form-example-react-compiler",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port=3001",
"build": "vite build",
"preview": "vite preview",
"_test:types": "tsc"
},
"dependencies": {
"@tanstack/react-form": "^0.38.0",
"react": "^19.0.0-rc.1",
"react-dom": "^19.0.0-rc.1"
},
"devDependencies": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc",
"@vitejs/plugin-react": "^4.3.3",
"babel-plugin-react-compiler": "^19.0.0-beta-0dec889-20241115",
"eslint-plugin-react-compiler": "^19.0.0-beta-0dec889-20241115",
"vite": "^5.4.11"
},
"overrides": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
13 changes: 13 additions & 0 deletions examples/react/compiler/public/emblem-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
118 changes: 118 additions & 0 deletions examples/react/compiler/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { useForm } from '@tanstack/react-form'
import * as React from 'react'
import { createRoot } from 'react-dom/client'
import type { FieldApi } from '@tanstack/react-form'

function FieldInfo({ field }: { field: FieldApi<any, any, any, any> }) {
return (
<>
{field.state.meta.isTouched && field.state.meta.errors.length ? (
<em>{field.state.meta.errors.join(',')}</em>
) : null}
{field.state.meta.isValidating ? 'Validating...' : null}
</>
)
}

export default function App() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
})

return (
<div>
<h1>Simple Form Example</h1>
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<div>
{/* A type-safe field component*/}
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return (
value.includes('error') && 'No "error" allowed in first name'
)
},
}}
children={(field) => {
// Avoid hasty abstractions. Render props are great!
return (
<>
<label htmlFor={field.name}>First Name:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field} />
</>
)
}}
/>
</div>
<div>
<form.Field
name="lastName"
children={(field) => (
<>
<label htmlFor={field.name}>Last Name:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field} />
</>
)}
/>
</div>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<>
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
<button type="reset" onClick={() => form.reset()}>
Reset
</button>
</>
)}
/>
</form>
</div>
)
}

const rootElement = document.getElementById('root')!

createRoot(rootElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
23 changes: 23 additions & 0 deletions examples/react/compiler/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
Loading

0 comments on commit 1a88b6f

Please sign in to comment.