Skip to content

Instantly share code, notes, and snippets.

@avin
Last active September 14, 2023 19:35
Show Gist options
  • Save avin/6b358dd73dbd7b178790b8ec9fee536c to your computer and use it in GitHub Desktop.
Save avin/6b358dd73dbd7b178790b8ec9fee536c to your computer and use it in GitHub Desktop.
Slim form control
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;
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