index.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. import type { StateModel, StateStore } from "@json-render/core";
  2. import { createStoreAdapter } from "@json-render/core/store-utils";
  3. import type { Store, Action, UnknownAction } from "redux";
  4. export type { StateStore } from "@json-render/core";
  5. /**
  6. * Options for {@link reduxStateStore}.
  7. */
  8. export interface ReduxStateStoreOptions<
  9. S extends StateModel = StateModel,
  10. A extends Action = UnknownAction,
  11. > {
  12. /** The Redux store instance. */
  13. store: Store<S, A>;
  14. /**
  15. * Select the json-render state slice from the Redux state tree.
  16. * Defaults to `(state) => state` (the entire store is the state model).
  17. */
  18. selector?: (state: S) => StateModel;
  19. /**
  20. * Dispatch a state change back to the Redux store.
  21. *
  22. * Called for every `set` / `update` with the full next state model.
  23. * You must dispatch an action that replaces the selected slice.
  24. *
  25. * @example
  26. * ```ts
  27. * dispatch: (nextState, reduxStore) =>
  28. * reduxStore.dispatch(replaceState(nextState))
  29. * ```
  30. */
  31. dispatch: (nextState: StateModel, store: Store<S, A>) => void;
  32. }
  33. /**
  34. * Create a {@link StateStore} backed by a Redux store.
  35. *
  36. * @example
  37. * ```ts
  38. * import { configureStore, createSlice } from "@reduxjs/toolkit";
  39. * import { reduxStateStore } from "@json-render/redux";
  40. *
  41. * const uiSlice = createSlice({
  42. * name: "ui",
  43. * initialState: { count: 0 } as Record<string, unknown>,
  44. * reducers: {
  45. * replaceUiState: (_state, action) => action.payload,
  46. * },
  47. * });
  48. *
  49. * const reduxStore = configureStore({
  50. * reducer: { ui: uiSlice.reducer },
  51. * });
  52. *
  53. * const store = reduxStateStore({
  54. * store: reduxStore,
  55. * selector: (state) => state.ui,
  56. * dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),
  57. * });
  58. *
  59. * <StateProvider store={store}>...</StateProvider>
  60. * ```
  61. */
  62. export function reduxStateStore<
  63. S extends StateModel = StateModel,
  64. A extends Action = UnknownAction,
  65. >(options: ReduxStateStoreOptions<S, A>): StateStore {
  66. const { store, selector = (s: S) => s as StateModel, dispatch } = options;
  67. return createStoreAdapter({
  68. getSnapshot: () => selector(store.getState()),
  69. setSnapshot: (next) => dispatch(next, store),
  70. subscribe(listener) {
  71. let prev = selector(store.getState());
  72. return store.subscribe(() => {
  73. const current = selector(store.getState());
  74. if (current !== prev) {
  75. prev = current;
  76. listener();
  77. // Re-read after listener in case it triggered a synchronous dispatch;
  78. // absorb that change so it doesn't fire a duplicate notification.
  79. prev = selector(store.getState());
  80. }
  81. });
  82. },
  83. });
  84. }