Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/solid-form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@
"@tanstack/solid-store": "^0.9.1"
},
"devDependencies": {
"solid-js": "^1.9.9",
"@solidjs/web": "2.0.0-beta.6",
"@testing-library/dom": "^10.4.0",
"solid-js": "2.0.0-beta.6",
"vite": "^7.2.2",
"vite-plugin-solid": "^2.11.8"
"vite-plugin-solid": "3.0.0-next.4"
},
"peerDependencies": {
"solid-js": ">=1.9.9"
"solid-js": ">=2.0.0-beta.6 <2.0.0-experimental.0"
}
}
49 changes: 31 additions & 18 deletions packages/solid-form/src/createField.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { FieldApi } from '@tanstack/form-core'
import {
createComponent,
createComputed,
createRenderEffect,
createSignal,
onCleanup,
onMount,
onSettled,
} from 'solid-js'
import { useStore } from '@tanstack/solid-store'
import type {
Expand Down Expand Up @@ -254,12 +253,16 @@ function makeFieldReactive<
const [field, setField] = createSignal(fieldApi, { equals: false })
// Handle shallow comparison to make sure that Derived doesn't create a new setField call every time
const store = useStore(fieldApi.store, (store) => store)
// Run before initial render
createComputed(() => {
// Use the store to track dependencies
store()
setField(fieldApi)
})
createRenderEffect(
() => {
// Use the store to track dependencies
store()
return fieldApi
},
(nextFieldApi) => {
setField(() => nextFieldApi)
},
)
return field
}

Expand Down Expand Up @@ -346,25 +349,35 @@ export function createField<

let mounted = false
// Instantiates field meta and removes it when unrendered
onMount(() => {
onSettled(() => {
api.update(opts())
const cleanupFn = api.mount()
mounted = true
onCleanup(() => {
cleanupFn()
return () => {
mounted = false
})
cleanupFn()
}
})

/**
* fieldApi.update should not have any side effects. Think of it like a `useRef`
* that we need to keep updated every render with the most up-to-date information.
*
* createComputed to make sure this effect runs before render effects
* createRenderEffect keeps the api options in sync before user effects run.
*/
createComputed(() => {
if (!mounted) return
api.update(opts())
})
createRenderEffect(
() => {
const nextOptions = opts()
return mounted ? nextOptions : undefined
},
(options) => {
if (options) {
api.update(options)
}

return undefined
},
)

return makeFieldReactive<
TParentData,
Expand Down
25 changes: 12 additions & 13 deletions packages/solid-form/src/createFieldGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FieldGroupApi, functionalUpdate } from '@tanstack/form-core'
import { useStore } from '@tanstack/solid-store'
import { onCleanup, onMount } from 'solid-js'
import { onSettled } from 'solid-js'
import type { Component, JSX, ParentProps } from 'solid-js'
import type {
DeepKeysOfType,
Expand Down Expand Up @@ -194,26 +194,25 @@ export function createFieldGroup<
> = api as never

extendedApi.AppForm = (appFormProps) => <form.AppForm {...appFormProps} />
extendedApi.AppField = (props) => (
<form.AppField {...(api.getFormFieldOptions(props) as any)} />
)
extendedApi.Field = (props) => (
<form.Field {...(api.getFormFieldOptions(props) as any)} />
)
extendedApi.AppField = (props) => {
const fieldOptions = () => api.getFormFieldOptions(props)
return <form.AppField {...(fieldOptions() as any)} />
}
extendedApi.Field = (props) => {
const fieldOptions = () => api.getFormFieldOptions(props)
return <form.Field {...(fieldOptions() as any)} />
}
extendedApi.Subscribe = (props) => {
const data = useStore(api.store, props.selector)

return functionalUpdate(props.children, data()) as Element
}

let mounted = false
onMount(() => {
onSettled(() => {
const cleanupFn = api.mount()
mounted = true
onCleanup(() => {
return () => {
cleanupFn()
mounted = false
})
}
})

return Object.assign(extendedApi, {
Expand Down
12 changes: 9 additions & 3 deletions packages/solid-form/src/createForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FormApi, functionalUpdate } from '@tanstack/form-core'
import { createComputed, onMount } from 'solid-js'
import { createRenderEffect, onSettled } from 'solid-js'
import { useStore } from '@tanstack/solid-store'
import { Field, createField } from './createField'
import type {
Expand Down Expand Up @@ -239,13 +239,19 @@ export function createForm<
extendedApi.Subscribe = (props) =>
functionalUpdate(props.children, useStore(api.store, props.selector))

onMount(api.mount)
onSettled(api.mount)

/**
* formApi.update should not have any side effects. Think of it like a `useRef`
* that we need to keep updated every render with the most up-to-date information.
*/
createComputed(() => api.update(opts?.()))
createRenderEffect(
() => opts?.(),
(options) => {
api.update(options)
return undefined
},
)

return extendedApi
}
26 changes: 9 additions & 17 deletions packages/solid-form/src/createFormHook.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
createComponent,
createContext,
mergeProps,
splitProps,
merge,
omit,
useContext,
} from 'solid-js'
import { createFieldGroup } from './createFieldGroup'
Expand Down Expand Up @@ -351,26 +351,18 @@ export function createFormHook<

const AppForm = ((formProps) => {
return (
<opts.formContext.Provider value={form}>
{formProps.children}
</opts.formContext.Provider>
<opts.formContext value={form}>{formProps.children}</opts.formContext>
)
}) as Component<ParentProps>

const AppField = ((_props) => {
const [childProps, fieldProps] = splitProps(_props, ['children'])
const fieldProps = omit(_props, 'children')
return (
<form.Field {...fieldProps}>
{(field) => (
<opts.fieldContext.Provider value={field}>
{createComponent(
() =>
childProps.children(
Object.assign(field, opts.fieldComponents),
),
{},
)}
</opts.fieldContext.Provider>
<opts.fieldContext value={field}>
{_props.children(Object.assign(field, opts.fieldComponents))}
</opts.fieldContext>
)}
</form.Field>
)
Expand Down Expand Up @@ -472,7 +464,7 @@ export function createFormHook<
return (innerProps) =>
createComponent(
render as Component<any>,
mergeProps(props ?? {}, innerProps),
props ? merge(props, innerProps) : innerProps,
)
}

Expand Down Expand Up @@ -560,7 +552,7 @@ export function createFormHook<
const fieldGroupApi = createFieldGroup(() => fieldGroupProps)
return createComponent(
render as Component<any>,
mergeProps(props ?? {}, innerProps, { group: fieldGroupApi as any }),
merge(props ?? {}, innerProps, { group: fieldGroupApi as any }),
)
}
}
Expand Down
14 changes: 7 additions & 7 deletions packages/solid-form/tests/createField.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { render, waitFor } from '@solidjs/testing-library'
import { userEvent } from '@testing-library/user-event'
import { Index, Show } from 'solid-js'
import { Repeat, Show } from 'solid-js'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

SolidJS 2.0 Repeat component API signature

💡 Result:

SolidJS 2.0 does not have a built-in "Repeat" component in core. Core control flow components are , , /, (replaces Suspense), and (replaces ErrorBoundary). In SolidJS 2.0 beta: - from 1.x is replaced by - children now receive accessors: (item, i) => ... where item and i are called. A component exists in the third-party @solid-primitives/range package (updated Feb 2026), with API: function Repeat<T>(props: { times: number; fallback?: T; children: ((index: number) => T) | T; }): Accessor<T[]>; Example: <Repeat times={10}> <div></div> </Repeat> or with render prop: <Repeat times={10}> {index => <div>{index}</div>} </Repeat> No evidence of core Repeat in 2.0 docs/RFCs/release notes. Official 2.0 docs at docs.solidjs.com cover 1.x; 2.0 migration in GitHub next branch.

Citations:


Repeat is not exported from solid-js 2.0 — use <For keyed={false}> instead.

Repeat does not exist in SolidJS 2.0 core. The import at line 4 will fail. The correct migration from Index is <For keyed={false}>, which receives the signature (item, i) => ... where both are accessor functions. If a Repeat component is needed for index-only iteration, it exists as a third-party component in @solid-primitives/range (not in core solid-js).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-form/tests/createField.test.tsx` at line 4, The test imports
Repeat from 'solid-js' which doesn't exist in Solid 2.0; remove the Repeat
import and replace any usage of Repeat with For keyed={false} from 'solid-js'
(keep Show import as-is), adapting the render callback to the For signature
(item, i) => ... where item and i are accessors; if you actually need an
index-only Repeat component, import it from `@solid-primitives/range` instead.

import { createForm } from '../src/index'
import { sleep } from './utils'

Expand Down Expand Up @@ -402,8 +402,8 @@ describe('createField', () => {
<div>
<Show when={field().state.value.length > 0}>
{/* Do not change this to For or the test will fail */}
<Index each={field().state.value}>
{(_, i) => {
<Repeat count={field().state.value.length}>
{(i) => {
return (
<form.Field name={`people[${i}]`}>
{(subField) => (
Expand All @@ -430,7 +430,7 @@ describe('createField', () => {
</form.Field>
)
}}
</Index>
</Repeat>
</Show>

<button onClick={() => field().pushValue('')} type="button">
Expand Down Expand Up @@ -496,8 +496,8 @@ describe('createField', () => {
<div>
<Show when={field().state.value.length > 0}>
{/* Do not change this to For or the test will fail */}
<Index each={field().state.value}>
{(_, i) => {
<Repeat count={field().state.value.length}>
{(i) => {
return (
<form.Field name={`people[${i}].name`}>
{(subField) => (
Expand All @@ -524,7 +524,7 @@ describe('createField', () => {
</form.Field>
)
}}
</Index>
</Repeat>
</Show>

<button
Expand Down
23 changes: 11 additions & 12 deletions packages/solid-form/tests/createForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { render, screen, waitFor } from '@solidjs/testing-library'
import { userEvent } from '@testing-library/user-event'
import { Index, Show, createSignal, onCleanup } from 'solid-js'
import { Repeat, Show, createSignal, onCleanup } from 'solid-js'
import { createForm } from '../src/index'
import { sleep } from './utils'
import type { FormValidationErrorMap } from '../src/index'
Expand Down Expand Up @@ -502,8 +502,8 @@ describe('createForm', () => {
<>
<form.Field name="foo" mode="array">
{(arrayField) => (
<Index each={arrayField().state.value}>
{(_, i) => (
<Repeat count={arrayField().state.value.length}>
{(i) => (
<form.Field name={`foo[${i}].name`}>
{(field) => {
expect(field().name).toBe(`foo[${i}].name`)
Expand All @@ -512,7 +512,7 @@ describe('createForm', () => {
}}
</form.Field>
)}
</Index>
</Repeat>
)}
</form.Field>
<button
Expand Down Expand Up @@ -560,8 +560,8 @@ describe('createForm', () => {
Add Item
</button>
<div>
<Index each={fieldArray().state.value}>
{(_, index) => (
<Repeat count={fieldArray().state.value.length}>
{(index) => (
<form.Field name={`items[${index}]`}>
{(field) => (
<div>
Expand All @@ -573,7 +573,7 @@ describe('createForm', () => {
)}
</form.Field>
)}
</Index>
</Repeat>
</div>
</div>
)}
Expand Down Expand Up @@ -610,10 +610,9 @@ describe('createForm', () => {
<form.Field name="foo" mode="array">
{(arrayField) => (
// This unit test provides different result based on
// using For vs. Index. Unit test both
// once that's fixed.
<Index each={arrayField().state.value}>
{(_, i) => (
// Keep index-based array field rendering covered.
<Repeat count={arrayField().state.value.length}>
{(i) => (
<form.Field name={`foo[${i}].name`}>
{(field) => {
expect(field().name).toBe(`foo[${i}].name`)
Expand All @@ -622,7 +621,7 @@ describe('createForm', () => {
}}
</form.Field>
)}
</Index>
</Repeat>
)}
</form.Field>
<button
Expand Down
9 changes: 6 additions & 3 deletions packages/solid-form/tests/createFormHook.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -593,9 +593,12 @@ describe('createFormHook', () => {
...formOpts,
props: { status: 'idle' as 'idle' | 'loading' },
render: (props) => {
createEffect(() => {
spy(props.status)
})
createEffect(
() => props.status,
(status) => {
spy(status)
},
)
return <div data-testid="status">{props.status}</div>
},
})
Expand Down
Loading
Loading