Skip to content

Commit 97d343c

Browse files
authored
Add setStore proxy (#79)
* Add setStore proxy * Add setStore types * Write docs * Add another example on docs * fix example * refactor example * Amplify setStore docu * fix * fix * fix
1 parent 3815021 commit 97d343c

File tree

5 files changed

+208
-15
lines changed

5 files changed

+208
-15
lines changed

README.md

+78-9
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ _Tiny, easy and powerful **React state management** library_
4141
- [How to export](#how-to-export)
4242
- [3. Manage the store 🕹](#manage-the-store-)
4343
- [useStore hook](#usestore-hook)
44+
- [setStore helper](#setstore-helper)
4445
- [getStore helper](#getstore-helper)
4546
- [withStore HoC](#withstore-hoc)
4647
- [4. Register events after an update 🚦](#register-events-after-an-update-)
@@ -111,6 +112,7 @@ _Output:_
111112
| ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
112113
| `useStore` | `Proxy` | Proxy hook to consume and update store properties inside your components. Each time the value changes, the component is rendered again with the new value. More [info](#usestore-hook). | `const [price, setPrice] = useStore.cart.price()` |
113114
| `getStore` | `Proxy` | Similar to `useStore` but without subscription. You can use it as a helper outside (or inside) components. Note that if the value changes, it does not cause a rerender. More [info](#getstore-helper). | `const [price, setPrice] = getStore.cart.price()` |
115+
| `setStore` | `Proxy` | It's a proxy helper to modify a store property outside (or inside) components. More [info](#setstore-helper). | `setStore.user.name('Aral')` or `setStore.cart.price(price => price + 10)` |
114116
| `withStore` | `Proxy` | HoC with `useStore` inside. Useful for components that are not functional. More [info](#withstore-hoc). | `withStore.cart.price(MyComponent)` |
115117

116118
### How to export
@@ -231,6 +233,75 @@ Is an `Array` with **2** items:
231233
| update value | `function` | Function to update the store property indicated with the proxy. | Updating a store portion:<div>`const [count, setCount] = useStore.count(0)`</div>Way 1:<div>`setCount(count + 1)`</div>Way 1:<div>`setCount(c => c + 1)`</div><div>-------</div>Updating all store:<div>`const [store, updateStore] = useStore()`</div>Way 1:<div>`updateStore({ ...store, count: 2 }))`</div>Way 1:<div>`updateStore(s => ({ ...s, count: 2 }))`</div> |
232234

233235

236+
### setStore helper
237+
238+
Useful helper to modify the store from anywhere (outside/inside components).
239+
240+
Example:
241+
242+
```js
243+
const initialStore = { count: 0, name: 'Aral' }
244+
const { setStore } = createStore(initialStore);
245+
246+
const resetStore = () => setStore(initialStore);
247+
const resetCount = () => setStore.count(initialStore.count);
248+
const resetName = () => setStore.name(initialStore.name);
249+
250+
// Component without any re-render (without useStore hook)
251+
function Resets() {
252+
return (
253+
<>
254+
<button onClick={resetStore}>
255+
Reset store
256+
</button>
257+
<button onClick={resetCount}>
258+
Reset count
259+
</button>
260+
<button onClick={resetName}>
261+
Reset name
262+
</button>
263+
</>
264+
);
265+
}
266+
```
267+
268+
Another example:
269+
270+
```js
271+
const { useStore, setStore } = createStore({
272+
firstName: '',
273+
lastName: ''
274+
});
275+
276+
function ExampleOfForm() {
277+
const [formFields] = useStore()
278+
279+
return Object.entries(formFields).map(([key, value]) => (
280+
<input
281+
defaultValue={value}
282+
type="text"
283+
key={key}
284+
onChange={e => {
285+
// Update depending the key attribute
286+
setStore[key](e.target.value)
287+
}}
288+
/>
289+
))
290+
}
291+
```
292+
293+
This second example only causes re-renders in the components that consume the property that has been modified.
294+
295+
In this way:
296+
297+
```js
298+
const [formFields, setFormFields] = useStore()
299+
// ...
300+
setFormFields(s => ({ ...s, [key]: e.target.value })) //
301+
```
302+
303+
This causes a re-render on all components that are consuming any of the form properties, instead of just the one that has been updated. So using the `setStore` proxy helper is more recommended.
304+
234305
### getStore helper
235306

236307
It works exactly like `useStore` but with **some differences**:
@@ -515,12 +586,11 @@ setStore({ count: 10, username: "" });
515586
If you have to update several properties and you don't want to disturb the rest of the components that are using other store properties you can create a helper with `getStore`.
516587

517588
```js
518-
export const { useStore, getStore } = createStore(initialStore);
589+
export const { useStore, setStore } = createStore(initialStore);
519590

520-
export function setStore(fields) {
521-
Object.keys(fields).forEach((key) => {
522-
const setStoreField = getStore[key]()[1];
523-
setStoreField(fields[key]);
591+
export function setFragmentedStore(fields) {
592+
Object.entries(fields).forEach(([key, value]) => {
593+
setStore[key](value);
524594
});
525595
}
526596
```
@@ -537,12 +607,12 @@ setStore({ count: 10, username: "" });
537607

538608
### Define calculated properties
539609

540-
It's possible to use the `getStore` together with the function that is executed after each update to have store properties calculated from others.
610+
It's possible to use the `setStore` together with the function that is executed after each update to have store properties calculated from others.
541611

542612
In this example the cart price value will always be a value calculated according to the array of items:
543613

544614
```js
545-
export const { useStore, getStore } = createStore(
615+
export const { useStore, setStore } = createStore(
546616
{
547617
cart: {
548618
price: 0,
@@ -558,8 +628,7 @@ function onAfterUpdate({ store }) {
558628

559629
// Price always will be items.length * 3
560630
if (price !== calculatedPrice) {
561-
const [, setPrice] = getStore.cart.price();
562-
setPrice(calculatedPrice);
631+
setStore.cart.price(calculatedPrice);
563632
}
564633
}
565634
```

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "teaful",
3-
"version": "0.9.2",
3+
"version": "0.10.0",
44
"description": "Tiny, easy and powerful React state management (less than 1kb)",
55
"license": "MIT",
66
"keywords": [

package/index.d.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ declare module "teaful" {
22

33
import React from "react";
44

5-
type HookReturn<T> = [T, (value: T | ((value: T) => T | undefined | null) ) => void];
5+
type setter<T> = (value?: T | ((value: T) => T | undefined | null) ) => void;
6+
type HookReturn<T> = [T, setter<T>];
67
type initialStoreType = Record<string, any>;
78

89
type Hook<S> = (
@@ -35,6 +36,11 @@ declare module "teaful" {
3536
? useStoreType<S[key]> & Hook<S[key]> : Hook<S[key]>;
3637
};
3738

39+
type setStoreType<S extends initialStoreType> = {
40+
[key in keyof S]: S[key] extends initialStoreType
41+
? setStoreType<S[key]> & setter<S[key]> : setter<S[key]>;
42+
};
43+
3844
type withStoreType<S extends initialStoreType> = {
3945
[key in keyof S]: S[key] extends initialStoreType
4046
? withStoreType<S[key]> & HocFunc<S>
@@ -47,6 +53,7 @@ declare module "teaful" {
4753
): {
4854
getStore: HookDry<S> & getStoreType<S>;
4955
useStore: Hook<S> & useStoreType<S>;
56+
setStore: setter<S> & setStoreType<S>;
5057
withStore: HocFunc<S> & withStoreType<S>;
5158
};
5259

package/index.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {useEffect, useReducer, createElement} from 'react';
33
let MODE_GET = 1;
44
let MODE_USE = 2;
55
let MODE_WITH = 3;
6+
let MODE_SET = 4;
67
let DOT = '.';
78
let extras = [];
89

@@ -58,9 +59,11 @@ export default function createStore(defaultStore = {}, callback) {
5859
//
5960
// MODE_GET: let [store, update] = useStore()
6061
// MODE_USE: let [store, update] = getStore()
62+
// MODE_SET: setStore({ newStore: true })
6163
if (!path.length) {
6264
let updateAll = updateField();
6365
if (mode === MODE_USE) useSubscription(DOT, callback);
66+
if (mode === MODE_SET) return updateAll(param);
6467
return [allStore, updateAll];
6568
}
6669

@@ -72,6 +75,9 @@ export default function createStore(defaultStore = {}, callback) {
7275
let value = getField(prop);
7376
let initializeValue = param !== undefined && !existProperty(path);
7477

78+
// MODE_SET: setStore.cart.price(10)
79+
if (mode === MODE_SET) return update(param);
80+
7581
if (initializeValue) {
7682
value = param;
7783
allStore = setField(allStore, path, value);
@@ -90,9 +96,11 @@ export default function createStore(defaultStore = {}, callback) {
9096
return [value, update];
9197
},
9298
};
93-
let useStore = new Proxy(() => MODE_USE, validator);
94-
let getStore = new Proxy(() => MODE_GET, validator);
95-
let withStore = new Proxy(() => MODE_WITH, validator);
99+
let createProxy = (mode) => new Proxy(() => mode, validator);
100+
let useStore = createProxy(MODE_USE);
101+
let getStore = createProxy(MODE_GET);
102+
let withStore = createProxy(MODE_WITH);
103+
let setStore = createProxy(MODE_SET);
96104

97105
/**
98106
* Hook to register a listener to force a render when the
@@ -165,7 +173,7 @@ export default function createStore(defaultStore = {}, callback) {
165173
let result = extras.reduce((res, fn) => {
166174
let newRes = fn(res, subscription);
167175
return typeof newRes === 'object' ? {...res, ...newRes} : res;
168-
}, {useStore, getStore, withStore});
176+
}, {useStore, getStore, withStore, setStore});
169177

170178
/**
171179
* createStore function returns:

tests/setStore.test.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {render, screen} from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
4+
import '@babel/polyfill';
5+
6+
import createStore from '../package/index';
7+
8+
describe('setStore', () => {
9+
it('should avoid rerenders on component that use the setStore', () => {
10+
const renderCart = jest.fn();
11+
const renderOther = jest.fn();
12+
const renderUpdateProps = jest.fn();
13+
14+
const {useStore, setStore} = createStore({cart: {price: 0}, name: 'Aral', count: 0});
15+
16+
function UpdateProps() {
17+
renderUpdateProps();
18+
return (
19+
<button data-testid="click" onClick={() => {
20+
setStore.name('ARAL');
21+
setStore.count(10);
22+
}}
23+
/>
24+
);
25+
}
26+
27+
function Cart() {
28+
const [cart] = useStore.cart();
29+
renderCart();
30+
return <div data-testid="price">{cart.price}</div>;
31+
}
32+
33+
function Other() {
34+
const [name] = useStore.name();
35+
const [count] = useStore.count();
36+
renderOther();
37+
return <div data-testid="other">{name} {count}</div>;
38+
}
39+
40+
render(
41+
<>
42+
<Cart />
43+
<Other />
44+
<UpdateProps />
45+
</>,
46+
);
47+
48+
expect(renderCart).toHaveBeenCalledTimes(1);
49+
expect(renderOther).toHaveBeenCalledTimes(1);
50+
expect(renderUpdateProps).toHaveBeenCalledTimes(1);
51+
expect(screen.getByTestId('price').textContent).toContain('0');
52+
expect(screen.getByTestId('other').textContent).toContain('Aral 0');
53+
54+
userEvent.click(screen.getByTestId('click'));
55+
expect(renderUpdateProps).toHaveBeenCalledTimes(1);
56+
expect(renderCart).toHaveBeenCalledTimes(1);
57+
expect(renderOther).toHaveBeenCalledTimes(2);
58+
expect(screen.getByTestId('price').textContent).toContain('0');
59+
expect(screen.getByTestId('other').textContent).toContain('ARAL 10');
60+
});
61+
62+
it('Update serveral portions should avoid rerenders in the rest', () => {
63+
const renderCart = jest.fn();
64+
const renderOther = jest.fn();
65+
const {useStore, setStore} = createStore({cart: {price: 0}, name: 'Aral', count: 0});
66+
67+
function setFragmentedStore(fields) {
68+
Object.keys(fields).forEach((key) => {
69+
setStore[key](fields[key]);
70+
});
71+
}
72+
73+
function UpdateProps() {
74+
return <button data-testid="click" onClick={() => setFragmentedStore({name: 'ARAL', count: 10})} />;
75+
}
76+
77+
function Cart() {
78+
const [cart] = useStore.cart();
79+
renderCart();
80+
return <div data-testid="price">{cart.price}</div>;
81+
}
82+
83+
function Other() {
84+
const [name] = useStore.name();
85+
const [count] = useStore.count();
86+
renderOther();
87+
return <div data-testid="other">{name} {count}</div>;
88+
}
89+
90+
render(
91+
<>
92+
<Cart />
93+
<Other />
94+
<UpdateProps />
95+
</>,
96+
);
97+
98+
expect(renderCart).toHaveBeenCalledTimes(1);
99+
expect(renderOther).toHaveBeenCalledTimes(1);
100+
expect(screen.getByTestId('price').textContent).toContain('0');
101+
expect(screen.getByTestId('other').textContent).toContain('Aral 0');
102+
103+
userEvent.click(screen.getByTestId('click'));
104+
expect(renderCart).toHaveBeenCalledTimes(1);
105+
expect(renderOther).toHaveBeenCalledTimes(2);
106+
expect(screen.getByTestId('price').textContent).toContain('0');
107+
expect(screen.getByTestId('other').textContent).toContain('ARAL 10');
108+
});
109+
});

0 commit comments

Comments
 (0)