@apogeelabs/beacon-react-utils 0.2.0 → 1.0.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/README.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Beacon/React Utilities
2
2
 
3
- This lib is experimental. We're still poking holes in it to see what we like and hate about it.
3
+ This package provides some basic utilities to help bridge beacon stores with react hooks.
4
4
 
5
- If it survives our cynical skepticism, then docs will appear here. Until then, stay thirsty my friends.
5
+ Right now it contains one helper hook: `useStoreWatcher`.
6
+
7
+ ## useStoreWatcher
8
+
9
+ Normally, you would use the `observer` higher order function from mobx-react-lite to make components opt into mobx state changes. However, you can't use `observer` on a hook. If you have a situation where a stand-alone hook needs to react to changes in a beacon store, you can use `useStoreWatcher`.
10
+
11
+ A basic example:
12
+
13
+ ```typescript
14
+ useStoreWatcher<
15
+ CharitySelectionStoreState,
16
+ CharitySelectionStoreDerivedState,
17
+ CharitySelectionStoreActions,
18
+ Partial<CharitySearchArgs>
19
+ >(
20
+ // first arg is the store you want to watch
21
+ charitySelectionStore,
22
+ // second arg is the selector that returns the state you want to watch for changes
23
+ state => {
24
+ return state.charitySearchParams;
25
+ },
26
+ // third arg is the "onChange" handler that should execute when the selector state changes
27
+ params => {
28
+ // Only update if the params actually changed
29
+ if (!isEqual(params, prevParamsRef.current)) {
30
+ prevParamsRef.current = params;
31
+ debouncedSetSearchParams(params);
32
+ }
33
+ },
34
+ // fourth arg is a `fireImmediately` boolean, indicating if the onChange call should execute
35
+ // immediately when the hook is called, or if it should wait until changes are observed later
36
+ true
37
+ );
38
+ ```
39
+
40
+ There are definitely other ways to go about this than to use this hook. For example, the component, if wrapped with `observer` can pass store state into a hook, and the hook can watch it via useEffect internally. (In fact, that's the 80% use case for these needs - this hook is just useful if you need to avoid prop drilling, or if it makes the particular intent more clear.)
package/dist/index.js CHANGED
@@ -10,14 +10,16 @@ var ___default = /*#__PURE__*/_interopDefault(_);
10
10
 
11
11
  // src/useStoreWatcher.ts
12
12
  function useStoreWatcher(store, selector, onChange, fireImmediately = false) {
13
+ const stableSelector = react.useEffectEvent(selector);
14
+ const stableOnChange = react.useEffectEvent(onChange);
13
15
  react.useEffect(() => {
14
16
  const disposer = mobx.reaction(
15
17
  () => {
16
- return mobx.toJS(selector(store));
18
+ return mobx.toJS(stableSelector(store));
17
19
  },
18
20
  (value, previousValue) => {
19
21
  if (!previousValue || !___default.default.isEqual(value, previousValue)) {
20
- onChange(value);
22
+ stableOnChange(value);
21
23
  }
22
24
  },
23
25
  { fireImmediately }
@@ -28,7 +30,7 @@ function useStoreWatcher(store, selector, onChange, fireImmediately = false) {
28
30
  return () => {
29
31
  disposer();
30
32
  };
31
- }, [store, selector, onChange, fireImmediately]);
33
+ }, [store, fireImmediately]);
32
34
  }
33
35
 
34
36
  exports.useStoreWatcher = useStoreWatcher;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/useStoreWatcher.ts"],"names":[],"mappings":";AACA,OAAO,OAAO;AACd,SAAS,UAAU,YAAY;AAC/B,SAAS,iBAAiB;AAenB,SAAS,gBAMZ,OACA,UACA,UACA,kBAA2B,OACvB;AACJ,YAAU,MAAM;AAEZ,UAAM,WAAW;AAAA,MACb,MAAM;AAEF,eAAO,KAAK,SAAS,KAAK,CAAC;AAAA,MAC/B;AAAA,MACA,CAAC,OAAO,kBAAkB;AAEtB,YAAI,CAAC,iBAAiB,CAAC,EAAE,QAAQ,OAAO,aAAa,GAAG;AACpD,mBAAS,KAAK;AAAA,QAClB;AAAA,MACJ;AAAA,MACA,EAAE,gBAAgB;AAAA,IACtB;AAGA,UAAM,gBAAgB,MAAM;AACxB,eAAS;AAAA,IACb,CAAC;AAGD,WAAO,MAAM;AACT,eAAS;AAAA,IACb;AAAA,EACJ,GAAG,CAAC,OAAO,UAAU,UAAU,eAAe,CAAC;AACnD","sourcesContent":["import type { BeaconActions, BeaconDerived, BeaconState, Store } from \"@apogeelabs/beacon\";\nimport _ from \"lodash\";\nimport { reaction, toJS } from \"mobx\";\nimport { useEffect } from \"react\";\n\n/**\n * A custom React hook to watch for state changes in a beacon store.\n *\n * @template TState - The type of the store's state, extending BeaconState.\n * @template TDerived - The type of the store's derived values, extending BeaconDerived<TState>. If you don't have derived values, use EmptyDerived<TState>.\n * @template TActions - The type of the store's actions, extending BeaconActions<TState>. If you don't have actions, use EmptyActions.\n * @template T - The type returned by the selector function, inferred from the selector.\n *\n * @param {Store<TState, TDerived, TActions>} store - The Beacon store instance.\n * @param {(store: Store<TState, TDerived, TActions>) => T} selector - A function that extracts the part of the store to watch.\n * @param {(value: T) => void} onChange - A callback invoked with the new value when the selected state changes.\n * @param {boolean} [fireImmediately=false] - Whether to invoke onChange with the initial value.\n */\nexport function useStoreWatcher<\n TState extends BeaconState,\n TDerived extends BeaconDerived<TState>,\n TActions extends BeaconActions<TState>,\n T,\n>(\n store: Store<TState, TDerived, TActions>,\n selector: (store: Store<TState, TDerived, TActions>) => T,\n onChange: (value: T) => void,\n fireImmediately: boolean = false\n): void {\n useEffect(() => {\n // Set up a MobX reaction to watch the selected state\n const disposer = reaction(\n () => {\n // Convert to plain JS to strip MobX observable properties\n return toJS(selector(store));\n },\n (value, previousValue) => {\n // Only trigger if the value actually changed using deep comparison\n if (!previousValue || !_.isEqual(value, previousValue)) {\n onChange(value);\n }\n },\n { fireImmediately }\n );\n\n // Register the disposer with the store for cleanup\n store.registerCleanup(() => {\n disposer();\n });\n\n // Cleanup when the component unmounts or dependencies change\n return () => {\n disposer();\n };\n }, [store, selector, onChange, fireImmediately]);\n}\n\nexport default useStoreWatcher;\n"]}
1
+ {"version":3,"sources":["../src/useStoreWatcher.ts"],"names":[],"mappings":";AACA,OAAO,OAAO;AACd,SAAS,UAAU,YAAY;AAC/B,SAAS,WAAW,sBAAsB;AAenC,SAAS,gBAMZ,OACA,UACA,UACA,kBAA2B,OACvB;AAGJ,QAAM,iBAAiB,eAAe,QAAQ;AAC9C,QAAM,iBAAiB,eAAe,QAAQ;AAE9C,YAAU,MAAM;AAEZ,UAAM,WAAW;AAAA,MACb,MAAM;AAEF,eAAO,KAAK,eAAe,KAAK,CAAC;AAAA,MACrC;AAAA,MACA,CAAC,OAAO,kBAAkB;AAEtB,YAAI,CAAC,iBAAiB,CAAC,EAAE,QAAQ,OAAO,aAAa,GAAG;AACpD,yBAAe,KAAK;AAAA,QACxB;AAAA,MACJ;AAAA,MACA,EAAE,gBAAgB;AAAA,IACtB;AAGA,UAAM,gBAAgB,MAAM;AACxB,eAAS;AAAA,IACb,CAAC;AAGD,WAAO,MAAM;AACT,eAAS;AAAA,IACb;AAAA,EACJ,GAAG,CAAC,OAAO,eAAe,CAAC;AAC/B","sourcesContent":["import type { BeaconActions, BeaconDerived, BeaconState, Store } from \"@apogeelabs/beacon\";\nimport _ from \"lodash\";\nimport { reaction, toJS } from \"mobx\";\nimport { useEffect, useEffectEvent } from \"react\";\n\n/**\n * A custom React hook to watch for state changes in a beacon store.\n *\n * @template TState - The type of the store's state, extending BeaconState.\n * @template TDerived - The type of the store's derived values, extending BeaconDerived<TState>. If you don't have derived values, use EmptyDerived<TState>.\n * @template TActions - The type of the store's actions, extending BeaconActions<TState>. If you don't have actions, use EmptyActions.\n * @template T - The type returned by the selector function, inferred from the selector.\n *\n * @param {Store<TState, TDerived, TActions>} store - The Beacon store instance.\n * @param {(store: Store<TState, TDerived, TActions>) => T} selector - A function that extracts the part of the store to watch.\n * @param {(value: T) => void} onChange - A callback invoked with the new value when the selected state changes.\n * @param {boolean} [fireImmediately=false] - Whether to invoke onChange with the initial value.\n */\nexport function useStoreWatcher<\n TState extends BeaconState,\n TDerived extends BeaconDerived<TState>,\n TActions extends BeaconActions<TState>,\n T,\n>(\n store: Store<TState, TDerived, TActions>,\n selector: (store: Store<TState, TDerived, TActions>) => T,\n onChange: (value: T) => void,\n fireImmediately: boolean = false\n): void {\n // Use useEffectEvent to wrap callbacks so they don't need to be in the dependency array\n // This prevents unnecessary re-runs when consumers pass inline functions\n const stableSelector = useEffectEvent(selector);\n const stableOnChange = useEffectEvent(onChange);\n\n useEffect(() => {\n // Set up a MobX reaction to watch the selected state\n const disposer = reaction(\n () => {\n // Convert to plain JS to strip MobX observable properties\n return toJS(stableSelector(store));\n },\n (value, previousValue) => {\n // Only trigger if the value actually changed using deep comparison\n if (!previousValue || !_.isEqual(value, previousValue)) {\n stableOnChange(value);\n }\n },\n { fireImmediately }\n );\n\n // Register the disposer with the store for cleanup\n store.registerCleanup(() => {\n disposer();\n });\n\n // Cleanup when the component unmounts or dependencies change\n return () => {\n disposer();\n };\n }, [store, fireImmediately]);\n}\n\nexport default useStoreWatcher;\n"]}
package/dist/index.mjs CHANGED
@@ -1,17 +1,19 @@
1
1
  import _ from 'lodash';
2
2
  import { reaction, toJS } from 'mobx';
3
- import { useEffect } from 'react';
3
+ import { useEffectEvent, useEffect } from 'react';
4
4
 
5
5
  // src/useStoreWatcher.ts
6
6
  function useStoreWatcher(store, selector, onChange, fireImmediately = false) {
7
+ const stableSelector = useEffectEvent(selector);
8
+ const stableOnChange = useEffectEvent(onChange);
7
9
  useEffect(() => {
8
10
  const disposer = reaction(
9
11
  () => {
10
- return toJS(selector(store));
12
+ return toJS(stableSelector(store));
11
13
  },
12
14
  (value, previousValue) => {
13
15
  if (!previousValue || !_.isEqual(value, previousValue)) {
14
- onChange(value);
16
+ stableOnChange(value);
15
17
  }
16
18
  },
17
19
  { fireImmediately }
@@ -22,7 +24,7 @@ function useStoreWatcher(store, selector, onChange, fireImmediately = false) {
22
24
  return () => {
23
25
  disposer();
24
26
  };
25
- }, [store, selector, onChange, fireImmediately]);
27
+ }, [store, fireImmediately]);
26
28
  }
27
29
 
28
30
  export { useStoreWatcher };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/useStoreWatcher.ts"],"names":[],"mappings":";AACA,OAAO,OAAO;AACd,SAAS,UAAU,YAAY;AAC/B,SAAS,iBAAiB;AAenB,SAAS,gBAMZ,OACA,UACA,UACA,kBAA2B,OACvB;AACJ,YAAU,MAAM;AAEZ,UAAM,WAAW;AAAA,MACb,MAAM;AAEF,eAAO,KAAK,SAAS,KAAK,CAAC;AAAA,MAC/B;AAAA,MACA,CAAC,OAAO,kBAAkB;AAEtB,YAAI,CAAC,iBAAiB,CAAC,EAAE,QAAQ,OAAO,aAAa,GAAG;AACpD,mBAAS,KAAK;AAAA,QAClB;AAAA,MACJ;AAAA,MACA,EAAE,gBAAgB;AAAA,IACtB;AAGA,UAAM,gBAAgB,MAAM;AACxB,eAAS;AAAA,IACb,CAAC;AAGD,WAAO,MAAM;AACT,eAAS;AAAA,IACb;AAAA,EACJ,GAAG,CAAC,OAAO,UAAU,UAAU,eAAe,CAAC;AACnD","sourcesContent":["import type { BeaconActions, BeaconDerived, BeaconState, Store } from \"@apogeelabs/beacon\";\nimport _ from \"lodash\";\nimport { reaction, toJS } from \"mobx\";\nimport { useEffect } from \"react\";\n\n/**\n * A custom React hook to watch for state changes in a beacon store.\n *\n * @template TState - The type of the store's state, extending BeaconState.\n * @template TDerived - The type of the store's derived values, extending BeaconDerived<TState>. If you don't have derived values, use EmptyDerived<TState>.\n * @template TActions - The type of the store's actions, extending BeaconActions<TState>. If you don't have actions, use EmptyActions.\n * @template T - The type returned by the selector function, inferred from the selector.\n *\n * @param {Store<TState, TDerived, TActions>} store - The Beacon store instance.\n * @param {(store: Store<TState, TDerived, TActions>) => T} selector - A function that extracts the part of the store to watch.\n * @param {(value: T) => void} onChange - A callback invoked with the new value when the selected state changes.\n * @param {boolean} [fireImmediately=false] - Whether to invoke onChange with the initial value.\n */\nexport function useStoreWatcher<\n TState extends BeaconState,\n TDerived extends BeaconDerived<TState>,\n TActions extends BeaconActions<TState>,\n T,\n>(\n store: Store<TState, TDerived, TActions>,\n selector: (store: Store<TState, TDerived, TActions>) => T,\n onChange: (value: T) => void,\n fireImmediately: boolean = false\n): void {\n useEffect(() => {\n // Set up a MobX reaction to watch the selected state\n const disposer = reaction(\n () => {\n // Convert to plain JS to strip MobX observable properties\n return toJS(selector(store));\n },\n (value, previousValue) => {\n // Only trigger if the value actually changed using deep comparison\n if (!previousValue || !_.isEqual(value, previousValue)) {\n onChange(value);\n }\n },\n { fireImmediately }\n );\n\n // Register the disposer with the store for cleanup\n store.registerCleanup(() => {\n disposer();\n });\n\n // Cleanup when the component unmounts or dependencies change\n return () => {\n disposer();\n };\n }, [store, selector, onChange, fireImmediately]);\n}\n\nexport default useStoreWatcher;\n"]}
1
+ {"version":3,"sources":["../src/useStoreWatcher.ts"],"names":[],"mappings":";AACA,OAAO,OAAO;AACd,SAAS,UAAU,YAAY;AAC/B,SAAS,WAAW,sBAAsB;AAenC,SAAS,gBAMZ,OACA,UACA,UACA,kBAA2B,OACvB;AAGJ,QAAM,iBAAiB,eAAe,QAAQ;AAC9C,QAAM,iBAAiB,eAAe,QAAQ;AAE9C,YAAU,MAAM;AAEZ,UAAM,WAAW;AAAA,MACb,MAAM;AAEF,eAAO,KAAK,eAAe,KAAK,CAAC;AAAA,MACrC;AAAA,MACA,CAAC,OAAO,kBAAkB;AAEtB,YAAI,CAAC,iBAAiB,CAAC,EAAE,QAAQ,OAAO,aAAa,GAAG;AACpD,yBAAe,KAAK;AAAA,QACxB;AAAA,MACJ;AAAA,MACA,EAAE,gBAAgB;AAAA,IACtB;AAGA,UAAM,gBAAgB,MAAM;AACxB,eAAS;AAAA,IACb,CAAC;AAGD,WAAO,MAAM;AACT,eAAS;AAAA,IACb;AAAA,EACJ,GAAG,CAAC,OAAO,eAAe,CAAC;AAC/B","sourcesContent":["import type { BeaconActions, BeaconDerived, BeaconState, Store } from \"@apogeelabs/beacon\";\nimport _ from \"lodash\";\nimport { reaction, toJS } from \"mobx\";\nimport { useEffect, useEffectEvent } from \"react\";\n\n/**\n * A custom React hook to watch for state changes in a beacon store.\n *\n * @template TState - The type of the store's state, extending BeaconState.\n * @template TDerived - The type of the store's derived values, extending BeaconDerived<TState>. If you don't have derived values, use EmptyDerived<TState>.\n * @template TActions - The type of the store's actions, extending BeaconActions<TState>. If you don't have actions, use EmptyActions.\n * @template T - The type returned by the selector function, inferred from the selector.\n *\n * @param {Store<TState, TDerived, TActions>} store - The Beacon store instance.\n * @param {(store: Store<TState, TDerived, TActions>) => T} selector - A function that extracts the part of the store to watch.\n * @param {(value: T) => void} onChange - A callback invoked with the new value when the selected state changes.\n * @param {boolean} [fireImmediately=false] - Whether to invoke onChange with the initial value.\n */\nexport function useStoreWatcher<\n TState extends BeaconState,\n TDerived extends BeaconDerived<TState>,\n TActions extends BeaconActions<TState>,\n T,\n>(\n store: Store<TState, TDerived, TActions>,\n selector: (store: Store<TState, TDerived, TActions>) => T,\n onChange: (value: T) => void,\n fireImmediately: boolean = false\n): void {\n // Use useEffectEvent to wrap callbacks so they don't need to be in the dependency array\n // This prevents unnecessary re-runs when consumers pass inline functions\n const stableSelector = useEffectEvent(selector);\n const stableOnChange = useEffectEvent(onChange);\n\n useEffect(() => {\n // Set up a MobX reaction to watch the selected state\n const disposer = reaction(\n () => {\n // Convert to plain JS to strip MobX observable properties\n return toJS(stableSelector(store));\n },\n (value, previousValue) => {\n // Only trigger if the value actually changed using deep comparison\n if (!previousValue || !_.isEqual(value, previousValue)) {\n stableOnChange(value);\n }\n },\n { fireImmediately }\n );\n\n // Register the disposer with the store for cleanup\n store.registerCleanup(() => {\n disposer();\n });\n\n // Cleanup when the component unmounts or dependencies change\n return () => {\n disposer();\n };\n }, [store, fireImmediately]);\n}\n\nexport default useStoreWatcher;\n"]}
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "name": "Jim Cowart"
7
7
  }
8
8
  ],
9
- "version": "0.2.0",
9
+ "version": "1.0.0",
10
10
  "license": "ISC",
11
11
  "description": "beacon middleware for react-query integration with beacon stores",
12
12
  "engines": {
@@ -39,13 +39,13 @@
39
39
  "dependencies": {
40
40
  "lodash": "4.17.16",
41
41
  "mobx": "^6.13.7",
42
- "react": "^18.0.0",
43
- "@apogeelabs/beacon": "0.4.1"
42
+ "react": "^19.2.3",
43
+ "@apogeelabs/beacon": "1.0.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/jest": "^29.5.5",
47
47
  "@types/lodash": "4.17.16",
48
- "@types/react": "^18.0.0",
48
+ "@types/react": "^19.2.3",
49
49
  "eslint": "^8.0.1",
50
50
  "eslint-config-standard": "^17.1.0",
51
51
  "eslint-plugin-import": "^2.25.2",