UNPKG

@ehfuse/forma

Version:

Advanced React state management library with individual field subscriptions - supports both forms and general state management with useFormaState

256 lines 10.7 kB
"use strict"; /** * useFormaState.ts * * Advanced state management hook with individual field subscriptions * Optimized for arrays, objects, and complex nested data structures * * @author KIM YOUNG JIN (ehfuse@gmail.com) * @license MIT License */ Object.defineProperty(exports, "__esModule", { value: true }); exports.useFieldSubscription = useFieldSubscription; exports.useFormaState = useFormaState; const react_1 = require("react"); const FieldStore_1 = require("../core/FieldStore"); const utils_1 = require("../utils"); /** * Individual field subscription hook for useFormaState * useFormaState를 위한 개별 필드 구독 훅 * * useSyncExternalStore를 사용하여 React 18의 동시성 모드를 지원하고 * 구독 등록과 값 읽기를 동기적으로 처리하여 타이밍 이슈를 방지합니다. * * @param store FieldStore 인스턴스 * @param fieldName 구독할 필드 이름 (dot notation 지원) * @returns 필드의 현재 값 */ function useFieldValue(store, fieldName) { // useSyncExternalStore를 사용하여 동기적으로 구독 등록 // 이렇게 하면 컴포넌트 렌더링 중에 구독이 등록되어 // setValues() 호출 시점과 상관없이 항상 알림을 받을 수 있습니다. const value = (0, react_1.useSyncExternalStore)((0, react_1.useCallback)((onStoreChange) => { // 구독 등록 (동기적으로 실행됨) return store.subscribe(fieldName, onStoreChange); }, [store, fieldName]), (0, react_1.useCallback)(() => { // 현재 값 읽기 (동기적으로 실행됨) return store.getValue(fieldName); }, [store, fieldName]), (0, react_1.useCallback)(() => { // 서버 사이드 렌더링용 초기값 return store.getValue(fieldName); }, [store, fieldName])); return value; } /** * Hook for subscribing to a specific field in a FieldStore * FieldStore의 특정 필드를 구독하기 위한 Hook * * @param store FieldStore 인스턴스 * @param path 필드 경로 (dot notation) * @returns 필드의 현재 값 */ function useFieldSubscription(store, path) { return useFieldValue(store, path); } function useFormaState(initialValues = {}, options = {}) { const { onChange, deepEquals = false, _externalStore, actions: actionsDefinition, } = options; // 초기값 안정화: 첫 번째 렌더링에서만 초기값을 고정 // Stabilize initial values: fix initial values only on first render const stableInitialValues = (0, react_1.useRef)(null); if (!stableInitialValues.current) { stableInitialValues.current = initialValues; } // Create or use external field store instance (persists across renders) // 필드 스토어 인스턴스 생성 또는 외부 스토어 사용 (렌더링 간 유지) const storeRef = (0, react_1.useRef)(null); if (_externalStore) { // 외부 스토어는 한번만 설정 (이미 설정되어 있으면 변경하지 않음) // Set external store only once (don't change if already set) if (!storeRef.current) { storeRef.current = _externalStore; // 외부 스토어 사용 시 초기값이 비어있으면 설정 const currentValues = _externalStore.getValues(); if (Object.keys(currentValues).length === 0 && Object.keys(initialValues).length > 0) { // ⭐ initialValues 먼저 설정 (reset()이 참조함) _externalStore.setInitialValues(initialValues); // 그 다음 값 설정 Object.keys(initialValues).forEach((key) => { _externalStore.setValue(key, initialValues[key]); }); } } } else if (!storeRef.current) { storeRef.current = new FieldStore_1.FieldStore(stableInitialValues.current); // Set up global change listener if provided // 글로벌 변경 리스너 설정 (제공된 경우) if (onChange) { storeRef.current.subscribeGlobal(() => { onChange(storeRef.current.getValues()); }); } } const store = storeRef.current; // Subscribe to a specific field value with dot notation // dot notation으로 특정 필드 값 구독 // NOTE: This is NOT a useCallback - hooks cannot be wrapped in useCallback const useValue = (path) => { return useFieldValue(store, path); }; // Set a specific field value with dot notation // dot notation으로 특정 필드 값 설정 const setValue = (0, react_1.useCallback)((path, value) => { store.setValue(path, value); }, [store] // store 의존성 추가 ); // Get all current values (non-reactive) // 모든 현재 값 가져오기 (반응형 아님) const getValues = (0, react_1.useCallback)(() => { return store.getValues(); }, [store]); // store 의존성 추가 // Set all values at once // 모든 값을 한 번에 설정 const setValues = (0, react_1.useCallback)((values) => { const currentValues = store.getValues(); const newValues = { ...currentValues, ...values }; store.setValues(newValues); }, [store] // store 의존성 추가 ); // Reset to initial values // 초기값으로 재설정 const reset = (0, react_1.useCallback)(() => { store.reset(); }, [store]); // store 의존성 추가 // Set new initial values (for dynamic initialization) // 새 초기값 설정 (동적 초기화용) const setInitialValues = (0, react_1.useCallback)((newInitialValues) => { stableInitialValues.current = newInitialValues; store.setInitialValues(newInitialValues); }, [store]); // store 의존성 추가 // Handle standard input change events // 표준 입력 변경 이벤트 처리 const handleChange = (0, react_1.useCallback)((event) => { const target = event.target; if (!target || !target.name) { (0, utils_1.devWarn)('useFormaState.handleChange: input element must have a "name" attribute'); return; } const { name, type, value, checked } = target; let processedValue = value; // DatePicker 처리 (Dayjs 객체) / DatePicker handling (Dayjs object) if (value && typeof value === "object" && value.format) { processedValue = value.format("YYYY-MM-DD"); } // 체크박스 처리 / Checkbox handling else if (type === "checkbox") { processedValue = checked; } // 숫자 타입 처리 / Number type handling else if (type === "number") { processedValue = Number(value); } // null 값 처리 / Null value handling else if (value === null) { processedValue = undefined; } setValue(name, processedValue); }, [setValue]); // Bind actions to context if provided // actions가 제공된 경우 context에 바인딩 const boundActions = (0, react_1.useMemo)(() => { if (!actionsDefinition) return {}; // 배열이면 병합, 객체면 그대로 사용 const mergedActions = (0, utils_1.mergeActions)(actionsDefinition); if (!mergedActions) return {}; const context = { values: store.getValues(), getValue: (field) => store.getValue(field), setValue: (field, value) => store.setValue(field, value), setValues: (values) => { const currentValues = store.getValues(); const newValues = { ...currentValues, ...values }; store.setValues(newValues); }, reset: () => store.reset(), actions: {}, // Will be filled after binding }; const bound = {}; for (const [key, action] of Object.entries(mergedActions)) { bound[key] = (...args) => { // Update context.values with latest state context.values = store.getValues(); return action(context, ...args); }; } // Fill context.actions with bound actions context.actions = bound; return bound; }, [actionsDefinition, store]); // Register watch callbacks // watch 콜백 등록 (0, react_1.useEffect)(() => { if (!options.watch) return; const unsubscribers = []; for (const [path, callback] of Object.entries(options.watch)) { const unsubscribe = store.watch(path, (value, prevValue) => { const context = { values: store.getValues(), getValue: (field) => store.getValue(field), setValue: (field, value) => store.setValue(field, value), setValues: (values) => { const currentValues = store.getValues(); const newValues = { ...currentValues, ...values }; store.setValues(newValues); }, reset: () => store.reset(), actions: boundActions, }; callback(context, value, prevValue); }); unsubscribers.push(unsubscribe); } return () => { unsubscribers.forEach((unsub) => unsub()); }; }, [options.watch, store, boundActions]); return { useValue, setValue, getValues, setValues, setBatch: (0, react_1.useCallback)((updates) => { store.setBatch(updates); }, [store] // store 의존성 추가 ), reset, setInitialValues, handleChange, hasField: (0, react_1.useCallback)((path) => { return store.hasField(path); }, [store] // store 의존성 추가 ), removeField: (0, react_1.useCallback)((path) => { store.removeField(path); }, [store] // store 의존성 추가 ), getValue: (0, react_1.useCallback)((path) => { return store.getValue(path); }, [store] // store 의존성 추가 ), subscribe: (0, react_1.useCallback)((callback) => { return store.subscribeToAll(callback); }, [store] // store 의존성 추가 ), refreshFields: (0, react_1.useCallback)((prefix) => { store.refreshFields(prefix); }, [store] // store 의존성 추가 ), actions: boundActions, _store: store, }; } //# sourceMappingURL=useFormaState.js.map