Skip to content

Commit

Permalink
Merge pull request #36 from react18-tools/selector-regexp
Browse files Browse the repository at this point in the history
Selector regexp
  • Loading branch information
mayank1513 authored Dec 18, 2024
2 parents 1481a7d + 3ea6073 commit 3d9f374
Show file tree
Hide file tree
Showing 16 changed files with 119 additions and 51 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

> 🌐 **Live Demo with Code**: [https://r18gs.vercel.app/](https://r18gs.vercel.app/)
> **Important Note:**
> 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.
## Motivation

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.
Expand Down
6 changes: 6 additions & 0 deletions lib/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# r18gs

## 3.0.0

### Major Changes

- 935e8f8: Selectors are now replaced by includeRegExp and excludeRegExp

## 2.1.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "r18gs",
"author": "Mayank Kumar Chaudhari <https://mayank-chaudhari.vercel.app>",
"private": false,
"version": "2.1.0",
"version": "3.0.0",
"description": "A simple yet elegant, light weight, react18 global store to replace Zustand for better tree shaking.",
"license": "MPL-2.0",
"main": "./dist/index.js",
Expand Down
24 changes: 14 additions & 10 deletions lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,30 @@ import type { RGS, SetStateAction, ValueType } from "./utils";
export type { SetterArgType, SetStateAction, Plugin } from "./utils";

/**
* Use this hook similar to `useState` hook.
* The difference is that you need to pass a
* unique key - unique across the app to make
* this state accessible to all client components.
* A React hook for managing shared global state, similar to the `useState` hook.
* This hook requires a unique key, which identifies the global store and allows state sharing across all client components.
*
* @example
* ```tsx
* const [state, setState] = useRGS<number>("counter", 1);
* ```
*
* @param key - Unique key to identify the store.
* @param value - Initial value of the store.
* @returns - A tuple (Ordered sequance of values) containing the state and a function to set the state.
* @param key - A unique key to identify the global store.
* @param value - The initial value of the global state. Can be a value or a function returning a value.
* @param includeRegExp - (Optional) A regular expression to specify which fields trigger updates.
* @param excludeRegExp - (Optional) A regular expression to specify which fields should be excluded from updates.
* @returns A tuple containing the current state and a function to update the state.
*
* @see [Learn More](https://r18gs.vercel.app/)
*/
const useRGS = <T>(
key: string,
value?: ValueType<T>,
...fields: (keyof T)[]
includeRegExp?: RegExp | null | 0,
excludeRegExp?: RegExp,
): [T, SetStateAction<T>] => {
/** Initialize the named store when invoked for the first time. */
if (!globalRGS[key])
if (!globalRGS[key]) {
globalRGS[key] = {
v: value instanceof Function ? value() : value,
l: [],
Expand All @@ -37,8 +40,9 @@ const useRGS = <T>(
triggerListeners(rgs, oldV, rgs.v);
},
};
}

return createHook<T>(key, fields);
return createHook<T>(key, includeRegExp, excludeRegExp);
};

export { useRGS };
45 changes: 37 additions & 8 deletions lib/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useSyncExternalStore } from "react";

export type Selector = string | number | Symbol;
type Listener = () => void;
type ListenerWithSelectors = { l: Listener; s: Selector[] };
type ListenerWithSelectors = {
l: Listener;
s: [includeRegExp?: RegExp | null, excludeRegExp?: RegExp];
};

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

/** trigger all listeners */
export const triggerListeners = <T>(rgs: RGS, oldV: T, newV: T) => {
const updatedFiels: Selector[] = [];
const updatedFiels: string[] = [];
// no need to test this --- it will automatically fail
// if (typeof oldV === "object" && typeof rgs.v === "object")
for (const key in oldV) if (oldV[key] !== newV[key]) updatedFiels.push(key);
rgs.l.forEach(({ l, s }) => (!s.length || s.some(filed => updatedFiels.includes(filed))) && l());
// const testStr = updatedFiels.join("; ");
rgs.l.forEach(
({ l, s: [includeRegExp, excludeRegExp] }) =>
(!(newV instanceof Object) ||
updatedFiels.filter(
s =>
(!includeRegExp || includeRegExp.test(s)) && (!excludeRegExp || !excludeRegExp.test(s)),
).length) &&
l(),
);
};

/** Extract coomon create hook logic to utils */
export const createHook = <T>(key: string, fields: (keyof T)[]): [T, SetStateAction<T>] => {
export const createHook = <T>(
key: string,
includeRegExp?: RegExp | null | 0,
excludeRegExp?: RegExp,
): [T, SetStateAction<T>] => {
const rgs = globalRGS[key] as RGS;
/** This function is called by react to get the current stored value. */
const getSnapshot = () => rgs.v as T;
const val = useSyncExternalStore<T>(
listener => {
const listenerWithSelectors = { l: listener, s: fields };
const listenerWithSelectors = {
l: listener,
s: [includeRegExp, excludeRegExp],
} as ListenerWithSelectors;
rgs.l.push(listenerWithSelectors);
return () => {
rgs.l = rgs.l.filter(l => l !== listenerWithSelectors);
Expand Down Expand Up @@ -133,8 +151,19 @@ export const useRGSWithPlugins = <T>(
value?: ValueType<T>,
plugins?: Plugin<T>[],
doNotInit = false,
...fields: (keyof T)[]
includeRegExp?: RegExp | null | 0,
excludeRegExp?: RegExp,
): [T, SetStateAction<T>] => {
if (!globalRGS[key]?.s) initWithPlugins(key, value, plugins, doNotInit);
return createHook<T>(key, fields);
return createHook<T>(key, includeRegExp, excludeRegExp);
};

/**
* Converts a list of selectors into a regular expression.
* @param list - An array of strings representing the fields to match.
* @returns A regular expression that matches any field from the provided list.
*/
export const listToRegExp = (list: string[]) => {
const escapedList = list.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
return new RegExp(`^(${escapedList.join("|")})$`);
};
2 changes: 1 addition & 1 deletion lib/tests/with-selectors/counter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useRef } from "react";
import { useStore } from "./store";

export function Counter() {
const [{ count }, setState] = useStore("count");
const [{ count }, setState] = useStore(/^count$/);
const renderCount = useRef(0);
renderCount.current++;
return (
Expand Down
24 changes: 14 additions & 10 deletions lib/tests/with-selectors/header.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { useRef } from "react";
import { useStore } from "./store";
import styles from "../demo.module.scss";
import { listToRegExp } from "../../src/utils";

export function Header() {
const [{ name }] = useStore("name");
const [{ name }] = useStore(listToRegExp(["name"]));
const renderCount = useRef(0);
renderCount.current++;
return (
<header className={styles.header}>
<h1>My name is {name}</h1>
<small>
<i>
Updates only when <code>name</code> changes.{" "}
<code data-testid="header-render-count">renderCount = {renderCount.current}</code>
</i>
</small>
</header>
<>
<h1>Example with Selectors</h1>
<header className={styles.header}>
<h2>My name is {name}</h2>
<small>
<i>
Updates only when <code>name</code> changes.{" "}
<code data-testid="header-render-count">renderCount = {renderCount.current}</code>
</i>
</small>
</header>
</>
);
}
5 changes: 3 additions & 2 deletions lib/tests/with-selectors/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface MyStore {
};
}

export const useStore = (...selectors: (keyof MyStore)[]) =>
export const useStore = (includeRegExp?: RegExp | null | 0, excludeRegExp?: RegExp) =>
useRGS<MyStore>(
"my-store-with-selectors",
{
Expand All @@ -20,5 +20,6 @@ export const useStore = (...selectors: (keyof MyStore)[]) =>
age: 30,
},
},
...selectors,
includeRegExp,
excludeRegExp,
);
2 changes: 1 addition & 1 deletion lib/tests/with-selectors/user-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useRef } from "react";
import { useStore } from "./store";

export function UserData() {
const [{ user }, setState] = useStore("user");
const [{ user }, setState] = useStore(/^user$/);
const renderCount = useRef(0);
renderCount.current++;
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
Expand Down
15 changes: 12 additions & 3 deletions md-docs/1.getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ interface MyStore {
};
}

export const useStore = (...selectors: (keyof MyStore)[]) =>
export const useStore = (includeRegExp?: RegExp | null | 0, excludeRegExp?: RegExp) =>
useRGS<MyStore>(
"my-store-with-selectors",
{
Expand All @@ -63,14 +63,23 @@ export const useStore = (...selectors: (keyof MyStore)[]) =>
age: 30,
},
},
...selectors,
includeRegExp,
excludeRegExp,
);
```

And use it like

```ts
const [{ count }, setState] = useStore("count");
const [{ count }, setState] = useStore(/^count$/);
```

or

```ts
import { listToRegExp } from "r18gs/dist/utils";
...
const [{ count }, setState] = useStore(listToRegExp([count]));
```

> Important: Please check out https://r18gs.vercel.app/ to learn how to use selectors.
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @repo/shared

## 0.0.6

### Patch Changes

- Updated dependencies [935e8f8]
- [email protected]

## 0.0.5

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@repo/shared",
"version": "0.0.5",
"version": "0.0.6",
"private": true,
"sideEffects": false,
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/client/demo/with-selectors/counter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useRef } from "react";
import { useStore } from "./store";

export function Counter() {
const [{ count }, setState] = useStore("count");
const [{ count }, setState] = useStore(/^count$/);
const renderCount = useRef(0);
renderCount.current++;
return (
Expand Down
24 changes: 14 additions & 10 deletions packages/shared/src/client/demo/with-selectors/header.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { useRef } from "react";
import { useStore } from "./store";
import styles from "../demo.module.scss";
import { listToRegExp } from "r18gs/dist/utils";

export function Header() {
const [{ name }] = useStore("name");
const [{ name }] = useStore(listToRegExp(["name"]));
const renderCount = useRef(0);
renderCount.current++;
return (
<header className={styles.header}>
<h1>My name is {name}</h1>
<small>
<i>
Updates only when <code>name</code> changes.{" "}
<code data-testid="header-render-count">renderCount = {renderCount.current}</code>
</i>
</small>
</header>
<>
<h1>Example with Selectors</h1>
<header className={styles.header}>
<h2>My name is {name}</h2>
<small>
<i>
Updates only when <code>name</code> changes.{" "}
<code data-testid="header-render-count">renderCount = {renderCount.current}</code>
</i>
</small>
</header>
</>
);
}
5 changes: 3 additions & 2 deletions packages/shared/src/client/demo/with-selectors/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface MyStore {
};
}

export const useStore = (...selectors: (keyof MyStore)[]) =>
export const useStore = (includeRegExp?: RegExp | null | 0, excludeRegExp?: RegExp) =>
useRGS<MyStore>(
"my-store-with-selectors",
{
Expand All @@ -20,5 +20,6 @@ export const useStore = (...selectors: (keyof MyStore)[]) =>
age: 30,
},
},
...selectors,
includeRegExp,
excludeRegExp,
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useRef } from "react";
import { useStore } from "./store";

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

0 comments on commit 3d9f374

Please sign in to comment.