Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] add tracked hooks selector (big thanks for react-tracked) #36

Merged
merged 3 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ API.
- Derived actions
- `immer`, `devtools` and `persist` middlewares
- Full typescript support
- `react-tracked` support

## Create a store

Expand All @@ -40,6 +41,10 @@ import { createStore } from '@udecode/zustood'
const repoStore = createStore('repo')({
name: 'zustood',
stars: 0,
owner: {
name: 'someone',
email: '[email protected]',
},
})
```

Expand Down Expand Up @@ -72,6 +77,17 @@ repoStore.use.name()
repoStore.use.stars()
```

### Tracked Hooks

> Big thanks for [react-tracked](https://github.com/dai-shi/react-tracked)

Use the tracked hooks in React components, no providers needed. Select your
state and the component will only triggers re-renders if the **accessed property** is changed. Use the `useTracked` method:

```ts
repoStore.useTracked.owner()
```

We recommend using the global hooks (see below) to support ESLint hook
linting.

Expand Down Expand Up @@ -182,6 +198,9 @@ export const rootStore = {
// Global hook selectors
export const useStore = () => mapValuesKey('use', rootStore);

// Global tracked hook selectors
export const useTrackedStore = () => mapValuesKey('useTracked', rootStore);

// Global getter selectors
export const store = mapValuesKey('get', rootStore);

Expand All @@ -202,7 +221,32 @@ useStore().modal.isOpen()
useStore().repo.middlewares(shallow)
```

By using `useStore()`, ESLint will correctly lint hook errors.
### Global tracked hook selectors

```tsx
// with useTrackStore UserEmail Component will only re-render when accessed property owner.email changed
const UserEmail = () => {
const owner = useTrackedStore().repo.owner()
return (
<div>
<span>User Email: {owner.email}</span>
</div>
);
};

// with useStore UserEmail Component re-render when owner changed, but you can pass equalityFn to avoid it.
const UserEmail = () => {
const owner = useStore().repo.owner()
// const owner = useStore().repo.owner((prev, next) => prev.owner.email === next.owner.email)
return (
<div>
<span>User Email: {owner.email}</span>
</div>
);
};
```

By using `useStore() or useTrackStore()`, ESLint will correctly lint hook errors.

### Global getter selectors

Expand Down
3 changes: 2 additions & 1 deletion packages/zustood/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"test": "jest"
},
"dependencies": {
"immer": "^9.0.6"
"immer": "^9.0.6",
"react-tracked": "^1.7.9"
},
"peerDependencies": {
"zustand": ">=3.5.10"
Expand Down
10 changes: 10 additions & 0 deletions packages/zustood/src/createStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { setAutoFreeze, enableMapSet } from 'immer';
import { createTrackedSelector } from 'react-tracked';
import create, { State, StateCreator } from 'zustand';
import {
devtools as devtoolsMiddleware,
Expand All @@ -18,6 +19,7 @@ import { generateStateActions } from './utils/generateStateActions';
import { storeFactory } from './utils/storeFactory';
import { generateStateGetSelectors } from './utils/generateStateGetSelectors';
import { generateStateHookSelectors } from './utils/generateStateHookSelectors';
import { generateStateTrackedHooksSelectors } from './utils/generateStateTrackedHooksSelectors';
import { immerMiddleware } from './middlewares/immer.middleware';
import { pipe } from './utils/pipe';
import { CreateStoreOptions } from './types/CreateStoreOptions';
Expand Down Expand Up @@ -77,6 +79,12 @@ export const createStore =
const hookSelectors = generateStateHookSelectors(useStore);
const getterSelectors = generateStateGetSelectors(useStore);

const useTrackedStore = createTrackedSelector(useStore);
const trackedHooksSelectors = generateStateTrackedHooksSelectors(
useStore,
useTrackedStore
);

const api = {
get: {
state: store.getState,
Expand All @@ -90,7 +98,9 @@ export const createStore =
} as StateActions<T>,
store,
use: hookSelectors,
useTracked: trackedHooksSelectors,
useStore,
useTrackedStore,
extendSelectors: () => api as any,
extendActions: () => api as any,
};
Expand Down
6 changes: 6 additions & 0 deletions packages/zustood/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export type StoreApiGet<
> = StateGetters<T> & TSelectors;
export type StoreApiUse<T extends State = {}, TSelectors = {}> = GetRecord<T> &
TSelectors;
export type StoreApiUseTracked<
T extends State = {},
TSelectors = {}
> = GetRecord<T> & TSelectors;
export type StoreApiSet<TActions = {}> = TActions;

export type StoreApi<
Expand All @@ -22,7 +26,9 @@ export type StoreApi<
set: StoreApiSet<TActions>;
store: ImmerStoreApi<T>;
use: StoreApiUse<T, TSelectors>;
useTracked: StoreApiUseTracked<T, TSelectors>;
useStore: UseImmerStore<T>;
useTrackedStore: () => T;

extendSelectors<SB extends SelectorBuilder<TName, T, TActions, TSelectors>>(
builder: SB
Expand Down
8 changes: 8 additions & 0 deletions packages/zustood/src/utils/extendSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
StoreApi,
StoreApiGet,
StoreApiUse,
StoreApiUseTracked,
} from '../types';

export const extendSelectors = <
Expand All @@ -26,6 +27,10 @@ export const extendSelectors = <
...api.use,
} as StoreApiUse<T, TSelectors & ReturnType<CB>>;

const useTracked = {
...api.useTracked,
} as StoreApiUseTracked<T, TSelectors & ReturnType<CB>>;

const get = {
...api.get,
} as StoreApiGet<T, TSelectors & ReturnType<CB>>;
Expand All @@ -35,6 +40,9 @@ export const extendSelectors = <
use[key] = (...args: any[]) =>
api.useStore((state) => builder(state, api.get, api)[key])(...args);
// @ts-ignore
useTracked[key] = (...args: any[]) =>
api.useStore((state) => builder(state, api.get, api)[key])(...args);
// @ts-ignore
get[key] = (...args: any[]) =>
builder(api.store.getState(), api.get, api)[key](...args);
});
Expand Down
17 changes: 17 additions & 0 deletions packages/zustood/src/utils/generateStateTrackedHooksSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { State } from 'zustand';
import { GetRecord, UseImmerStore } from '../types';

export const generateStateTrackedHooksSelectors = <T extends State>(
store: UseImmerStore<T>,
trackedStore: () => T
) => {
const selectors: GetRecord<T> = {} as any;

Object.keys(store.getState()).forEach((key) => {
selectors[key] = () => {
return trackedStore()[key as keyof T];
};
});

return selectors;
};
45 changes: 45 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3585,6 +3585,7 @@ __metadata:
resolution: "@udecode/zustood@workspace:packages/zustood"
dependencies:
immer: ^9.0.6
react-tracked: ^1.7.9
peerDependencies:
zustand: ">=3.5.10"
languageName: unknown
Expand Down Expand Up @@ -11502,6 +11503,13 @@ __metadata:
languageName: node
linkType: hard

"proxy-compare@npm:2.1.0":
version: 2.1.0
resolution: "proxy-compare@npm:2.1.0"
checksum: e431403abbb52468045635f434846c55b388c3ccf4012efe729e3fa846513b5d49e2488328582d5be792e7b9b0ba5f5a111887b3a2c4f9a273fc432ab79c7b63
languageName: node
linkType: hard

"prr@npm:~0.0.0":
version: 0.0.0
resolution: "prr@npm:0.0.0"
Expand Down Expand Up @@ -11721,6 +11729,26 @@ __metadata:
languageName: node
linkType: hard

"react-tracked@npm:^1.7.9":
version: 1.7.9
resolution: "react-tracked@npm:1.7.9"
dependencies:
proxy-compare: 2.1.0
use-context-selector: 1.3.10
peerDependencies:
react: ">=16.8.0"
react-dom: "*"
react-native: "*"
scheduler: ">=0.19.0"
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: f8b173603173fd764263fc7e5db304482169faed32a2d528d4414d9ec60dce58c7bba0eba617f91e2e6a6af12900c4b8eb49c2caa1f7e0ac1f848f35db01b15c
languageName: node
linkType: hard

"react@npm:^17.0.1":
version: 17.0.2
resolution: "react@npm:17.0.2"
Expand Down Expand Up @@ -14131,6 +14159,23 @@ typescript@^4.4.3:
languageName: node
linkType: hard

"use-context-selector@npm:1.3.10":
version: 1.3.10
resolution: "use-context-selector@npm:1.3.10"
peerDependencies:
react: ">=16.8.0"
react-dom: "*"
react-native: "*"
scheduler: ">=0.19.0"
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: 86fe17bb25dc2c6730e83911b3baf46fbc1be45f27c86bf2f94b4ed55d443572c6a5d725b77cf6a2ae1ddfe388bfbd6850851591ca1a26e7e216e5aa58b2631d
languageName: node
linkType: hard

"use@npm:^3.1.0":
version: 3.1.1
resolution: "use@npm:3.1.1"
Expand Down