Skip to content

Commit 3d9f374

Browse files
authored
Merge pull request #36 from react18-tools/selector-regexp
Selector regexp
2 parents 1481a7d + 3ea6073 commit 3d9f374

File tree

16 files changed

+119
-51
lines changed

16 files changed

+119
-51
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
> 🌐 **Live Demo with Code**: [https://r18gs.vercel.app/](https://r18gs.vercel.app/)
1212
13+
> **Important Note:**
14+
> There are no breaking changes when upgrading from version 2.0.\* to version 3.\*. However, we released a major version because version 2.1.0 introduced new APIs. To improve those APIs, we made some breaking changes, which will only affect you if you are using the new features introduced in version 2.1.0.
15+
1316
## Motivation
1417

1518
While developing libraries utilizing React 18 features with Zustand, I encountered issues with tree-shaking when importing from specific folders. This caused the creation of multiple Zustand stores, resulting in bugs and unnecessary JavaScript code execution.

lib/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# r18gs
22

3+
## 3.0.0
4+
5+
### Major Changes
6+
7+
- 935e8f8: Selectors are now replaced by includeRegExp and excludeRegExp
8+
39
## 2.1.0
410

511
### Minor Changes

lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "r18gs",
33
"author": "Mayank Kumar Chaudhari <https://mayank-chaudhari.vercel.app>",
44
"private": false,
5-
"version": "2.1.0",
5+
"version": "3.0.0",
66
"description": "A simple yet elegant, light weight, react18 global store to replace Zustand for better tree shaking.",
77
"license": "MPL-2.0",
88
"main": "./dist/index.js",

lib/src/index.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@ import type { RGS, SetStateAction, ValueType } from "./utils";
66
export type { SetterArgType, SetStateAction, Plugin } from "./utils";
77

88
/**
9-
* Use this hook similar to `useState` hook.
10-
* The difference is that you need to pass a
11-
* unique key - unique across the app to make
12-
* this state accessible to all client components.
9+
* A React hook for managing shared global state, similar to the `useState` hook.
10+
* This hook requires a unique key, which identifies the global store and allows state sharing across all client components.
1311
*
1412
* @example
1513
* ```tsx
1614
* const [state, setState] = useRGS<number>("counter", 1);
1715
* ```
1816
*
19-
* @param key - Unique key to identify the store.
20-
* @param value - Initial value of the store.
21-
* @returns - A tuple (Ordered sequance of values) containing the state and a function to set the state.
17+
* @param key - A unique key to identify the global store.
18+
* @param value - The initial value of the global state. Can be a value or a function returning a value.
19+
* @param includeRegExp - (Optional) A regular expression to specify which fields trigger updates.
20+
* @param excludeRegExp - (Optional) A regular expression to specify which fields should be excluded from updates.
21+
* @returns A tuple containing the current state and a function to update the state.
22+
*
23+
* @see [Learn More](https://r18gs.vercel.app/)
2224
*/
2325
const useRGS = <T>(
2426
key: string,
2527
value?: ValueType<T>,
26-
...fields: (keyof T)[]
28+
includeRegExp?: RegExp | null | 0,
29+
excludeRegExp?: RegExp,
2730
): [T, SetStateAction<T>] => {
2831
/** Initialize the named store when invoked for the first time. */
29-
if (!globalRGS[key])
32+
if (!globalRGS[key]) {
3033
globalRGS[key] = {
3134
v: value instanceof Function ? value() : value,
3235
l: [],
@@ -37,8 +40,9 @@ const useRGS = <T>(
3740
triggerListeners(rgs, oldV, rgs.v);
3841
},
3942
};
43+
}
4044

41-
return createHook<T>(key, fields);
45+
return createHook<T>(key, includeRegExp, excludeRegExp);
4246
};
4347

4448
export { useRGS };

lib/src/utils.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useSyncExternalStore } from "react";
22

3-
export type Selector = string | number | Symbol;
43
type Listener = () => void;
5-
type ListenerWithSelectors = { l: Listener; s: Selector[] };
4+
type ListenerWithSelectors = {
5+
l: Listener;
6+
s: [includeRegExp?: RegExp | null, excludeRegExp?: RegExp];
7+
};
68

79
export type SetterArgType<T> = T | ((prevState: T) => T);
810
export type SetStateAction<T> = (value: SetterArgType<T>) => void;
@@ -29,21 +31,37 @@ export const globalRGS = globalThisForBetterMinification.rgs;
2931

3032
/** trigger all listeners */
3133
export const triggerListeners = <T>(rgs: RGS, oldV: T, newV: T) => {
32-
const updatedFiels: Selector[] = [];
34+
const updatedFiels: string[] = [];
3335
// no need to test this --- it will automatically fail
3436
// if (typeof oldV === "object" && typeof rgs.v === "object")
3537
for (const key in oldV) if (oldV[key] !== newV[key]) updatedFiels.push(key);
36-
rgs.l.forEach(({ l, s }) => (!s.length || s.some(filed => updatedFiels.includes(filed))) && l());
38+
// const testStr = updatedFiels.join("; ");
39+
rgs.l.forEach(
40+
({ l, s: [includeRegExp, excludeRegExp] }) =>
41+
(!(newV instanceof Object) ||
42+
updatedFiels.filter(
43+
s =>
44+
(!includeRegExp || includeRegExp.test(s)) && (!excludeRegExp || !excludeRegExp.test(s)),
45+
).length) &&
46+
l(),
47+
);
3748
};
3849

3950
/** Extract coomon create hook logic to utils */
40-
export const createHook = <T>(key: string, fields: (keyof T)[]): [T, SetStateAction<T>] => {
51+
export const createHook = <T>(
52+
key: string,
53+
includeRegExp?: RegExp | null | 0,
54+
excludeRegExp?: RegExp,
55+
): [T, SetStateAction<T>] => {
4156
const rgs = globalRGS[key] as RGS;
4257
/** This function is called by react to get the current stored value. */
4358
const getSnapshot = () => rgs.v as T;
4459
const val = useSyncExternalStore<T>(
4560
listener => {
46-
const listenerWithSelectors = { l: listener, s: fields };
61+
const listenerWithSelectors = {
62+
l: listener,
63+
s: [includeRegExp, excludeRegExp],
64+
} as ListenerWithSelectors;
4765
rgs.l.push(listenerWithSelectors);
4866
return () => {
4967
rgs.l = rgs.l.filter(l => l !== listenerWithSelectors);
@@ -133,8 +151,19 @@ export const useRGSWithPlugins = <T>(
133151
value?: ValueType<T>,
134152
plugins?: Plugin<T>[],
135153
doNotInit = false,
136-
...fields: (keyof T)[]
154+
includeRegExp?: RegExp | null | 0,
155+
excludeRegExp?: RegExp,
137156
): [T, SetStateAction<T>] => {
138157
if (!globalRGS[key]?.s) initWithPlugins(key, value, plugins, doNotInit);
139-
return createHook<T>(key, fields);
158+
return createHook<T>(key, includeRegExp, excludeRegExp);
159+
};
160+
161+
/**
162+
* Converts a list of selectors into a regular expression.
163+
* @param list - An array of strings representing the fields to match.
164+
* @returns A regular expression that matches any field from the provided list.
165+
*/
166+
export const listToRegExp = (list: string[]) => {
167+
const escapedList = list.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
168+
return new RegExp(`^(${escapedList.join("|")})$`);
140169
};

lib/tests/with-selectors/counter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useRef } from "react";
22
import { useStore } from "./store";
33

44
export function Counter() {
5-
const [{ count }, setState] = useStore("count");
5+
const [{ count }, setState] = useStore(/^count$/);
66
const renderCount = useRef(0);
77
renderCount.current++;
88
return (
Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import { useRef } from "react";
22
import { useStore } from "./store";
33
import styles from "../demo.module.scss";
4+
import { listToRegExp } from "../../src/utils";
45

56
export function Header() {
6-
const [{ name }] = useStore("name");
7+
const [{ name }] = useStore(listToRegExp(["name"]));
78
const renderCount = useRef(0);
89
renderCount.current++;
910
return (
10-
<header className={styles.header}>
11-
<h1>My name is {name}</h1>
12-
<small>
13-
<i>
14-
Updates only when <code>name</code> changes.{" "}
15-
<code data-testid="header-render-count">renderCount = {renderCount.current}</code>
16-
</i>
17-
</small>
18-
</header>
11+
<>
12+
<h1>Example with Selectors</h1>
13+
<header className={styles.header}>
14+
<h2>My name is {name}</h2>
15+
<small>
16+
<i>
17+
Updates only when <code>name</code> changes.{" "}
18+
<code data-testid="header-render-count">renderCount = {renderCount.current}</code>
19+
</i>
20+
</small>
21+
</header>
22+
</>
1923
);
2024
}

lib/tests/with-selectors/store.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ interface MyStore {
99
};
1010
}
1111

12-
export const useStore = (...selectors: (keyof MyStore)[]) =>
12+
export const useStore = (includeRegExp?: RegExp | null | 0, excludeRegExp?: RegExp) =>
1313
useRGS<MyStore>(
1414
"my-store-with-selectors",
1515
{
@@ -20,5 +20,6 @@ export const useStore = (...selectors: (keyof MyStore)[]) =>
2020
age: 30,
2121
},
2222
},
23-
...selectors,
23+
includeRegExp,
24+
excludeRegExp,
2425
);

lib/tests/with-selectors/user-data.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useRef } from "react";
22
import { useStore } from "./store";
33

44
export function UserData() {
5-
const [{ user }, setState] = useStore("user");
5+
const [{ user }, setState] = useStore(/^user$/);
66
const renderCount = useRef(0);
77
renderCount.current++;
88
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {

md-docs/1.getting-started.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ interface MyStore {
5252
};
5353
}
5454

55-
export const useStore = (...selectors: (keyof MyStore)[]) =>
55+
export const useStore = (includeRegExp?: RegExp | null | 0, excludeRegExp?: RegExp) =>
5656
useRGS<MyStore>(
5757
"my-store-with-selectors",
5858
{
@@ -63,14 +63,23 @@ export const useStore = (...selectors: (keyof MyStore)[]) =>
6363
age: 30,
6464
},
6565
},
66-
...selectors,
66+
includeRegExp,
67+
excludeRegExp,
6768
);
6869
```
6970

7071
And use it like
7172

7273
```ts
73-
const [{ count }, setState] = useStore("count");
74+
const [{ count }, setState] = useStore(/^count$/);
75+
```
76+
77+
or
78+
79+
```ts
80+
import { listToRegExp } from "r18gs/dist/utils";
81+
...
82+
const [{ count }, setState] = useStore(listToRegExp([count]));
7483
```
7584

7685
> Important: Please check out https://r18gs.vercel.app/ to learn how to use selectors.

0 commit comments

Comments
 (0)