@ehfuse/forma
Version:
Advanced React state management library with individual field subscriptions - supports both forms and general state management with useFormaState
253 lines (250 loc) • 12.1 kB
JavaScript
;
/**
* useGlobalFormaState.ts
*
* Forma - 글로벌 FormaState 관리 훅 / Global FormaState management hook
* 여러 컴포넌트 간 개별 필드 구독 기반 상태 공유를 위한 확장 훅
* Extended hook for sharing state across multiple components with individual field subscriptions
*
* @license MIT License
* @copyright 2025 KIM YOUNG JIN (Kim Young Jin)
* @author KIM YOUNG JIN (ehfuse@gmail.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.useGlobalFormaState = useGlobalFormaState;
const react_1 = require("react");
const useFormaState_1 = require("./useFormaState");
const GlobalFormaContext_1 = require("../contexts/GlobalFormaContext");
const utils_1 = require("../utils");
/**
* 글로벌 FormaState 관리 훅 / Global FormaState management hook
*
* 여러 컴포넌트 간 개별 필드 구독 기반 상태를 공유하기 위한 훅입니다
* Hook for sharing state across multiple components with individual field subscriptions
*
* 데이터 공유에만 집중하며, 각 컴포넌트에서 필요한 필드만 구독하여 최적화된 렌더링을 제공합니다
* Focuses only on data sharing and provides optimized rendering by subscribing only to necessary fields in each component
*
* @template T FormaState 데이터의 타입 / FormaState data type
* @param propsOrStateId 글로벌 FormaState 설정 옵션 또는 stateId 문자열 / Global FormaState configuration options or stateId string
* @returns 글로벌 FormaState 관리 API 객체 / Global FormaState management API object
*
* @example
* ```typescript
* // 글로벌 상태 정의
* interface AppState {
* user: { name: string; email: string };
* settings: { theme: 'light' | 'dark'; notifications: boolean };
* cart: { items: any[]; total: number };
* }
*
* // 컴포넌트 A - 사용자 정보만 구독
* function UserProfile() {
* const state = useGlobalFormaState<AppState>({ stateId: 'app-state' });
* const userName = state.useValue('user.name'); // name 변경시만 리렌더
* const userEmail = state.useValue('user.email'); // email 변경시만 리렌더
*
* return (
* <div>
* <h1>{userName}</h1>
* <p>{userEmail}</p>
* <button onClick={() => state.setValue('user.name', 'New Name')}>
* Update Name
* </button>
* </div>
* );
* }
*
* // 컴포넌트 B - 설정만 구독
* function Settings() {
* const state = useGlobalFormaState<AppState>({ stateId: 'app-state' });
* const theme = state.useValue('settings.theme'); // theme 변경시만 리렌더
* const notifications = state.useValue('settings.notifications'); // notifications 변경시만 리렌더
*
* return (
* <div>
* <select
* value={theme}
* onChange={(e) => state.setValue('settings.theme', e.target.value)}
* >
* <option value="light">Light</option>
* <option value="dark">Dark</option>
* </select>
* </div>
* );
* }
*
* // 컴포넌트 C - 장바구니만 구독
* function Cart() {
* const state = useGlobalFormaState<AppState>({ stateId: 'app-state' });
* const cartItems = state.useValue('cart.items'); // cart.items 변경시만 리렌더
* const cartTotal = state.useValue('cart.total'); // cart.total 변경시만 리렌더
*
* return (
* <div>
* <h2>Cart ({cartItems.length} items)</h2>
* <p>Total: ${cartTotal}</p>
* </div>
* );
* }
* ```
*/
function useGlobalFormaState(propsOrStateId, initialValuesArg) {
// 문자열로 전달된 경우 props 객체로 변환
const props = typeof propsOrStateId === "string"
? { stateId: propsOrStateId, initialValues: initialValuesArg }
: propsOrStateId;
const { stateId, initialValues, autoCleanup = true, actions, watch, } = props;
const context = (0, react_1.useContext)(GlobalFormaContext_1.GlobalFormaContext);
// 참조 등록 상태를 추적하는 ref 추가 + 컴포넌트 고유 ID
const isRegisteredRef = (0, react_1.useRef)(false);
const componentIdRef = (0, react_1.useRef)(undefined);
// 컴포넌트 고유 ID 생성 (한 번만)
if (!componentIdRef.current) {
componentIdRef.current = `${stateId}-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`;
}
// Context가 제대로 설정되지 않았을 때 명확한 에러 표시
// Show clear error when Context is not properly configured
if (!context || !context.getOrCreateStore) {
// 페이지에 에러가 표시되도록 컴포넌트 렌더링을 방해하는 에러를 던짐
// Throw error that prevents component rendering so error shows on page
const errorMessage = `
🚨 GlobalFormaProvider 설정 오류 | Configuration Error
GlobalFormaProvider가 App.tsx에 설정되지 않았습니다!
GlobalFormaProvider is not configured in App.tsx!
해결 방법 | Solution:
1. App.tsx 파일에서 GlobalFormaProvider로 컴포넌트를 감싸주세요.
2. import { GlobalFormaProvider } from '@/forma';
3. <GlobalFormaProvider><YourApp /></GlobalFormaProvider>
Details: GlobalFormaContext must be used within GlobalFormaProvider (stateId: ${stateId})
`.trim();
throw new Error(errorMessage);
}
const { getOrCreateStore, incrementRef, decrementRef, validateAndStoreAutoCleanupSetting, registerActions, getActions, } = context;
// autoCleanup 설정 일관성 검증
validateAndStoreAutoCleanupSetting(stateId, autoCleanup);
// 글로벌 스토어 가져오기 또는 생성 / Get or create global store
const store = getOrCreateStore(stateId);
// actions가 제공되면 글로벌에 동기적으로 등록 / Register actions to global synchronously if provided
if (actions) {
const mergedActions = (0, utils_1.mergeActions)(actions);
if (mergedActions) {
registerActions(stateId, mergedActions);
}
}
// 글로벌 actions 가져오기 / Get global actions
const globalActions = getActions(stateId);
// 로컬 actions가 없으면 글로벌 actions 사용 / Use global actions if local actions are not provided
const effectiveActions = actions || globalActions;
// useFormaState에 초기값과 외부 스토어 전달 (올바른 방식)
const formaState = (0, useFormaState_1.useFormaState)(initialValues || {}, {
_externalStore: store,
actions: effectiveActions,
watch, // watch 옵션 전달
});
// 초기값이 있고 스토어가 비어있다면 초기값 설정 (올바른 방법으로)
// Set initial values if provided and store is empty (using proper method)
(0, react_1.useEffect)(() => {
if (initialValues && Object.keys(store.getValues()).length === 0) {
formaState.setInitialValues(initialValues);
}
}, [stateId, initialValues, store, formaState]);
// 참조 카운팅을 통한 자동 정리 관리 (마운트 시 한 번만 실행)
// Auto cleanup management through reference counting (execute only once on mount)
(0, react_1.useEffect)(() => {
if (!autoCleanup)
return;
// 첫 번째 등록시에만 참조 카운트 증가
const componentId = componentIdRef.current;
incrementRef(stateId, autoCleanup);
isRegisteredRef.current = true;
return () => {
// 컴포넌트 언마운트 시에만 참조 카운트 감소
decrementRef(stateId, autoCleanup);
isRegisteredRef.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 완전히 빈 의존성 배열로 마운트 시 한 번만 실행
// actions를 동적으로 가져오는 getter 생성 / Create getter to dynamically fetch actions
const actionsGetter = (0, react_1.useMemo)(() => {
return new Proxy({}, {
get: (_target, prop) => {
// 항상 최신 글로벌 actions를 가져옴 / Always get the latest global actions
const currentGlobalActions = getActions(stateId);
const currentEffectiveActions = actions || currentGlobalActions || {};
const action = currentEffectiveActions[prop];
if (typeof action === "function") {
// context를 바인딩하여 반환 / Return with context binding
return (...args) => {
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
};
context.actions = actionsGetter;
return action(context, ...args);
};
}
return action;
},
has: (_target, prop) => {
const currentGlobalActions = getActions(stateId);
const currentEffectiveActions = actions || currentGlobalActions || {};
return prop in currentEffectiveActions;
},
ownKeys: (_target) => {
const currentGlobalActions = getActions(stateId);
const currentEffectiveActions = actions || currentGlobalActions || {};
return Reflect.ownKeys(currentEffectiveActions);
},
getOwnPropertyDescriptor: (_target, prop) => {
const currentGlobalActions = getActions(stateId);
const currentEffectiveActions = actions || currentGlobalActions || {};
if (prop in currentEffectiveActions) {
return {
enumerable: true,
configurable: true,
};
}
return undefined;
},
});
}, [stateId, actions, getActions, store]);
return {
...formaState,
actions: actionsGetter, // 동적 actions getter로 교체 / Replace with dynamic actions getter
stateId, // 글로벌 FormaState ID 추가 제공 / Provide additional global FormaState ID
_store: store, // 글로벌 스토어 직접 접근용 (이미 formaState에 있지만 명시적으로 재정의) / Direct access to global store
};
}
//# sourceMappingURL=useGlobalFormaState.js.map