Last active
September 14, 2023 19:35
-
-
Save avin/6b358dd73dbd7b178790b8ec9fee536c to your computer and use it in GitHub Desktop.
Slim form control
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {memo, useCallback, useEffect, useRef} from 'react'; | |
import {useControl, useForm, useValidation} from './utils/useFormState'; | |
// function useTraceUpdate(props: any) { | |
// const prev = useRef(props); | |
// useEffect(() => { | |
// const changedProps = Object.entries(props).reduce((ps: any, [k, v]) => { | |
// if (prev.current[k] !== v) { | |
// ps[k] = [prev.current[k], v]; | |
// } | |
// return ps; | |
// }, {}); | |
// if (Object.keys(changedProps).length > 0) { | |
// console.log('Changed props:', changedProps); | |
// } | |
// prev.current = props; | |
// }); | |
// } | |
const ChildApp = memo(({ control }: any) => { | |
const { watch, setValue } = useControl(control); | |
const arr = watch('arr'); | |
// const foo = watch('foo'); | |
console.log('renderChild'); | |
return ( | |
<div> | |
<div>child app arr: {arr}</div> | |
<button | |
type="button" | |
onClick={() => { | |
setValue('foo', new Date().getTime()); | |
}} | |
> | |
+ | |
</button> | |
</div> | |
); | |
}); | |
const ChildAppWithAll = memo(({ control }: any) => { | |
const { watchAll } = useControl(control); | |
const formState = watchAll(); | |
return ( | |
<div> | |
<div>formState: {JSON.stringify(formState)};</div> | |
</div> | |
); | |
}); | |
const ChildValidation = memo((props: any) => { | |
const { control } = props; | |
const validationFunc = useCallback((fields: any) => { | |
const errors: any = {}; | |
if (!fields.foo) { | |
errors.foo = 'no foo'; | |
} | |
if (!fields.bar) { | |
errors.bar = 'no bar'; | |
} | |
if (!fields.arr) { | |
errors.arr = 'no arr' | |
} | |
return errors; | |
}, []); | |
const {isValid, errors} = useValidation(control, validationFunc); | |
// useTraceUpdate(props) | |
console.log('renderChildValidation'); | |
return ( | |
<div> | |
<div>isValid: {String(isValid)};</div> | |
<div>errors: {JSON.stringify(errors)};</div> | |
</div> | |
); | |
}); | |
function App() { | |
const { watch, setValue, getFormValues, control } = useForm<{ | |
foo: string, | |
bar: string, | |
arr: string, | |
}>({ | |
initialValues: { | |
foo: '11111', | |
bar: '22222', | |
arr: '33333', | |
}, | |
}); | |
const foo = watch('foo'); | |
const bar = watch('bar'); | |
const handleArrInputChange = useCallback( | |
(event: React.ChangeEvent<HTMLInputElement>) => { | |
setValue('arr', event.currentTarget.value); | |
}, | |
[setValue], | |
); | |
const handleFooInputChange = useCallback( | |
(event: React.ChangeEvent<HTMLInputElement>) => { | |
setValue('foo', event.currentTarget.value); | |
}, | |
[setValue], | |
); | |
const handleBarInputChange = useCallback( | |
(event: React.ChangeEvent<HTMLInputElement>) => { | |
setValue('bar', event.currentTarget.value); | |
}, | |
[setValue], | |
); | |
const handleFormSubmit = useCallback( | |
(event: React.FormEvent<HTMLFormElement>) => { | |
event.preventDefault(); | |
console.log('formState:', getFormValues()); | |
}, | |
[getFormValues], | |
); | |
console.log('render'); | |
return ( | |
<form onSubmit={handleFormSubmit}> | |
<div>foo: {foo}</div> | |
{/*<div>bar: {bar}</div>*/} | |
<div> | |
arr: <input type="text" name="arr" onChange={handleArrInputChange} /> | |
</div> | |
<div> | |
foo: <input type="text" name="foo" onChange={handleFooInputChange} /> | |
</div> | |
<div> | |
bar: <input type="text" name="bar" onChange={handleBarInputChange} /> | |
</div> | |
<ChildApp control={control} /> | |
<ChildAppWithAll control={control} /> | |
<ChildValidation control={control} /> | |
<div> | |
<button type="submit">submit</button> | |
</div> | |
</form> | |
); | |
} | |
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |
// Функция для проверки эквивалентности двух объектов | |
function objectsAreEqual(objA: Record<string, any>, objB: Record<string, any>): boolean { | |
// Проверка идентичности ссылок | |
if (objA === objB) { | |
return true; | |
} | |
// Проверка наличия объектов и их типов | |
if ( | |
!objA || | |
!objB || | |
(typeof objA !== 'object' && typeof objB !== 'object') | |
) { | |
return false; | |
} | |
const keysA = Object.keys(objA); | |
const keysB = Object.keys(objB); | |
// Если количество ключей не совпадает, объекты разные | |
if (keysA.length !== keysB.length) { | |
return false; | |
} | |
// Проверка каждого ключа и значения на эквивалентность | |
for (let key of keysA) { | |
if (!keysB.includes(key) || objA[key] !== objB[key]) { | |
return false; | |
} | |
} | |
return true; | |
} | |
// Опции для хука формы | |
interface Options<T extends Record<string, any>> { | |
initialValues: T; | |
} | |
// Интерфейс для отслеживания изменений полей формы | |
interface Watcher { | |
watching: Set<string>; | |
isWatchingAll: boolean; | |
onUpdate: () => void; | |
} | |
// Контрол для управления состоянием формы | |
interface Control<T extends Record<string, any>> { | |
formState: T; | |
watchers: Watcher[]; | |
formUpdateHandlers: Array<() => void>; | |
onFormUpdate: (func: () => void) => void; | |
} | |
// Хук для управления формой | |
export const useControl = <T extends Record<string, any>>(control: Control<T>) => { | |
// Локальный стейт для принудительного перерисовывания компонента | |
const [, setRerenderState] = useState(0); | |
// Референс для отслеживания изменений полей формы | |
const watcherRef = useRef<Watcher>({ | |
watching: new Set(), | |
isWatchingAll: false, | |
onUpdate: () => setRerenderState((v) => v + 1), | |
}); | |
const formState = control.formState; | |
const watchers = control.watchers; | |
// При монтировании компонента добавляем watcher в список наблюдателей | |
useEffect(() => { | |
watchers.push(watcherRef.current); | |
}, [watchers]); | |
// Функция для установки значения поля формы | |
const setValue = useCallback( | |
(fieldName: string, value: any) => { | |
// Если значение не изменилось, то ничего не делаем | |
if (formState[fieldName] === value) { | |
return; | |
} | |
(formState as Record<string, any>)[fieldName] = value; | |
// Уведомляем всех наблюдателей о изменении значения | |
watchers.forEach(({ watching, onUpdate, isWatchingAll }) => { | |
if (isWatchingAll) { | |
return onUpdate(); | |
} | |
if (watching.has(fieldName)) { | |
onUpdate(); | |
} | |
}); | |
// Вызываем все обработчики обновления формы | |
control.formUpdateHandlers.forEach((formUpdateHandler) => { | |
formUpdateHandler(); | |
}); | |
}, | |
[formState, watchers, control] | |
); | |
// Функция для отслеживания конкретного поля формы | |
const watch = (fieldName: keyof T): T[keyof T] => { | |
watcherRef.current.watching.add(fieldName as string); | |
return formState[fieldName]; | |
}; | |
// Функция для отслеживания всех полей формы | |
const watchAll = (): T => { | |
watcherRef.current.isWatchingAll = true; | |
return formState; | |
}; | |
// Функция для получения текущего состояния формы | |
const getFormValues = useCallback(() => { | |
return formState; | |
}, [formState]); | |
return { | |
watch, | |
watchAll, | |
setValue, | |
getFormValues, | |
}; | |
}; | |
// Тип функции валидации | |
type ValidationFunc<T> = (formState: T) => Record<string, string>; | |
// Хук для валидации формы | |
export const useValidation = <T extends Record<string, any>>( | |
control: Control<T>, | |
validationFunc: ValidationFunc<T> | |
) => { | |
// Локальные состояния для отображения валидности формы и ошибок | |
const [isValid, setIsValid] = useState(false); | |
const [errors, setErrors] = useState<Record<string, string>>({}); | |
useEffect(() => { | |
// Обработчик обновления формы | |
const handleFormUpdate = () => { | |
// Получаем ошибки валидации | |
const newErrors = validationFunc(control.formState); | |
// Устанавливаем состояние валидности формы | |
setIsValid(!Object.keys(newErrors).length); | |
// Устанавливаем ошибки | |
setErrors((prevErrors) => { | |
if (objectsAreEqual(prevErrors, newErrors)) { | |
return prevErrors; | |
} | |
return newErrors; | |
}); | |
}; | |
// Подписываемся на обновления формы | |
control.onFormUpdate(handleFormUpdate); | |
handleFormUpdate(); | |
}, [control, validationFunc]); | |
return { isValid, errors }; | |
}; | |
// Основной хук для работы с формой | |
export const useForm = <T extends Record<string, any>>(options: Options<T>) => { | |
// Референсы для хранения состояния формы, обработчиков обновлений и наблюдателей | |
const formStateRef = useRef<T>(options.initialValues); | |
const formUpdateHandlersRef = useRef<Array<() => void>>([]); | |
const watchersRef = useRef<Watcher[]>([]); | |
// Мемоизированное значение контрола формы | |
const control = useMemo((): Control<T> => { | |
return { | |
formState: formStateRef.current, | |
watchers: watchersRef.current, | |
formUpdateHandlers: formUpdateHandlersRef.current, | |
onFormUpdate: (func: () => void) => { | |
formUpdateHandlersRef.current.push(func); | |
}, | |
}; | |
}, []); | |
const { watch, watchAll, setValue, getFormValues } = useControl(control); | |
return { | |
watch, | |
watchAll, | |
setValue, | |
getFormValues, | |
control, | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment