Skip to content

Commit

Permalink
Merge pull request #4 from Albert-Gao/v2
Browse files Browse the repository at this point in the history
BREAKING CHANGE: updated the support for zustand v4
  • Loading branch information
Albert-Gao authored Jul 26, 2022
2 parents 6042e66 + b03ed75 commit 338338e
Show file tree
Hide file tree
Showing 9 changed files with 2,116 additions and 1,642 deletions.
55 changes: 40 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@
<img src="https://badgen.net/twitter/follow/albertgao"/>
</a>

**Stop writing selectors and do more work!**
**Enjoy the performance gain of selectors without writing selectors!**

## Features

- auto generate selectors for Zustand store
- 2 styles available
- Typescript auto completion supported
- auto generate selectors for Zustand store (be it a value or a function)
- Two styles available
- Fully Typescript support (auto-completion for the generated selectors!)

## Install

Expand All @@ -56,6 +56,16 @@ Or with yarn:
yarn add auto-zustand-selectors-hook
```

## Notice

The `v2` supports `Zustand v4`, if you are using a `Zustand v3`, please install the `v1` version

```bash
yarn add [email protected]

npm install --save [email protected]
```

## Usage

Let's say you have a store like this
Expand All @@ -72,9 +82,9 @@ const useStoreBase = create<BearState>((set) => ({
}));
```

**There are two types of selectors you can generate, purely function signature difference, underneath, they are all selectors.**
> **There are two types of selectors you can generate, purely function signature difference, underneath, they are all selectors.**
### For function style ( createSelectorFunctions )
## 1. For function style ( createSelectorFunctions )

```typescript
import { createSelectorFunctions } from 'auto-zustand-selectors-hook';
Expand All @@ -84,14 +94,15 @@ import { create } from 'zustand';
const useStore = createSelectorFunctions(useStoreBase);

// use it like this!
// .use.blahblah is a pre-generated selector, yeah!
// useStore.use.blah is a pre-generated selector, yeah!
const TestComponent = () => {
const bears = useStore.use.bears();
const increase = useStore.use.increase();

return (
<>
<span>{bears}</span>

<button
onClick={() => {
increase(1);
Expand All @@ -104,30 +115,25 @@ const TestComponent = () => {
};
```

### For hook style ( createSelectorHooks )
## 2. For hook style ( createSelectorHooks )

```typescript
import { createSelectorHooks } from 'auto-zustand-selectors-hook';
import { create } from 'zustand';

// create your store
const useStoreBase = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}));

// wrap your store
const useStore = createSelectorHooks(useStoreBase);

// use it like this!
// .useBlahblah is a pre-generated selector, yeah!
// useStore.useBlah is a pre-generated selector, yeah!
const TestComponent = () => {
const bears = useStore.useBears();
const increase = useStore.useIncrease();

return (
<>
<span>{bears}</span>

<button
onClick={() => {
increase(1);
Expand All @@ -140,6 +146,25 @@ const TestComponent = () => {
};
```

## 3. use with middlewares

> You use the middleware for creating the base store, and `ALWAYS` use `auto-zustand-selectors-hooks` as a separate wrapper
```typescript
import { createSelectorHooks } from '../src/index';
import create from 'zustand';
import { persist } from 'zustand/middleware';

const useStoreBase = create<BearState>()(
persist((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}))
);

export const useStore = createSelectorHooks(useStoreBase);
```

## License

MIT © [Albert Gao](https://github.com/Albert-Gao)
Expand Down
67 changes: 36 additions & 31 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
],
"author": "Albert Gao <[email protected]>",
"repository": {
"url": "",
"url": "git+https://github.com/Albert-Gao/auto-zustand-selectors-hook",
"type": "git"
},
"homepage": "",
"bugs": {
"url": "https://github.com/Albert-Gao/auto-zustand-selectors-hook/issues"
},
"homepage": "https://github.com/Albert-Gao/auto-zustand-selectors-hook",
"license": "MIT",
"scripts": {
"start": "rollup -c rollup.config.js -w",
Expand All @@ -26,44 +29,46 @@
"test:ci": "cross-env CI=1 jest"
},
"engines": {
"node": ">=10.0.0"
"node": ">=14.0.0"
},
"peerDependencies": {
"zustand": ">=3"
"zustand": ">=4"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^19.0.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.7",
"@types/jest": "^26.0.23",
"@types/node": "^15.3.0",
"@types/react": "^17.0.5",
"@types/react-dom": "^17.0.5",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"@rollup/plugin-commonjs": "^22.0.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@types/jest": "^28.1.6",
"@types/node": "^18.6.1",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"cross-env": "^7.0.3",
"eslint": "^7.26.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.23.2",
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react-hooks": "^4.6.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.0.4",
"lint-staged": "^11.0.0",
"prettier": "^2.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.1",
"rollup": "^2.48.0",
"jest": "^28.1.3",
"jest-environment-jsdom": "^28.1.3",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rollup": "^2.77.1",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.30.0",
"ts-jest": "^27.0.3",
"typescript": "^4.3.2",
"zustand": "^3.5.2"
"rollup-plugin-typescript2": "^0.32.1",
"ts-jest": "^28.0.7",
"typescript": "^4.7.4",
"zustand": "^4.0.0"
}
}
23 changes: 23 additions & 0 deletions src/createSelectorFunctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { StoreApi, UseBoundStore } from 'zustand';

export interface ZustandFuncSelectors<StateType> {
use: {
[key in keyof StateType]: () => StateType[key];
};
}

export function createSelectorFunctions<StateType extends object>(
store: UseBoundStore<StoreApi<StateType>>
) {
const storeIn = store as any;

storeIn.use = {};

Object.keys(storeIn.getState()).forEach((key) => {
const selector = (state: StateType) => state[key as keyof StateType];
storeIn.use[key] = () => storeIn(selector);
});

return store as UseBoundStore<StoreApi<StateType>> &
ZustandFuncSelectors<StateType>;
}
23 changes: 23 additions & 0 deletions src/createSelectorHooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { StoreApi, UseBoundStore } from 'zustand';

export type ZustandHookSelectors<StateType> = {
[Key in keyof StateType as `use${Capitalize<
string & Key
>}`]: () => StateType[Key];
};

const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);

export function createSelectorHooks<StateType extends object>(
store: UseBoundStore<StoreApi<StateType>>
) {
const storeIn = store as any;

Object.keys(storeIn.getState()).forEach((key) => {
const selector = (state: StateType) => state[key as keyof StateType];
storeIn[`use${capitalize(key)}`] = () => storeIn(selector);
});

return storeIn as UseBoundStore<StoreApi<StateType>> &
ZustandHookSelectors<StateType>;
}
44 changes: 2 additions & 42 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,2 @@
import { State, UseStore } from 'zustand';

interface Selector<StoreType> {
use: {
[key in keyof StoreType]: () => StoreType[key];
};
}

type Hook<BaseType> = {
[Key in keyof BaseType as `use${Capitalize<
string & Key
>}`]: () => BaseType[Key];
};

const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);

export function createSelectorFunctions<StoreType extends State>(
store: UseStore<StoreType>
) {
(store as any).use = {};

Object.keys(store.getState()).forEach((key) => {
const selector = (state: StoreType) => state[key as keyof StoreType];
(store as any).use[key] = () => store(selector);
});

return store as UseStore<StoreType> & Selector<StoreType>;
}

export function createSelectorHooks<StoreType extends State>(
store: UseStore<StoreType>
) {
(store as any).use = {};

Object.keys(store.getState()).forEach((key) => {
const selector = (state: StoreType) => state[key as keyof StoreType];
// @ts-ignore
store[`use${capitalize(key)}`] = () => store(selector);
});

return store as UseStore<StoreType> & Hook<StoreType>;
}
export * from './createSelectorFunctions';
export * from './createSelectorHooks';
59 changes: 4 additions & 55 deletions tests/index.test.tsx → tests/createSelectorFunctions.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import create from 'zustand';
import { createSelectorFunctions, createSelectorHooks } from '../src/index';
import { createSelectorFunctions } from '../src/index';
import { render, fireEvent } from '@testing-library/react';

interface BearState {
Expand All @@ -17,6 +17,9 @@ describe('Test createSelectorFunctions', () => {

const useStore = createSelectorFunctions(useStoreBase);

expect(typeof useStore.subscribe).toBe('function');
expect(typeof useStore.setState).toBe('function');
expect(typeof useStore.getState).toBe('function');
expect(typeof useStore.use.bears).toBe('function');
expect(typeof useStore.use.increase).toBe('function');
});
Expand Down Expand Up @@ -61,57 +64,3 @@ describe('Test createSelectorFunctions', () => {
expect(getByTestId('text').textContent).toBe('2');
});
});

describe('Test createSelectorHooks', () => {
it('should create functions for both property and action', () => {
const useStoreBase = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}));

const useStore = createSelectorHooks(useStoreBase);

expect(typeof useStore.useBears).toBe('function');
expect(typeof useStore.useIncrease).toBe('function');
});

it('should function correct when rendering in React.js', () => {
const useStoreBase = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}));

const useStore = createSelectorHooks(useStoreBase);

const TestComponent = () => {
const bears = useStore.useBears();
const increase = useStore.useIncrease();

return (
<>
<span data-testid="text">{bears}</span>
<button
data-testid="button"
onClick={() => {
increase(1);
}}
>
increase
</button>
</>
);
};

const { getByTestId } = render(<TestComponent />);

expect(getByTestId('text').textContent).toBe('0');

fireEvent.click(getByTestId('button'));

expect(getByTestId('text').textContent).toBe('1');

fireEvent.click(getByTestId('button'));

expect(getByTestId('text').textContent).toBe('2');
});
});
Loading

0 comments on commit 338338e

Please sign in to comment.