react-use-sub
Advanced tools
Comparing version 2.2.2 to 3.0.0-alpha.0
@@ -6,3 +6,2 @@ 'use strict'; | ||
var react = require('react'); | ||
var reactDom = require('react-dom'); | ||
@@ -27,5 +26,9 @@ function _extends() { | ||
let timeout = undefined; | ||
const _config = { | ||
enqueue: fn => setTimeout(fn, 0), | ||
batch: reactDom.unstable_batchedUpdates | ||
dispatch: fn => setTimeout(fn, 0), | ||
batch: fn => { | ||
clearTimeout(timeout); | ||
timeout = setTimeout(fn, 0); | ||
} | ||
}; | ||
@@ -48,13 +51,2 @@ | ||
const _dispatch = D => _config.batch(() => { | ||
D.subs.forEach(sub => { | ||
const next = sub.mapper(D.data); | ||
if (_diff(next, sub.last)) { | ||
sub.last = next; | ||
sub.update(); | ||
} | ||
}); | ||
}); | ||
const _update = (D, next) => { | ||
@@ -77,22 +69,24 @@ const result = _extends({}, D.data); | ||
_config.enqueue(() => _dispatch(D)); | ||
_config.batch(() => { | ||
D.subs.forEach(listener => listener()); | ||
}); | ||
} | ||
}, | ||
listen: (mapper, listener) => { | ||
const sub = { | ||
mapper, | ||
last: mapper(D.data) | ||
}; | ||
let thisLast = sub.last; | ||
let last = mapper(D.data); | ||
sub.update = () => { | ||
_config.enqueue(() => { | ||
listener(sub.last, thisLast); | ||
thisLast = sub.last; | ||
}); | ||
const l = () => { | ||
const next = mapper(D.data); | ||
const prev = last; | ||
if (_diff(next, prev)) { | ||
_config.dispatch(() => listener(next, prev)); | ||
last = next; | ||
} | ||
}; | ||
D.subs.add(sub); | ||
D.subs.add(l); | ||
return () => { | ||
D.subs.delete(sub); | ||
D.subs.delete(l); | ||
}; | ||
@@ -102,15 +96,10 @@ } | ||
const _emptyDeps = []; | ||
const _toggle = b => !b; | ||
const useUpdate = () => { | ||
const setBool = react.useState(true)[1]; | ||
return react.useCallback(() => setBool(_toggle), []); | ||
}; | ||
const createStore = data => { | ||
const D = { | ||
data, | ||
subs: new Set() | ||
subs: new Set(), | ||
subscribe: listener => { | ||
D.subs.add(listener); | ||
return () => D.subs.delete(listener); | ||
} | ||
}; | ||
@@ -120,24 +109,13 @@ | ||
const useSub = (mapper, deps = _emptyDeps) => { | ||
const lastDeps = react.useRef(deps); | ||
const update = useUpdate(); | ||
const sub = react.useRef({ | ||
mapper, | ||
update, | ||
last: mapper(D.data) | ||
}); | ||
const useSub = mapper => { | ||
const resultRef = react.useRef(mapper(D.data)); | ||
return react.useSyncExternalStore(D.subscribe, () => { | ||
const next = mapper(D.data); | ||
if (_diffArr(lastDeps.current, deps)) { | ||
sub.current.mapper = mapper; | ||
sub.current.last = mapper(D.data); | ||
} | ||
if (_diff(next, resultRef.current)) { | ||
resultRef.current = next; | ||
} | ||
lastDeps.current = deps; | ||
react.useEffect(() => { | ||
D.subs.add(sub.current); | ||
return () => { | ||
D.subs.delete(sub.current); | ||
}; | ||
}, []); | ||
return sub.current.last; | ||
return resultRef.current; | ||
}); | ||
}; | ||
@@ -144,0 +122,0 @@ |
@@ -1,3 +0,2 @@ | ||
import { useRef, useCallback, useEffect, useState } from 'react'; | ||
import { unstable_batchedUpdates } from 'react-dom'; | ||
import { useRef, useSyncExternalStore } from 'react'; | ||
@@ -22,5 +21,9 @@ function _extends() { | ||
let timeout = undefined; | ||
const _config = { | ||
enqueue: fn => setTimeout(fn, 0), | ||
batch: unstable_batchedUpdates | ||
dispatch: fn => setTimeout(fn, 0), | ||
batch: fn => { | ||
clearTimeout(timeout); | ||
timeout = setTimeout(fn, 0); | ||
} | ||
}; | ||
@@ -43,13 +46,2 @@ | ||
const _dispatch = D => _config.batch(() => { | ||
D.subs.forEach(sub => { | ||
const next = sub.mapper(D.data); | ||
if (_diff(next, sub.last)) { | ||
sub.last = next; | ||
sub.update(); | ||
} | ||
}); | ||
}); | ||
const _update = (D, next) => { | ||
@@ -72,22 +64,24 @@ const result = _extends({}, D.data); | ||
_config.enqueue(() => _dispatch(D)); | ||
_config.batch(() => { | ||
D.subs.forEach(listener => listener()); | ||
}); | ||
} | ||
}, | ||
listen: (mapper, listener) => { | ||
const sub = { | ||
mapper, | ||
last: mapper(D.data) | ||
}; | ||
let thisLast = sub.last; | ||
let last = mapper(D.data); | ||
sub.update = () => { | ||
_config.enqueue(() => { | ||
listener(sub.last, thisLast); | ||
thisLast = sub.last; | ||
}); | ||
const l = () => { | ||
const next = mapper(D.data); | ||
const prev = last; | ||
if (_diff(next, prev)) { | ||
_config.dispatch(() => listener(next, prev)); | ||
last = next; | ||
} | ||
}; | ||
D.subs.add(sub); | ||
D.subs.add(l); | ||
return () => { | ||
D.subs.delete(sub); | ||
D.subs.delete(l); | ||
}; | ||
@@ -97,15 +91,10 @@ } | ||
const _emptyDeps = []; | ||
const _toggle = b => !b; | ||
const useUpdate = () => { | ||
const setBool = useState(true)[1]; | ||
return useCallback(() => setBool(_toggle), []); | ||
}; | ||
const createStore = data => { | ||
const D = { | ||
data, | ||
subs: new Set() | ||
subs: new Set(), | ||
subscribe: listener => { | ||
D.subs.add(listener); | ||
return () => D.subs.delete(listener); | ||
} | ||
}; | ||
@@ -115,24 +104,13 @@ | ||
const useSub = (mapper, deps = _emptyDeps) => { | ||
const lastDeps = useRef(deps); | ||
const update = useUpdate(); | ||
const sub = useRef({ | ||
mapper, | ||
update, | ||
last: mapper(D.data) | ||
}); | ||
const useSub = mapper => { | ||
const resultRef = useRef(mapper(D.data)); | ||
return useSyncExternalStore(D.subscribe, () => { | ||
const next = mapper(D.data); | ||
if (_diffArr(lastDeps.current, deps)) { | ||
sub.current.mapper = mapper; | ||
sub.current.last = mapper(D.data); | ||
} | ||
if (_diff(next, resultRef.current)) { | ||
resultRef.current = next; | ||
} | ||
lastDeps.current = deps; | ||
useEffect(() => { | ||
D.subs.add(sub.current); | ||
return () => { | ||
D.subs.delete(sub.current); | ||
}; | ||
}, []); | ||
return sub.current.last; | ||
return resultRef.current; | ||
}); | ||
}; | ||
@@ -139,0 +117,0 @@ |
/// <reference types="node" /> | ||
import { unstable_batchedUpdates } from 'react-dom'; | ||
export declare const _config: { | ||
enqueue: (fn: () => void) => NodeJS.Timeout; | ||
batch: typeof unstable_batchedUpdates; | ||
dispatch: (fn: () => void) => NodeJS.Timeout; | ||
batch: (fn: () => void) => void; | ||
}; | ||
declare type Mapper<DATA, OP> = (state: DATA) => OP; | ||
export declare type UseSubType<DATA> = <OP>(mapper: Mapper<DATA, OP>, deps?: ReadonlyArray<unknown>) => OP; | ||
export declare type UseSubType<DATA> = <OP>(mapper: Mapper<DATA, OP>) => OP; | ||
export declare type StoreSetArg<DATA, K extends keyof DATA> = Pick<DATA, K> | undefined | ((prev: DATA) => Pick<DATA, K> | undefined); | ||
@@ -10,0 +9,0 @@ export declare type StoreSet<DATA> = <K extends keyof DATA>(update: StoreSetArg<DATA, K>) => void; |
{ | ||
"name": "react-use-sub", | ||
"version": "2.2.2", | ||
"version": "3.0.0-alpha.0", | ||
"description": "Subscription based lightweight React store", | ||
@@ -30,7 +30,7 @@ "engines": { | ||
"peerDependencies": { | ||
"react": ">= 16.8.0 < 18", | ||
"react-dom": ">= 16.8.0 < 18" | ||
"react": ">= 18.0.0", | ||
"react-dom": ">= 18.0.0" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.17.3", | ||
"@babel/core": "^7.17.9", | ||
"@babel/plugin-proposal-class-properties": "^7.16.7", | ||
@@ -40,22 +40,22 @@ "@babel/preset-env": "^7.16.11", | ||
"@babel/preset-typescript": "^7.16.7", | ||
"@rollup/plugin-babel": "^5.3.0", | ||
"@rollup/plugin-babel": "^5.3.1", | ||
"@rollup/plugin-node-resolve": "^13.1.3", | ||
"@testing-library/react": "^12.1.2", | ||
"@types/jest": "^27.4.0", | ||
"@types/react": "^17.0.39", | ||
"@types/react-dom": "^17.0.11", | ||
"@typescript-eslint/eslint-plugin": "^5.12.0", | ||
"@typescript-eslint/parser": "^5.12.0", | ||
"@testing-library/react": "^13.0.0", | ||
"@types/jest": "^27.4.1", | ||
"@types/react": "^18.0.0", | ||
"@types/react-dom": "^18.0.0", | ||
"@typescript-eslint/eslint-plugin": "^5.18.0", | ||
"@typescript-eslint/parser": "^5.18.0", | ||
"babel-jest": "^27.5.1", | ||
"coveralls": "^3.1.1", | ||
"eslint": "^8.9.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint": "^8.13.0", | ||
"eslint-config-prettier": "^8.5.0", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"eslint-plugin-react": "^7.28.0", | ||
"eslint-plugin-react": "^7.29.4", | ||
"jest": "^27.5.1", | ||
"prettier": "^2.5.1", | ||
"react": "^17.0.2", | ||
"react-dom": "^17.0.2", | ||
"rollup": "^2.67.2", | ||
"typescript": "^4.5.5" | ||
"prettier": "^2.6.2", | ||
"react": "^18.0.0", | ||
"react-dom": "^18.0.0", | ||
"rollup": "^2.70.1", | ||
"typescript": "^4.6.3" | ||
}, | ||
@@ -69,3 +69,3 @@ "scripts": { | ||
}, | ||
"readme": "[![GitHub license][license-image]][license-url]\n[![npm package][npm-image]][npm-url] \n[![Travis][build-image]][build-url]\n[![Coverage Status][coveralls-image]][coveralls-url]\n[![styled with prettier][prettier-image]][prettier-url]\n\n# react-use-sub\n\nSubscription based lightweight React store.\n\n### Benefits\n- easy to use\n- easy testing\n- no dependencies\n- no react context\n- TypeScript support included\n- Very small package size ([< 1kB gzipped](https://bundlephobia.com/result?p=react-use-sub))\n- Much better performance than react-redux\n- works with SSR\n\n### Examples\n```tsx\n// >>> in your store.js\nimport { createStore } from 'react-use-sub';\n\nconst initialState = { foo: 'bar', num: 2 };\nexport const [useSub, Store] = createStore(initialState);\n\n// >>> in any component\nimport { useSub } from '/path/to/store.js';\n\nexport const App = () => {\n // subscribe here your custom store mapper\n const { fooLength, num } = useSub(({ foo, num }) => ({ fooLength: foo.length, num }));\n const square = useSub(({ num }) => num**2);\n \n return <div>Magic number is: {fooLength * num * square}</div>;\n}\n\n// >>> in any other (or same) place\nimport { Store } from '/path/to/store.js';\n\n// signature (almost) equally to the Setter function of useState\nStore.set({ foo: 'something' });\n// or functional\nStore.set(({ foo }) => ({ foo: foo + '_2' }));\n// this updates the stored data\n// and updates all components that would be passed\n// different values from the subscribed store mapper\nexpect(Store.get()).toEqual({ foo: 'something_2', num: 2 });\n\n// or listen to any changes\n// all below mentioned optimizations listed for \"useSub\" apply also to these listeners\nconst removeListener = Store.listen(({ foo }) => foo, (nextFoo, prevFoo) => {\n // will be only called if \"nextFoo !== prevFoo\" so you don't need to check this\n if (nextFoo.length > prevFoo.length) {\n alert('foo is growing');\n }\n});\n// and you can unsubscribe by calling the returned callback\nremoveListener();\n```\n\n## Hints\nLet me introduce you to some interesting things.\n### Optional types\nSince version [2.0.0](https://github.com/fdc-viktor-luft/react-use-sub/blob/master/CHANGELOG.md#200---2021-03-21) you\ncan simply specify the optional type you want.\n```ts\ntype State = { lastVisit?: Date };\ntype State = { lastVisit: null | Date };\n```\n\n### Conditional updates\nWhen calling `Store.set` with `undefined` or a function that returns `undefined` has\nno effect and performs no update.\n```ts\n// only update the stock if articles are present\nStore.set(articles.length ? { stock: articles.length } : undefined);\n// but this easy example could/should be rewritten to\narticles.length && Store.set({ stock: articles.length });\n\n// this feature comes more handy in examples like this\nStore.set(({ articles }) => (articles.length ? { stock: articles.length } : undefined));\n// or equivalent\nStore.set(({ articles }) => {\n if (articles.length) {\n return { stock: articles.length };\n }\n});\n```\n\n### Subscription with dependencies\nSometimes you may want to subscribe your component to state that depends\non additional component state. This can be accomplished with the typical\ndependency array most of us got used to with most basic React hooks.\n```tsx\nexport const FancyItem: React.FC<{ id: string }> = ({ id }) => {\n const { name, color } = useSub(({ items }) => items[id], [id]);\n \n return <div style={{ color }}>{name}</div>;\n}\n```\nBut you shouldn't provide an empty array as second argument to `useSub`,\nsince internal optimizations make this the default.\n\n### Shallow equality optimization\nThe returned value of the defined mapper will be compared shallowly against\nthe next computed value to determine if some rerender is necessary. E.g.\nfollowing the example of the `App` component above:\n```ts\n// if Store.get().foo === 'bar'\nStore.set({ foo: '123' });\n// --> no rerender since \"foo.length\" did not change\n\n// if Store.get().num === 3\nStore.set({ num: 3 });\n// --> no rerender since \"num\" did not change\n```\n\n### Multiple subscriptions in a single component\nPlease feel free to use multiple subscriptions in a single component.\n```tsx\nexport const GreatArticle = () => {\n const { id, author, title } = useSub(({ article }) => article);\n const reviews = useSub(({ reviews }) => reviews);\n const [trailer, recommendation] = useSub(({ trailers, recommendations }) => [trailer[id], recommendations[id]], [id]);\n \n return <>...</>;\n}\n```\nWhenever a store update would trigger any of the above subscriptions the\ncomponent will be rerendered only once even if all subscriptions would\nreturn different data. That's a pretty important capability when thinking\nabout custom hooks that subscribe to certain states.\n\n### Multiple store updates\nIf you perform multiple store updates in the same synchronous task this\nhas (almost) no negative impact on your performance and leads not to any\nunnecessary rerenders. All updates will be enqueued, processed in the next\ntick and batched to minimize the necessary rerenders.\n```ts\nStore.set({ foo: 'bar' });\nStore.set({ num: 2 });\nStore.set({ lastVisit: new Date() });\n```\n\n### Multiple stores\nYou can instantiate as many stores as you like, but make sure you don't create\nyour own hell with too many convoluted stores to subscribe.\n```ts\nimport { createStore } from 'react-use-sub';\n\nexport const [useArticleSub, ArticleStore] = createStore(initialArticleState);\n\nexport const [useCustomerSub, CustomerStore] = createStore(initialCustomerState);\n\nexport const [useEventSub, EventStore] = createStore(initialEventState);\n```\n\n### Improve IDE auto-import\nIf you're exporting `useSub` and `Store` like mentioned in the\nexample above, your IDE most likely doesn't suggest importing those\nwhile typing inside some component. To achieve this you could do the\nfollowing special trick.\n```ts\nconst [useSub, Store] = createStore(initialState);\n\nexport { useSub, Store };\n```\n\n### Persisting data on the client\nBecause of the simplicity of this library, there are various ways how to persist data. One\nexample could be a custom hook persisting into the local storage.\n\n```ts\nconst usePersistArticles = () => {\n const articles = useSub(({ articles }) => articles);\n\n useEffect(() => {\n localStorage.setItem('articles', JSON.stringify(articles));\n }, [articles]);\n};\n\n// and if you want to initialize your store with this data on page reload\nconst localStorageArticles = localStorage.getItem('articles');\nconst initialState = {\n articles: localStorageArticles ? JSON.parse(localStorageArticles) : {},\n}\n\nconst [useSub, Store] = createStore(initialState);\n```\n\nYou can also initialize the data lazy inside another effect of the custom hook. You can\nuse `IndexedDB` if you need to store objects that are not lossless serializable to JSON.\nYou can use `sessionStorage` or `cookies` depending on your use case. No limitations.\n\n### Middlewares\nIt's totally up to you to write any sorts of middleware. One example of tracking special\nstate updates:\n```ts\nimport { createStore, StoreSet } from 'react-use-sub';\n\ntype State = { conversionStep: number };\nconst initialState: State = { conversionStep: 1 };\n\nconst [useSub, _store] = createStore<State>(initialState);\n\n// here comes the middleware implementation\nconst set: StoreSet<State> = (update) => {\n const prevState = _store.get();\n _store.set(update);\n const state = _store.get();\n if (prevState.conversionStep !== state.conversionStep) {\n trackConversionStep(state.conversionStep)\n }\n}\n\n// you can also add a reset functionality for Store which is very convenient for logouts\n// with or without tracking. It's all up to you.\nconst Store = { ..._store, set, reset: () => _store.set(initialState) };\n\nexport { useSub, Store };\n```\nYes, I know, it's basically just a higher order function. But let's keep things simple.\n\n\n### Testing\nYou don't need to mock any functions in order to test the integration of\nthe store. There is \"test-util\" that will improve your testing experience a lot.\nThe only thing you need to do is importing it. E.g. by putting it into your \"setupTests\" file.\n```ts\nimport 'react-use-sub/test-util';\n```\nPossible downsides: Some optimizations like batched processing of all updates will be disabled.\nYou won't notice the performance impact in your tests, but you should not relay on the number\nof renders caused by the store.\n\nTesting would look like this\n```tsx\n// in some component\nexport const MyExample: React.FC = () => {\n const stock = useSub(({ article: { stock } }) => stock);\n\n return <span>Article stock is: {stock}</span>;\n};\n\ndescribe('<MyExample />', () => {\n it('renders the stock', () => {\n // initialization\n // feel free to use any-casts in your tests (but only there)\n Store.set({ article: { stock: 1337 } as any });\n\n // render with stock 1337 (see '@testing-library/react')\n const { container } = render(<MyExample />);\n expect(container.textContent).toBe('Article stock is: 1337');\n\n // update the stock (not need to wrap into \"act\", it's already done for you)\n Store.set({ article: { stock: 444 } as any });\n expect(container.textContent).toBe('Article stock is: 444');\n });\n});\n```\n\n### Testing (without \"test-util\")\nYou can use the store as is, but you will need to run all timers with jest because \nall updates of components are processed batched. The above test would become:\n```tsx\ndescribe('<MyExample />', () => {\n it('renders the stock', () => {\n jest.useFakeTimers();\n // initialization\n // feel free to use any-casts in your tests (but only there)\n Store.set({ article: { stock: 1337 } as any });\n\n // render with stock 1337 (see '@testing-library/react')\n const { container } = render(<MyExample />);\n expect(container.textContent).toBe('Article stock is: 1337');\n\n // update the stock\n Store.set({ article: { stock: 444 } as any });\n jest.runAllTimers();\n expect(container.textContent).toBe('Article stock is: 444');\n });\n});\n```\n\n[license-image]: https://img.shields.io/badge/license-MIT-blue.svg\n[license-url]: https://github.com/fdc-viktor-luft/react-use-sub/blob/master/LICENSE\n[build-image]: https://img.shields.io/travis/fdc-viktor-luft/react-use-sub/master.svg?style=flat-square\n[build-url]: https://app.travis-ci.com/fdc-viktor-luft/react-use-sub\n[npm-image]: https://img.shields.io/npm/v/react-use-sub.svg?style=flat-square\n[npm-url]: https://www.npmjs.org/package/react-use-sub\n[coveralls-image]: https://coveralls.io/repos/github/fdc-viktor-luft/react-use-sub/badge.svg?branch=master\n[coveralls-url]: https://coveralls.io/github/fdc-viktor-luft/react-use-sub?branch=master\n[prettier-image]: https://img.shields.io/badge/styled_with-prettier-ff69b4.svg\n[prettier-url]: https://github.com/prettier/prettier\n" | ||
"readme": "[![GitHub license][license-image]][license-url]\n[![npm package][npm-image]][npm-url] \n[![Travis][build-image]][build-url]\n[![Coverage Status][coveralls-image]][coveralls-url]\n[![styled with prettier][prettier-image]][prettier-url]\n\n# react-use-sub\n\nSubscription based lightweight React store.\n\n### Benefits\n- easy to use\n- easy testing\n- no dependencies\n- no react context\n- TypeScript support included\n- Very small package size ([< 1kB gzipped](https://bundlephobia.com/result?p=react-use-sub))\n- Much better performance than react-redux\n- works with [SSR](#SSR)\n\n### Examples\n```tsx\n// >>> in your store.js\nimport { createStore } from 'react-use-sub';\n\nconst initialState = { foo: 'bar', num: 2 };\nexport const [useSub, Store] = createStore(initialState);\n\n// >>> in any component\nimport { useSub } from '/path/to/store.js';\n\nexport const App = () => {\n // subscribe here your custom store mapper\n const { fooLength, num } = useSub(({ foo, num }) => ({ fooLength: foo.length, num }));\n const square = useSub(({ num }) => num**2);\n \n return <div>Magic number is: {fooLength * num * square}</div>;\n}\n\n// >>> in any other (or same) place\nimport { Store } from '/path/to/store.js';\n\n// signature (almost) equally to the Setter function of useState\nStore.set({ foo: 'something' });\n// or functional\nStore.set(({ foo }) => ({ foo: foo + '_2' }));\n// this updates the stored data\n// and updates all components that would be passed\n// different values from the subscribed store mapper\nexpect(Store.get()).toEqual({ foo: 'something_2', num: 2 });\n\n// or listen to any changes\n// all below mentioned optimizations listed for \"useSub\" apply also to these listeners\nconst removeListener = Store.listen(({ foo }) => foo, (nextFoo, prevFoo) => {\n // will be only called if \"nextFoo !== prevFoo\" so you don't need to check this\n if (nextFoo.length > prevFoo.length) {\n alert('foo is growing');\n }\n});\n// and you can unsubscribe by calling the returned callback\nremoveListener();\n```\n\n## Hints\nLet me introduce you to some interesting things.\n### Optional types\nSince version [2.0.0](https://github.com/fdc-viktor-luft/react-use-sub/blob/master/CHANGELOG.md#200---2021-03-21) you\ncan simply specify the optional type you want.\n```ts\ntype State = { lastVisit?: Date };\ntype State = { lastVisit: null | Date };\n```\n\n### Conditional updates\nWhen calling `Store.set` with `undefined` or a function that returns `undefined` has\nno effect and performs no update.\n```ts\n// only update the stock if articles are present\nStore.set(articles.length ? { stock: articles.length } : undefined);\n// but this easy example could/should be rewritten to\narticles.length && Store.set({ stock: articles.length });\n\n// this feature comes more handy in examples like this\nStore.set(({ articles }) => (articles.length ? { stock: articles.length } : undefined));\n// or equivalent\nStore.set(({ articles }) => {\n if (articles.length) {\n return { stock: articles.length };\n }\n});\n```\n\n### Subscription with dependencies\nSometimes you may want to subscribe your component to state that depends\non additional component state. This can be accomplished with the typical\ndependency array most of us got used to with most basic React hooks.\n```tsx\nexport const FancyItem: React.VFC<{ id: string }> = ({ id }) => {\n const { name, color } = useSub(({ items }) => items[id], [id]);\n \n return <div style={{ color }}>{name}</div>;\n}\n```\nBut you shouldn't provide an empty array as second argument to `useSub`,\nsince internal optimizations make this the default.\n\n### Shallow equality optimization\nThe returned value of the defined mapper will be compared shallowly against\nthe next computed value to determine if some rerender is necessary. E.g.\nfollowing the example of the `App` component above:\n```ts\n// if Store.get().foo === 'bar'\nStore.set({ foo: '123' });\n// --> no rerender since \"foo.length\" did not change\n\n// if Store.get().num === 3\nStore.set({ num: 3 });\n// --> no rerender since \"num\" did not change\n```\n\n### Multiple subscriptions in a single component\nPlease feel free to use multiple subscriptions in a single component.\n```tsx\nexport const GreatArticle = () => {\n const { id, author, title } = useSub(({ article }) => article);\n const reviews = useSub(({ reviews }) => reviews);\n const [trailer, recommendation] = useSub(({ trailers, recommendations }) => [trailer[id], recommendations[id]], [id]);\n \n return <>...</>;\n}\n```\nWhenever a store update would trigger any of the above subscriptions the\ncomponent will be rerendered only once even if all subscriptions would\nreturn different data. That's a pretty important capability when thinking\nabout custom hooks that subscribe to certain states.\n\n### Multiple store updates\nIf you perform multiple store updates in the same synchronous task this\nhas (almost) no negative impact on your performance and leads not to any\nunnecessary rerenders. All updates will be enqueued, processed in the next\ntick and batched to minimize the necessary rerenders.\n```ts\nStore.set({ foo: 'bar' });\nStore.set({ num: 2 });\nStore.set({ lastVisit: new Date() });\n```\n\n### Multiple stores\nYou can instantiate as many stores as you like, but make sure you don't create\nyour own hell with too many convoluted stores to subscribe.\n```ts\nimport { createStore } from 'react-use-sub';\n\nexport const [useArticleSub, ArticleStore] = createStore(initialArticleState);\n\nexport const [useCustomerSub, CustomerStore] = createStore(initialCustomerState);\n\nexport const [useEventSub, EventStore] = createStore(initialEventState);\n```\n\n### Improve IDE auto-import\nIf you're exporting `useSub` and `Store` like mentioned in the\nexample above, your IDE most likely doesn't suggest importing those\nwhile typing inside some component. To achieve this you could do the\nfollowing special trick.\n```ts\nconst [useSub, Store] = createStore(initialState);\n\nexport { useSub, Store };\n```\n\n### Persisting data on the client\nBecause of the simplicity of this library, there are various ways how to persist data. One\nexample could be a custom hook persisting into the local storage.\n\n```ts\nconst usePersistArticles = () => {\n const articles = useSub(({ articles }) => articles);\n\n useEffect(() => {\n localStorage.setItem('articles', JSON.stringify(articles));\n }, [articles]);\n};\n\n// and if you want to initialize your store with this data on page reload\nconst localStorageArticles = localStorage.getItem('articles');\nconst initialState = {\n articles: localStorageArticles ? JSON.parse(localStorageArticles) : {},\n}\n\nconst [useSub, Store] = createStore(initialState);\n```\n\nYou can also initialize the data lazy inside another effect of the custom hook. You can\nuse `IndexedDB` if you need to store objects that are not lossless serializable to JSON.\nYou can use `sessionStorage` or `cookies` depending on your use case. No limitations.\n\n### Middlewares\nIt's totally up to you to write any sorts of middleware. One example of tracking special\nstate updates:\n```ts\nimport { createStore, StoreSet } from 'react-use-sub';\n\ntype State = { conversionStep: number };\nconst initialState: State = { conversionStep: 1 };\n\nconst [useSub, _store] = createStore<State>(initialState);\n\n// here comes the middleware implementation\nconst set: StoreSet<State> = (update) => {\n const prevState = _store.get();\n _store.set(update);\n const state = _store.get();\n if (prevState.conversionStep !== state.conversionStep) {\n trackConversionStep(state.conversionStep)\n }\n}\n\n// you can also add a reset functionality for Store which is very convenient for logouts\n// with or without tracking. It's all up to you.\nconst Store = { ..._store, set, reset: () => _store.set(initialState) };\n\nexport { useSub, Store };\n```\nYes, I know, it's basically just a higher order function. But let's keep things simple.\n\n\n### Testing\nYou don't need to mock any functions in order to test the integration of\nthe store. There is \"test-util\" that will improve your testing experience a lot.\nThe only thing you need to do is importing it. E.g. by putting it into your \"setupTests\" file.\n```ts\nimport 'react-use-sub/test-util';\n```\nPossible downsides: Some optimizations like batched processing of all updates will be disabled.\nYou won't notice the performance impact in your tests, but you should not relay on the number\nof renders caused by the store.\n\nTesting would look like this\n```tsx\n// in some component\nexport const MyExample: React.FC = () => {\n const stock = useSub(({ article: { stock } }) => stock);\n\n return <span>Article stock is: {stock}</span>;\n};\n\ndescribe('<MyExample />', () => {\n it('renders the stock', () => {\n // initialization\n // feel free to use any-casts in your tests (but only there)\n Store.set({ article: { stock: 1337 } as any });\n\n // render with stock 1337 (see '@testing-library/react')\n const { container } = render(<MyExample />);\n expect(container.textContent).toBe('Article stock is: 1337');\n\n // update the stock (not need to wrap into \"act\", it's already done for you)\n Store.set({ article: { stock: 444 } as any });\n expect(container.textContent).toBe('Article stock is: 444');\n });\n});\n```\n\n### Testing (without \"test-util\")\nYou can use the store as is, but you will need to run all timers with jest because \nall updates of components are processed batched. The above test would become:\n```tsx\ndescribe('<MyExample />', () => {\n it('renders the stock', () => {\n jest.useFakeTimers();\n // initialization\n // feel free to use any-casts in your tests (but only there)\n Store.set({ article: { stock: 1337 } as any });\n\n // render with stock 1337 (see '@testing-library/react')\n const { container } = render(<MyExample />);\n expect(container.textContent).toBe('Article stock is: 1337');\n\n // update the stock\n Store.set({ article: { stock: 444 } as any });\n jest.runAllTimers();\n expect(container.textContent).toBe('Article stock is: 444');\n });\n});\n```\n\n### SSR\nFor SSR you want to create a store instance that is provided by a React context. Otherwise, you'll\nneed to prevent store updates on singletons that live in the server scope and share state with other\nrequests. To do this you could basically create a `StoreProvider` like this one:\n\n```tsx\nimport React, { useMemo } from 'react';\nimport { createStore, StoreType, UseSubType } from 'react-use-sub';\n\nconst initialState = { foo: 'bar', num: 2 };\ntype State = typeof initialState;\nconst Context = React.createContext<{ useSub: UseSubType<State>; store: StoreType<State> }>({} as any);\n\n// those can be used everywhere on server- and client-side safely\nexport const useStore = (): StoreType<State> => React.useContext(Context).store;\nexport const useSub: UseSubType<State> = (...args) => React.useContext(Context).useSub(...args);\n\n// this needs to wrap your whole application\nexport const StoreProvider: React.FC = ({ children }) => {\n const value = useMemo(() => {\n const [useSub, store] = createStore(initialState);\n return { useSub, store };\n }, []);\n \n return <Context.Provider value={value}>{children}</Context.Provider>;\n};\n```\n\nYou might have already guessed, that this has some caveats, because you would have to get first via\n`useStore` the store in order to make updates or even provide it down in callbacks to perform these\nupdates any time later. But this is the complexity price to pay, when handling SSR. We need to make\nsure that updates are not performed by the server that cause conflicts with other incoming requests.\n\nThe new hooks `useStore` and `useSub` however are not performing any worse because the React context\nvalue is not updated after the initial render anymore.\n\n[license-image]: https://img.shields.io/badge/license-MIT-blue.svg\n[license-url]: https://github.com/fdc-viktor-luft/react-use-sub/blob/master/LICENSE\n[build-image]: https://img.shields.io/travis/fdc-viktor-luft/react-use-sub/master.svg?style=flat-square\n[build-url]: https://app.travis-ci.com/fdc-viktor-luft/react-use-sub\n[npm-image]: https://img.shields.io/npm/v/react-use-sub.svg?style=flat-square\n[npm-url]: https://www.npmjs.org/package/react-use-sub\n[coveralls-image]: https://coveralls.io/repos/github/fdc-viktor-luft/react-use-sub/badge.svg?branch=master\n[coveralls-url]: https://coveralls.io/github/fdc-viktor-luft/react-use-sub?branch=master\n[prettier-image]: https://img.shields.io/badge/styled_with-prettier-ff69b4.svg\n[prettier-url]: https://github.com/prettier/prettier\n" | ||
} |
@@ -19,3 +19,3 @@ [![GitHub license][license-image]][license-url] | ||
- Much better performance than react-redux | ||
- works with SSR | ||
- works with [SSR](#SSR) | ||
@@ -99,3 +99,3 @@ ### Examples | ||
```tsx | ||
export const FancyItem: React.FC<{ id: string }> = ({ id }) => { | ||
export const FancyItem: React.VFC<{ id: string }> = ({ id }) => { | ||
const { name, color } = useSub(({ items }) => items[id], [id]); | ||
@@ -290,2 +290,38 @@ | ||
### SSR | ||
For SSR you want to create a store instance that is provided by a React context. Otherwise, you'll | ||
need to prevent store updates on singletons that live in the server scope and share state with other | ||
requests. To do this you could basically create a `StoreProvider` like this one: | ||
```tsx | ||
import React, { useMemo } from 'react'; | ||
import { createStore, StoreType, UseSubType } from 'react-use-sub'; | ||
const initialState = { foo: 'bar', num: 2 }; | ||
type State = typeof initialState; | ||
const Context = React.createContext<{ useSub: UseSubType<State>; store: StoreType<State> }>({} as any); | ||
// those can be used everywhere on server- and client-side safely | ||
export const useStore = (): StoreType<State> => React.useContext(Context).store; | ||
export const useSub: UseSubType<State> = (...args) => React.useContext(Context).useSub(...args); | ||
// this needs to wrap your whole application | ||
export const StoreProvider: React.FC = ({ children }) => { | ||
const value = useMemo(() => { | ||
const [useSub, store] = createStore(initialState); | ||
return { useSub, store }; | ||
}, []); | ||
return <Context.Provider value={value}>{children}</Context.Provider>; | ||
}; | ||
``` | ||
You might have already guessed, that this has some caveats, because you would have to get first via | ||
`useStore` the store in order to make updates or even provide it down in callbacks to perform these | ||
updates any time later. But this is the complexity price to pay, when handling SSR. We need to make | ||
sure that updates are not performed by the server that cause conflicts with other incoming requests. | ||
The new hooks `useStore` and `useSub` however are not performing any worse because the React context | ||
value is not updated after the initial render anymore. | ||
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg | ||
@@ -292,0 +328,0 @@ [license-url]: https://github.com/fdc-viktor-luft/react-use-sub/blob/master/LICENSE |
@@ -5,2 +5,2 @@ const { act } = require('react-dom/test-utils'); | ||
_config.batch = act; | ||
_config.enqueue = (fn) => fn(); | ||
_config.dispatch = (fn) => fn(); |
@@ -5,2 +5,2 @@ import { act } from 'react-dom/test-utils'; | ||
_config.batch = act; | ||
_config.enqueue = (fn) => fn(); | ||
_config.dispatch = (fn) => fn(); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
35010
334
211
2
1