Compare commits
3 Commits
fix/ui-imp
...
perf/field
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6124e26468 | ||
|
|
5342d303ea | ||
|
|
766236f38e |
@@ -136,8 +136,8 @@
|
||||
"scheduler": "0.25.0",
|
||||
"sonner": "^1.7.2",
|
||||
"ts-essentials": "10.0.3",
|
||||
"use-context-selector": "2.0.0",
|
||||
"uuid": "10.0.0"
|
||||
"uuid": "10.0.0",
|
||||
"zustand": "5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.26.4",
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import type { RenderedField } from 'payload'
|
||||
|
||||
import { createContext, use } from 'react'
|
||||
import {
|
||||
createContext as createSelectorContext,
|
||||
useContextSelector,
|
||||
useContext as useFullContext,
|
||||
} from 'use-context-selector'
|
||||
// import {
|
||||
// createContext as createSelectorContext,
|
||||
// useContextSelector,
|
||||
// useContext as useFullContext,
|
||||
// } from 'use-context-selector'
|
||||
|
||||
import type { Context, FormFieldsContext as FormFieldsContextType } from './types.js'
|
||||
|
||||
@@ -22,7 +22,7 @@ const ProcessingContext = createContext(false)
|
||||
const BackgroundProcessingContext = createContext(false)
|
||||
const ModifiedContext = createContext(false)
|
||||
const InitializingContext = createContext(false)
|
||||
const FormFieldsContext = createSelectorContext<FormFieldsContextType>([{}, () => null])
|
||||
// const FormFieldsContext = createSelectorContext<FormFieldsContextType>([{}, () => null])
|
||||
|
||||
export type RenderedFieldSlots = Map<string, RenderedField>
|
||||
|
||||
@@ -56,20 +56,20 @@ const useFormInitializing = (): boolean => use(InitializingContext)
|
||||
*/
|
||||
const useFormFields = <Value = unknown>(
|
||||
selector: (context: FormFieldsContextType) => Value,
|
||||
): Value => useContextSelector(FormFieldsContext, selector)
|
||||
): Value => {}
|
||||
|
||||
/**
|
||||
* Get the state of all form fields.
|
||||
*
|
||||
* @see https://payloadcms.com/docs/admin/react-hooks#useallformfields
|
||||
*/
|
||||
const useAllFormFields = (): FormFieldsContextType => useFullContext(FormFieldsContext)
|
||||
const useAllFormFields = (): FormFieldsContextType => {}
|
||||
|
||||
export {
|
||||
BackgroundProcessingContext,
|
||||
DocumentFormContext,
|
||||
FormContext,
|
||||
FormFieldsContext,
|
||||
// FormFieldsContext,
|
||||
FormWatchContext,
|
||||
InitializingContext,
|
||||
ModifiedContext,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
reduceFieldsToValues,
|
||||
wait,
|
||||
} from 'payload/shared'
|
||||
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type {
|
||||
@@ -41,8 +41,6 @@ import {
|
||||
BackgroundProcessingContext,
|
||||
DocumentFormContext,
|
||||
FormContext,
|
||||
FormFieldsContext,
|
||||
FormWatchContext,
|
||||
InitializingContext,
|
||||
ModifiedContext,
|
||||
ProcessingContext,
|
||||
@@ -50,12 +48,12 @@ import {
|
||||
useDocumentForm,
|
||||
} from './context.js'
|
||||
import { errorMessages } from './errorMessages.js'
|
||||
import { fieldReducer } from './fieldReducer.js'
|
||||
import { initContextState } from './initContextState.js'
|
||||
import { FormStateStoreContext, useCreateFormStateStore, useFormStateStore } from './store.js'
|
||||
|
||||
const baseClass = 'form'
|
||||
|
||||
export const Form: React.FC<FormProps> = (props) => {
|
||||
const FormComponent: React.FC<FormProps> = (props) => {
|
||||
const { id, collectionSlug, docConfig, docPermissions, getDocPreferences, globalSlug } =
|
||||
useDocumentInfo()
|
||||
|
||||
@@ -119,9 +117,11 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
const abortResetFormRef = useRef<AbortController>(null)
|
||||
const isFirstRenderRef = useRef(true)
|
||||
|
||||
const fieldsReducer = useReducer(fieldReducer, {}, () => initialState)
|
||||
const store = useFormStateStore((store) => store)
|
||||
|
||||
const [formState, dispatchFields] = fieldsReducer
|
||||
const formState = useMemo(() => store.state, [store])
|
||||
|
||||
const dispatchFields = useMemo(() => store.dispatchFields, [store])
|
||||
|
||||
contextRef.current.fields = formState
|
||||
|
||||
@@ -190,13 +190,13 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
await Promise.all(validationPromises)
|
||||
|
||||
if (!dequal(contextRef.current.fields, validatedFieldState)) {
|
||||
dispatchFields({ type: 'REPLACE_STATE', state: validatedFieldState })
|
||||
store.dispatchFields({ type: 'REPLACE_STATE', state: validatedFieldState })
|
||||
}
|
||||
|
||||
setIsValid(isValid)
|
||||
|
||||
return isValid
|
||||
}, [collectionSlug, config, dispatchFields, id, operation, t, user, documentForm])
|
||||
}, [collectionSlug, config, store, id, operation, t, user, documentForm])
|
||||
|
||||
const submit = useCallback(
|
||||
async (options: SubmitOptions = {}, e): Promise<void> => {
|
||||
@@ -797,37 +797,35 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
>
|
||||
<DocumentFormContextComponent {...documentFormContextProps}>
|
||||
<FormContext value={contextRef.current}>
|
||||
<FormWatchContext
|
||||
value={{
|
||||
fields: formState,
|
||||
...contextRef.current,
|
||||
}}
|
||||
>
|
||||
<SubmittedContext value={submitted}>
|
||||
<InitializingContext value={!isMounted || (isMounted && initializing)}>
|
||||
<ProcessingContext value={processing}>
|
||||
<BackgroundProcessingContext value={backgroundProcessing}>
|
||||
<ModifiedContext value={modified}>
|
||||
{/* eslint-disable-next-line @eslint-react/no-context-provider */}
|
||||
<FormFieldsContext.Provider value={fieldsReducer}>
|
||||
{children}
|
||||
</FormFieldsContext.Provider>
|
||||
</ModifiedContext>
|
||||
</BackgroundProcessingContext>
|
||||
</ProcessingContext>
|
||||
</InitializingContext>
|
||||
</SubmittedContext>
|
||||
</FormWatchContext>
|
||||
<SubmittedContext value={submitted}>
|
||||
<InitializingContext value={!isMounted || (isMounted && initializing)}>
|
||||
<ProcessingContext value={processing}>
|
||||
<BackgroundProcessingContext value={backgroundProcessing}>
|
||||
<ModifiedContext value={modified}>{children}</ModifiedContext>
|
||||
</BackgroundProcessingContext>
|
||||
</ProcessingContext>
|
||||
</InitializingContext>
|
||||
</SubmittedContext>
|
||||
</FormContext>
|
||||
</DocumentFormContextComponent>
|
||||
</El>
|
||||
)
|
||||
}
|
||||
|
||||
export const Form = (props) => {
|
||||
const formStateStore = useCreateFormStateStore(props.initialState)
|
||||
|
||||
return (
|
||||
<FormStateStoreContext value={formStateStore}>
|
||||
<FormComponent {...props} />
|
||||
</FormStateStoreContext>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DocumentFormContext,
|
||||
FormContext,
|
||||
FormFieldsContext,
|
||||
// FormFieldsContext,
|
||||
FormWatchContext,
|
||||
ModifiedContext,
|
||||
ProcessingContext,
|
||||
|
||||
132
packages/ui/src/forms/Form/store.ts
Normal file
132
packages/ui/src/forms/Form/store.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type { FormState } from 'payload'
|
||||
import type { StoreApi } from 'zustand'
|
||||
|
||||
import { createContext, useContext, useRef } from 'react'
|
||||
import { createStore, useStore } from 'zustand'
|
||||
|
||||
import { fieldReducer } from './fieldReducer.js'
|
||||
|
||||
// export const useFormFields = create((set, initialState) => ({
|
||||
// fields: {} as FormState,
|
||||
// } satisfies FormState))
|
||||
|
||||
export type FormStateStore = {
|
||||
dispatchFields: (action: any) => void
|
||||
state: FormState
|
||||
}
|
||||
|
||||
export const FormStateStoreContext = createContext<null | StoreApi<FormStateStore>>(null)
|
||||
|
||||
export const createFormStateStore = (initialState: FormState) =>
|
||||
createStore<FormStateStore>()((set) => ({
|
||||
dispatchFields: (action) =>
|
||||
set((store) => ({ ...store, state: fieldReducer(store.state, action) })),
|
||||
state: initialState,
|
||||
}))
|
||||
|
||||
export const useCreateFormStateStore = (initialState: FormState) => {
|
||||
const storeRef = useRef<null | StoreApi<FormStateStore>>(null)
|
||||
|
||||
if (storeRef.current === null) {
|
||||
storeRef.current = createFormStateStore(initialState)
|
||||
}
|
||||
|
||||
return storeRef.current
|
||||
}
|
||||
|
||||
export const useFormStateStore = <T>(selector: (store: FormStateStore) => T): T => {
|
||||
const formStateStoreContext = useContext(FormStateStoreContext)
|
||||
|
||||
if (!formStateStoreContext) {
|
||||
throw new Error(`useCounterStore must be used within CounterStoreProvider`)
|
||||
}
|
||||
|
||||
return useStore(formStateStoreContext, selector)
|
||||
}
|
||||
|
||||
// import { randomUUID } from 'crypto'
|
||||
// import type { FormState } from 'payload'
|
||||
|
||||
// type Listener = () => void
|
||||
|
||||
// type FieldSubscriber = {
|
||||
// callback: Listener
|
||||
// path: string
|
||||
// }
|
||||
|
||||
// type FormStateStore = {
|
||||
// getSnapshot: (path: string) => any
|
||||
// setField: (path: string, value: any) => void
|
||||
// setFields: (values: FormState) => void
|
||||
// subscribe: (path: string, callback: Listener) => () => void
|
||||
// }
|
||||
|
||||
// function create(initialState: FormState = {}): FormStateStore {
|
||||
// let state = { ...initialState }
|
||||
// const subscribers = new Set<FieldSubscriber>()
|
||||
|
||||
// const subscribe = (path: string, callback: Listener) => {
|
||||
// const subscriber = { callback, path }
|
||||
// subscribers.add(subscriber)
|
||||
|
||||
// return () => {
|
||||
// subscribers.delete(subscriber)
|
||||
// }
|
||||
// }
|
||||
|
||||
// const getSnapshot = (path: string) => {
|
||||
// return state[path]
|
||||
// }
|
||||
|
||||
// const setField = (path: string, value: any) => {
|
||||
// if (state[path] !== value) {
|
||||
// state = { ...state, [path]: value }
|
||||
|
||||
// // Notify only subscribers of this specific field
|
||||
// subscribers.forEach((subscriber) => {
|
||||
// if (subscriber.path === path) {
|
||||
// subscriber.callback()
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// const setFields = (values: FormState) => {
|
||||
// const changedFields = new Set<string>()
|
||||
|
||||
// // Track which fields actually changed
|
||||
// Object.entries(values).forEach(([path, value]) => {
|
||||
// if (state[path] !== value) {
|
||||
// changedFields.add(path)
|
||||
// }
|
||||
// })
|
||||
|
||||
// if (changedFields.size > 0) {
|
||||
// state = { ...state, ...values }
|
||||
|
||||
// // Notify only subscribers of changed fields
|
||||
// subscribers.forEach((subscriber) => {
|
||||
// if (changedFields.has(subscriber.path)) {
|
||||
// subscriber.callback()
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// return {
|
||||
// getSnapshot,
|
||||
// setField,
|
||||
// setFields,
|
||||
// subscribe,
|
||||
// }
|
||||
// }
|
||||
|
||||
// const globalStore = new Map<string, FormStateStore>()
|
||||
|
||||
// export const createFormStateStore = (initialState) => {
|
||||
// const id = randomUUID()
|
||||
// const formStateStore = create(initialState)
|
||||
// globalStore.set(id, formStateStore)
|
||||
// }
|
||||
|
||||
// export { formStateStore }
|
||||
@@ -17,12 +17,12 @@ import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import {
|
||||
useDocumentForm,
|
||||
useForm,
|
||||
useFormFields,
|
||||
useFormInitializing,
|
||||
useFormModified,
|
||||
useFormProcessing,
|
||||
useFormSubmitted,
|
||||
} from '../Form/context.js'
|
||||
import { useFormStateStore } from '../Form/store.js'
|
||||
import { useFieldPath } from '../RenderFields/context.js'
|
||||
|
||||
/**
|
||||
@@ -52,8 +52,11 @@ export const useField = <TValue,>(options?: Options): FieldType<TValue> => {
|
||||
const { id, collectionSlug } = useDocumentInfo()
|
||||
const operation = useOperation()
|
||||
|
||||
const dispatchField = useFormFields(([_, dispatch]) => dispatch)
|
||||
const field = useFormFields(([fields]) => (fields && fields?.[path]) || null)
|
||||
const field = useFormStateStore((store) => store.state[path])
|
||||
|
||||
const value = field?.value
|
||||
|
||||
const dispatchField = useFormStateStore((store) => store.dispatchFields)
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { config } = useConfig()
|
||||
@@ -63,7 +66,6 @@ export const useField = <TValue,>(options?: Options): FieldType<TValue> => {
|
||||
const modified = useFormModified()
|
||||
|
||||
const filterOptions = field?.filterOptions
|
||||
const value = field?.value as TValue
|
||||
const initialValue = field?.initialValue as TValue
|
||||
const valid = typeof field?.valid === 'boolean' ? field.valid : true
|
||||
const showError = valid === false && submitted
|
||||
|
||||
42
pnpm-lock.yaml
generated
42
pnpm-lock.yaml
generated
@@ -1621,12 +1621,12 @@ importers:
|
||||
ts-essentials:
|
||||
specifier: 10.0.3
|
||||
version: 10.0.3(typescript@5.7.3)
|
||||
use-context-selector:
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0(react@19.1.0)(scheduler@0.25.0)
|
||||
uuid:
|
||||
specifier: 10.0.0
|
||||
version: 10.0.0
|
||||
zustand:
|
||||
specifier: 5.0.4
|
||||
version: 5.0.4(@types/react@19.1.0)(immer@9.0.21)(react@19.1.0)
|
||||
devDependencies:
|
||||
'@babel/cli':
|
||||
specifier: 7.26.4
|
||||
@@ -8594,6 +8594,7 @@ packages:
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch-native@1.6.4:
|
||||
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
||||
@@ -10282,12 +10283,6 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
use-context-selector@2.0.0:
|
||||
resolution: {integrity: sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==}
|
||||
peerDependencies:
|
||||
react: 19.1.0
|
||||
scheduler: '>=0.19.0'
|
||||
|
||||
use-isomorphic-layout-effect@1.2.0:
|
||||
resolution: {integrity: sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==}
|
||||
peerDependencies:
|
||||
@@ -10538,6 +10533,24 @@ packages:
|
||||
zod@3.23.8:
|
||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||
|
||||
zustand@5.0.4:
|
||||
resolution: {integrity: sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18.0.0'
|
||||
immer: '>=9.0.6'
|
||||
react: 19.1.0
|
||||
use-sync-external-store: '>=1.2.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
use-sync-external-store:
|
||||
optional: true
|
||||
|
||||
zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
@@ -20564,11 +20577,6 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
use-context-selector@2.0.0(react@19.1.0)(scheduler@0.25.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
scheduler: 0.25.0
|
||||
|
||||
use-isomorphic-layout-effect@1.2.0(@types/react@19.1.0)(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@@ -20827,4 +20835,10 @@ snapshots:
|
||||
|
||||
zod@3.23.8: {}
|
||||
|
||||
zustand@5.0.4(@types/react@19.1.0)(immer@9.0.21)(react@19.1.0):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
immer: 9.0.21
|
||||
react: 19.1.0
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
Reference in New Issue
Block a user