@pixotope/react-context-store 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/store.tsx CHANGED
@@ -10,11 +10,21 @@ import { shallowEqual } from "@pixotope/utils/comparison";
10
10
 
11
11
  const LIB_NAME = "@pixotope/react-context-store";
12
12
 
13
+ /**
14
+ * A function used to compare two versions of selected state, to decide
15
+ * whether subscribers should be notified of a change.
16
+ * @param first - The previously selected state.
17
+ * @param second - The newly selected state.
18
+ * @returns `true` if the two values should be treated as equal (no update triggered), `false` otherwise.
19
+ */
13
20
  export type SelectorOptions<Selected> = (
14
21
  first: Selected,
15
22
  second: Selected
16
23
  ) => boolean;
17
24
 
25
+ /**
26
+ * Options that configure the behavior of a store created with {@link createContextStore}.
27
+ */
18
28
  export type ContextOptions = {
19
29
  /**
20
30
  * If true, the store state will be preserved across mounts and un-mounts
@@ -29,8 +39,15 @@ export type ContextOptions = {
29
39
  compare?: SelectorOptions<unknown>;
30
40
  };
31
41
 
42
+ /**
43
+ * The value accepted by a store's `set` function: either the next state
44
+ * directly, or an updater function that receives the previous state and
45
+ * returns the next state.
46
+ */
32
47
  export type SetterArgs<Store> = Store | ((prev: Store) => Store);
33
- type ExtractActionKeys<T> = {
48
+
49
+ /** Maps an actions map (as passed to {@link createContextStore}) to the shape of the object returned by `useActions`. */
50
+ export type ExtractActionKeys<T> = {
34
51
  [K in keyof T]: T[K] extends (
35
52
  stateProps: never,
36
53
  action: infer A
@@ -43,47 +60,105 @@ type ExtractActionKeys<T> = {
43
60
  : never;
44
61
  };
45
62
 
46
- type Prettify<T> = {
63
+ /** Flattens an intersection/mapped type into a single object type, for cleaner hover tooltips. */
64
+ export type Prettify<T> = {
47
65
  [K in keyof T]: T[K];
48
66
  } & {};
49
67
 
68
+ /**
69
+ * The action argument passed to an action handler, wrapping its payload.
70
+ * Use this as the type of an action's second parameter to give the
71
+ * corresponding {@link createContextStore} action a typed payload, which is
72
+ * then reflected in the function returned by `useActions`.
73
+ * @typeParam Payload - The type of the action's payload. Defaults to `any`.
74
+ * @example
75
+ * ```tsx
76
+ * const store = createContextStore(initialState, {
77
+ * incrementBy: ({ set, get }, action: ActionablePayload<number>) => {
78
+ * set((prev) => ({ ...prev, count: get().count + action.payload }));
79
+ * },
80
+ * });
81
+ * ```
82
+ */
50
83
  export type ActionablePayload<Payload = any> = {
51
84
  payload: Payload;
52
85
  };
53
86
 
54
- type StateActionProps<Store> = {
87
+ /** The state accessors passed as the first argument to every action handler. */
88
+ export type StateActionProps<Store> = {
89
+ /** Updates the store state. */
55
90
  set: (value: SetterArgs<Store>) => void;
91
+ /** Returns the current store state. */
56
92
  get: () => Store;
57
93
  };
58
94
 
59
- type Actions<Store> = {
95
+ /**
96
+ * A map of action names to action handlers, as passed to {@link createContextStore}.
97
+ * Each handler receives {@link StateActionProps} for reading/writing the
98
+ * store, plus an {@link ActionablePayload} carrying any payload passed by the caller.
99
+ */
100
+ export type Actions<Store> = {
60
101
  [key: string]: (
61
102
  stateProps: StateActionProps<Store>,
62
103
  action: ActionablePayload
63
104
  ) => void | Promise<void>;
64
105
  };
65
106
 
66
- type UseStoreReturnType<Store, SelectorOutput> = {
107
+ /** The object returned by a store's `useStore` hook. */
108
+ export type UseStoreReturnType<Store, SelectorOutput> = {
109
+ /** The currently selected state. Reading this subscribes the component to updates. */
67
110
  state: SelectorOutput;
111
+ /** Returns the currently selected state. Equivalent to reading `state`. */
68
112
  get: () => SelectorOutput;
113
+ /** Updates the store state. */
69
114
  set: (value: SetterArgs<Store>) => void;
115
+ /** Returns the entire store state, not just the selected slice. */
70
116
  selector: () => Store;
71
117
  };
72
118
 
73
- type ContextReturnType<Store, A extends Actions<Store>> = {
119
+ /**
120
+ * The object returned by {@link createContextStore}: the `Provider`
121
+ * component plus the hooks and functions used to read, update and
122
+ * subscribe to the store.
123
+ */
124
+ export type ContextReturnType<Store, A extends Actions<Store>> = {
125
+ /** Wraps the part of the component tree that should have access to the store. */
74
126
  Provider: React.FC<React.PropsWithChildren>;
127
+ /**
128
+ * Reads (a selection of) the store state from the nearest `Provider`,
129
+ * re-rendering the component whenever the selected state changes.
130
+ * @param selector - Selects the part of the store the component needs. Defaults to the entire store.
131
+ * @param options - A function used to compare the previous and next selected state. Defaults to the store's `compare` option, or shallow equality.
132
+ * @returns The selected state, plus functions to read and update the store.
133
+ */
75
134
  useStore: <SelectorOutput = Store>(
76
135
  selector?: (store: Store) => SelectorOutput,
77
136
  options?: SelectorOptions<SelectorOutput>
78
137
  ) => UseStoreReturnType<Store, SelectorOutput>;
138
+ /**
139
+ * Returns the actions passed to {@link createContextStore}, bound to the
140
+ * current store. Actions declared with an {@link ActionablePayload}
141
+ * parameter take a single payload argument; other actions take none.
142
+ */
79
143
  useActions: () => Prettify<ExtractActionKeys<A>>;
144
+ /**
145
+ * Returns a function to update the store state from the nearest
146
+ * `Provider`, without subscribing the component to store updates.
147
+ */
80
148
  useSetStore: () => (value: SetterArgs<Store>) => void;
81
- useStoreSelector: () => () => Store;
149
+ /**
150
+ * Subscribes to store updates from outside the React component tree.
151
+ * @param selector - Selects the part of the store to watch.
152
+ * @param callback - Called with the selected state whenever it changes.
153
+ * @param options - A function used to compare the previous and next selected state. Defaults to shallow equality.
154
+ * @returns A function to remove the subscription.
155
+ */
82
156
  subscribe: <SelectorOutput = Store>(
83
157
  selector: (store: Store) => SelectorOutput,
84
158
  callback: (state: SelectorOutput) => void,
85
159
  options?: SelectorOptions<SelectorOutput>
86
160
  ) => () => void;
161
+ /** Removes a subscription created with `subscribe`. */
87
162
  unsubscribe: (callback: () => void) => void;
88
163
  };
89
164
 
@@ -91,6 +166,44 @@ function isFunction(value: any): value is (prev: any) => any {
91
166
  return typeof value === "function";
92
167
  }
93
168
 
169
+ /**
170
+ * Creates a React context-based store: a `Provider` component plus hooks to
171
+ * read, update and subscribe to its state.
172
+ *
173
+ * Each call creates an independent store with its own React context, so the
174
+ * `Provider` and hooks returned by one call must always be used together.
175
+ * @typeParam Store - The shape of the store state.
176
+ * @typeParam A - The map of named actions available to update the store.
177
+ * @param args - Positional arguments: the `initialState` the store is initialized with, an
178
+ * optional `actions` map of named action handlers used to update the store (see
179
+ * {@link ActionablePayload} for actions that take a payload), and optional `options` to
180
+ * customize the store's behavior.
181
+ * @returns An object with a `Provider` component and hooks to read, update and subscribe to the store's state. See {@link ContextReturnType}.
182
+ * @example
183
+ * ```tsx
184
+ * const store = createContextStore(
185
+ * { count: 0 },
186
+ * {
187
+ * increment: ({ set, get }) => set({ ...get(), count: get().count + 1 }),
188
+ * }
189
+ * );
190
+ *
191
+ * function Counter() {
192
+ * const { state: count } = store.useStore((state) => state.count);
193
+ * const { increment } = store.useActions();
194
+ *
195
+ * return <button onClick={() => increment()}>{count}</button>;
196
+ * }
197
+ *
198
+ * function App() {
199
+ * return (
200
+ * <store.Provider>
201
+ * <Counter />
202
+ * </store.Provider>
203
+ * );
204
+ * }
205
+ * ```
206
+ */
94
207
  export function createContextStore<Store, A extends Actions<Store> = Actions<Store>>(
95
208
  ...args: Extract<A, { payload: A }> extends { payload: infer Payload }
96
209
  ? [initialState: Store, actions?: A, options?: ContextOptions]
@@ -150,8 +263,12 @@ export function createContextStore<Store, A extends Actions<Store> = Actions<Sto
150
263
  const StoreContext = createContext<UseStoreDataReturnType | null>(null);
151
264
 
152
265
  /**
153
- * This is the provider that will be used to wrap the react component tree
154
- * to provide the store to all the components in the tree.
266
+ * Wraps the part of the component tree that should have access to the
267
+ * store. Must wrap any component using `useStore`, `useActions` or `useSetStore`.
268
+ * @remarks
269
+ * If the store was created with `{ global: true }`, state changes made
270
+ * while a `Provider` is mounted are preserved and restored the next time
271
+ * a `Provider` for this store mounts.
155
272
  */
156
273
  function Provider({ children }: React.PropsWithChildren) {
157
274
  return (
@@ -171,6 +288,13 @@ export function createContextStore<Store, A extends Actions<Store> = Actions<Sto
171
288
  */
172
289
  const observers = new Set<(state: Store) => void>();
173
290
 
291
+ /**
292
+ * Subscribes to store updates from outside the React component tree.
293
+ * @param selector - Selects the part of the store to watch. Defaults to the whole store.
294
+ * @param callback - Called with the selected state whenever it changes.
295
+ * @param compare - A function used to compare the previous and next selected state. Defaults to shallow equality.
296
+ * @returns A function to remove the subscription.
297
+ */
174
298
  function subscribeExternal<SelectorOutput = Store>(
175
299
  selector: (store: Store) => SelectorOutput = (store) =>
176
300
  store as unknown as SelectorOutput,
@@ -220,20 +344,24 @@ export function createContextStore<Store, A extends Actions<Store> = Actions<Sto
220
344
  } as const;
221
345
 
222
346
  /**
223
- * This is the hook that will be used to access the store from any component
224
- * in the react component tree.
225
- * @param selector A function that will be used to select the part of the store
226
- * that is needed by the component.
227
- * @param options Options to customize the behavior of the hook.
228
- * @returns An object with the selected state, a function to update the store
229
- * and a function to get the entire store.
347
+ * Reads (a selection of) the store state from the nearest `Provider`,
348
+ * re-rendering the component whenever the selected state changes.
349
+ * @param selector - A function that selects the part of the store that is
350
+ * needed by the component. Defaults to the entire store.
351
+ * @param compare - A function used to compare the previous and next
352
+ * selected state; the component only re-renders when it returns `false`.
353
+ * Defaults to the store's `compare` option (see {@link ContextOptions}),
354
+ * or `shallowEqual` from `@pixotope/utils/comparison`.
355
+ * @returns An object containing the selected `state`, a `set` function to
356
+ * update the store, a `get` function equivalent to reading `state`, and a
357
+ * `selector` function to read the entire store without selecting.
230
358
  * @example
231
359
  * ```tsx
232
- * const { get, set } = useStore(store => store.user);
233
- * const { get, set } = useStore(store => store.user, { deepEqual: false });
234
- * const { get, set } = useStore(store => store.user, {
235
- * compare: (first, second) => first.id === second.id
236
- * });
360
+ * const { state, set } = store.useStore((store) => store.user);
361
+ * const { state, set } = store.useStore(
362
+ * (store) => store.user,
363
+ * (first, second) => first.id === second.id
364
+ * );
237
365
  * ```
238
366
  */
239
367
  function useStore<SelectorOutput = Store>(
@@ -275,6 +403,28 @@ export function createContextStore<Store, A extends Actions<Store> = Actions<Sto
275
403
  };
276
404
  }
277
405
 
406
+ /**
407
+ * Returns the actions passed to {@link createContextStore}, bound to the
408
+ * current store from the nearest `Provider`. Calling an action does not
409
+ * itself cause a re-render; only the state changes it makes (via `set`)
410
+ * do, and only for components subscribed to the affected state.
411
+ * @returns An object with one bound function per action. Actions declared
412
+ * with an {@link ActionablePayload} parameter take a single payload
413
+ * argument; other actions take no arguments.
414
+ * @example
415
+ * ```tsx
416
+ * const store = createContextStore(initialState, {
417
+ * increment: ({ set, get }) => set({ ...get(), count: get().count + 1 }),
418
+ * incrementBy: ({ set, get }, action: ActionablePayload<number>) =>
419
+ * set({ ...get(), count: get().count + action.payload }),
420
+ * });
421
+ *
422
+ * function Counter() {
423
+ * const { incrementBy } = store.useActions();
424
+ * return <button onClick={() => incrementBy(5)}>+5</button>;
425
+ * }
426
+ * ```
427
+ */
278
428
  function useActions() {
279
429
  const store = useContext(StoreContext);
280
430
 
@@ -314,15 +464,24 @@ export function createContextStore<Store, A extends Actions<Store> = Actions<Sto
314
464
  return actionProxy as unknown as ExtractActionKeys<typeof actions>;
315
465
  }
316
466
 
467
+ /**
468
+ * Returns a function to update the store state from the nearest
469
+ * `Provider`, without subscribing the component to store updates.
470
+ * @returns A function that sets the store state, accepting either the
471
+ * next state directly or an updater function that receives the previous state.
472
+ * @example
473
+ * ```tsx
474
+ * const setStore = store.useSetStore();
475
+ * setStore((prev) => ({ ...prev, count: prev.count + 1 }));
476
+ * ```
477
+ */
317
478
  const useSetStore = () => useStore(() => false).set;
318
- const useStoreSelector = () => useStore(() => false).selector;
319
479
 
320
480
  return {
321
481
  Provider,
322
482
  useStore,
323
483
  useSetStore,
324
484
  useActions,
325
- useStoreSelector,
326
485
  subscribe: observable.subscribe,
327
486
  unsubscribe: observable.unsubscribe,
328
487
  };
package/typedoc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "entryPoints": ["src/index.ts"]
3
+ }