@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
JavaScript
;
/**
* 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