state.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import React, {
  2. createContext,
  3. useContext,
  4. useState,
  5. useCallback,
  6. useMemo,
  7. useEffect,
  8. useRef,
  9. type ReactNode,
  10. } from "react";
  11. import { getByPath, setByPath, type StateModel } from "@json-render/core";
  12. export interface StateContextValue {
  13. state: StateModel;
  14. get: (path: string) => unknown;
  15. set: (path: string, value: unknown) => void;
  16. update: (updates: Record<string, unknown>) => void;
  17. }
  18. const StateContext = createContext<StateContextValue | null>(null);
  19. export interface StateProviderProps {
  20. initialState?: StateModel;
  21. onStateChange?: (path: string, value: unknown) => void;
  22. children: ReactNode;
  23. }
  24. export function StateProvider({
  25. initialState = {},
  26. onStateChange,
  27. children,
  28. }: StateProviderProps) {
  29. const [state, setStateInternal] = useState<StateModel>(initialState);
  30. const stateRef = useRef(state);
  31. stateRef.current = state;
  32. const initialStateJsonRef = useRef<string>(JSON.stringify(initialState));
  33. useEffect(() => {
  34. const newJson = JSON.stringify(initialState);
  35. if (newJson !== initialStateJsonRef.current) {
  36. initialStateJsonRef.current = newJson;
  37. if (initialState && Object.keys(initialState).length > 0) {
  38. setStateInternal((prev) => ({ ...prev, ...initialState }));
  39. }
  40. }
  41. }, [initialState]);
  42. const get = useCallback(
  43. (path: string) => getByPath(stateRef.current, path),
  44. [],
  45. );
  46. const set = useCallback(
  47. (path: string, value: unknown) => {
  48. setStateInternal((prev) => {
  49. const next = { ...prev };
  50. setByPath(next, path, value);
  51. return next;
  52. });
  53. onStateChange?.(path, value);
  54. },
  55. [onStateChange],
  56. );
  57. const update = useCallback(
  58. (updates: Record<string, unknown>) => {
  59. const entries = Object.entries(updates);
  60. setStateInternal((prev) => {
  61. const next = { ...prev };
  62. for (const [path, value] of entries) {
  63. setByPath(next, path, value);
  64. }
  65. return next;
  66. });
  67. for (const [path, value] of entries) {
  68. onStateChange?.(path, value);
  69. }
  70. },
  71. [onStateChange],
  72. );
  73. const value = useMemo<StateContextValue>(
  74. () => ({ state, get, set, update }),
  75. [state, get, set, update],
  76. );
  77. return (
  78. <StateContext.Provider value={value}>{children}</StateContext.Provider>
  79. );
  80. }
  81. export function useStateStore(): StateContextValue {
  82. const ctx = useContext(StateContext);
  83. if (!ctx) {
  84. throw new Error("useStateStore must be used within a StateProvider");
  85. }
  86. return ctx;
  87. }
  88. export function useStateValue<T>(path: string): T | undefined {
  89. const { state } = useStateStore();
  90. return getByPath(state, path) as T | undefined;
  91. }
  92. export function useStateBinding<T>(
  93. path: string,
  94. ): [T | undefined, (value: T) => void] {
  95. const { state, set } = useStateStore();
  96. const value = getByPath(state, path) as T | undefined;
  97. const setValue = useCallback(
  98. (newValue: T) => set(path, newValue),
  99. [path, set],
  100. );
  101. return [value, setValue];
  102. }