├── .editorconfig
├── .github
├── FUNDING.yml
└── workflows
│ └── tests.yml
├── .gitignore
├── LICENSE
├── README.md
├── babel.config.json
├── docs
└── ssr.md
├── jest.config.json
├── jest.setup-once.ts
├── lerna.json
├── og.png
├── package.json
├── rollup.config.js
├── src
├── index.ts
└── lib
│ ├── react-shared-internals.ts
│ └── use-force-update.ts
├── tests
├── effect.test.tsx
├── example.test.tsx
├── functions.test.ts
├── subscription.test.tsx
├── use-between.test.tsx
└── use-initial.test.tsx
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 2
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | indent_style = space
14 | indent_size = 2
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # betula
5 | open_collective: use-between # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # betula
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on: [push, pull_request]
3 | env:
4 | CI: true
5 |
6 | jobs:
7 | run:
8 | name: Node ${{ matrix.node }} on ${{ matrix.os }}
9 | runs-on: ${{ matrix.os }}
10 |
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | node: [12]
15 | os: [ubuntu-latest, windows-latest]
16 |
17 | steps:
18 | - name: Clone repository
19 | uses: actions/checkout@v2
20 |
21 | - name: Set Node.js version
22 | uses: actions/setup-node@v1
23 | with:
24 | node-version: ${{ matrix.node }}
25 |
26 | - run: node --version
27 | - run: npm --version
28 |
29 | - name: Install, build, and test
30 | run: npm run bootstrap
31 |
32 | - name: Coveralls
33 | uses: coverallsapp/github-action@master
34 | with:
35 | github-token: ${{ secrets.GITHUB_TOKEN }}
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDEs and editors
2 | .idea
3 | .project
4 | .classpath
5 | .c9/
6 | *.launch
7 | .settings/
8 | *.sublime-workspace
9 |
10 | # IDE - VSCode
11 | .vscode/*
12 | !.vscode/settings.json
13 | !.vscode/tasks.json
14 | !.vscode/launch.json
15 | !.vscode/extensions.json
16 |
17 | # OS files
18 | .DS_Store
19 | Thumbs.db
20 |
21 | # dependencies
22 | node_modules
23 | package-lock.json
24 |
25 | # logs
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 | lerna-debug.log*
30 |
31 | # testing
32 | /coverage
33 |
34 | # production
35 | /release
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Slava Bereza
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # use-between
2 |
3 | [](https://www.npmjs.com/package/use-between) [](https://github.com/betula/use-between/actions?workflow=Tests) [](https://bundlephobia.com/result?p=use-between) [](https://coveralls.io/github/betula/use-between) [](https://github.com/betula/use-between) [](https://www.npmjs.com/package/use-between)
4 |
5 | When you want to separate your React hooks between several components it's can be very difficult, because all context data stored in React component function area.
6 | If you want to share some of state parts or control functions to another component your need pass It thought React component props. But If you want to share It with sibling one level components or a set of scattered components, you will be frustrated.
7 |
8 | `useBetween` hook is the solution to your problem :kissing_closed_eyes:
9 |
10 | ```javascript
11 | import React, { useState, useCallback } from 'react';
12 | import { useBetween } from 'use-between';
13 |
14 | const useCounter = () => {
15 | const [count, setCount] = useState(0);
16 | const inc = useCallback(() => setCount(c => c + 1), []);
17 | const dec = useCallback(() => setCount(c => c - 1), []);
18 | return {
19 | count,
20 | inc,
21 | dec
22 | };
23 | };
24 |
25 | const useSharedCounter = () => useBetween(useCounter);
26 |
27 | const Count = () => {
28 | const { count } = useSharedCounter();
29 | return
{count}
;
30 | };
31 |
32 | const Buttons = () => {
33 | const { inc, dec } = useSharedCounter();
34 | return (
35 | <>
36 |
37 |
38 | >
39 | );
40 | };
41 |
42 | const App = () => (
43 | <>
44 |
45 |
46 |
47 |
48 | >
49 | );
50 |
51 | export default App;
52 | ```
53 | [](https://codesandbox.io/s/counter-with-usebetween-zh4tp?file=/src/App.js)
54 |
55 | `useBetween` is a way to call any hook. But so that the state will not be stored in the React component. For the same hook, the result of the call will be the same. So we can call one hook in different components and work together on one state. When updating the shared state, each component using it will be updated too.
56 |
57 | If you like this idea and would like to use it, please put star in github. It will be your first contribution!
58 |
59 | ### Developers :sparkling_heart: use-between
60 |
61 | > Hey [@betula](https://github.com/betula), just wanted to say thank you for this awesome library! ✋
62 | > Switching from React Context + useReducer to this library reduced soooo much boilerplate code.
63 | > It's much more nice, clean and simple now, plus the bonus of using "useEffect" incapsulated within the state hook is just awesome.
64 | >
65 | > I don't get why this library doesn't have more stars and more popularity. People using useContext+useReducer are really missing out 😃
66 | >
67 | > [**Jesper**, _This library should have way more stars! 🥇_](https://github.com/betula/use-between/issues/14)
68 |
69 |
70 | > [@betula](https://github.com/betula) as I mentioned before this lib is awesome and it allowed me to simplify an app that was using Redux. I was able to replace everything we were doing with Redux with just use-between and its tiny 2K footprint!
71 | >
72 | > Plus personally I think the code is cleaner because with use-between it just looks like normal hooks and not anything special like Redux code. I personally find it easier to read and understand than Redux!
73 | >
74 | > [**Melloware**, _Release discussion_](https://github.com/betula/use-between/discussions/20#discussioncomment-1715792)
75 |
76 | > I was about to install Redux until I found this library and it is a live saver. Really awesome job [@betula](https://github.com/betula). I don't know if I ever need to use Redux again haha
77 | >
78 | > [**Ronald Castillo**](https://github.com/betula/use-between/issues/14#issuecomment-1050601343)
79 |
80 | ### Supported hooks
81 |
82 | ```diff
83 | + useState
84 | + useEffect
85 | + useReducer
86 | + useCallback
87 | + useMemo
88 | + useRef
89 | + useImperativeHandle
90 | ```
91 |
92 | If you found some bug or want to propose improvement please [make an Issue](https://github.com/betula/use-between/issues/new) or join to [release discussion on Github](https://github.com/betula/use-between/discussions/35). I would be happy for your help to make It better! :wink:
93 |
94 | + [How to use SSR](./docs/ssr.md)
95 | + [Try Todos demo on CodeSandbox](https://codesandbox.io/s/todos-use-bettwen-8d2th?file=/src/components/todo-list.jsx)
96 | + [The article “Reuse React hooks in state sharing” on dev.to](https://dev.to/betula/reuse-react-hooks-in-state-sharing-1ell)
97 |
98 | ### Install
99 |
100 | ```bash
101 | npm install use-between
102 | # or
103 | yarn add use-between
104 | ```
105 |
106 | Enjoy and happy coding!
107 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", {
4 | "targets": {
5 | "node": "current"
6 | }
7 | }],
8 | "@babel/preset-react",
9 | "@babel/preset-typescript"
10 | ],
11 | "plugins": []
12 | }
13 |
--------------------------------------------------------------------------------
/docs/ssr.md:
--------------------------------------------------------------------------------
1 | # How to use SSR
2 |
3 | [Example of using SSR with Next.js on Github](https://github.com/betula/use-between-ssr)
4 |
--------------------------------------------------------------------------------
/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "rootDir": ".",
3 | "setupFiles": ["/jest.setup-once.ts"],
4 | "testMatch": [
5 | "/tests/**/*.ts*"
6 | ],
7 | "moduleNameMapper": {
8 | "~/(.*)$": "/src/$1"
9 | },
10 | "verbose": true
11 | }
12 |
--------------------------------------------------------------------------------
/jest.setup-once.ts:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | Enzyme.configure({adapter: new Adapter()});
5 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.3.5",
3 | "packages": [
4 | "."
5 | ],
6 | "npmClient": "npm",
7 | "loglevel": "verbose"
8 | }
9 |
--------------------------------------------------------------------------------
/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betula/use-between/ddd50040c1e97a45bd2f809f30e209b7401ab2dc/og.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "use-between",
3 | "version": "1.3.5",
4 | "description": "How to share React hooks state between components",
5 | "repository": {
6 | "url": "https://github.com/betula/use-between"
7 | },
8 | "bugs": {
9 | "url": "https://github.com/betula/use-between/issues"
10 | },
11 | "license": "MIT",
12 | "scripts": {
13 | "build": "rollup -c",
14 | "dev": "rollup -cw",
15 | "test": "jest --coverage",
16 | "clean": "rimraf ./release && rimraf ./coverage",
17 | "bootstrap": "npm install && npm run build && npm run test",
18 | "reset": "npm run clean && lerna clean --yes && npm run bootstrap",
19 | "publish": "npm run clean && npm run build && npm run test && lerna publish"
20 | },
21 | "author": "Slava Bereza (http://betula.co)",
22 | "keywords": [
23 | "model",
24 | "state",
25 | "reactive",
26 | "shared state",
27 | "use between",
28 | "react hooks",
29 | "react",
30 | "javascript",
31 | "typescript"
32 | ],
33 | "source": "./src/index.ts",
34 | "main": "./release/index.js",
35 | "module": "./release/index.esm.js",
36 | "types": "./release/index.d.ts",
37 | "sideEffects": false,
38 | "files": [
39 | "/release"
40 | ],
41 | "peerDependencies": {
42 | "react": ">=16.8.0"
43 | },
44 | "devDependencies": {
45 | "@babel/preset-env": "^7.9.6",
46 | "@babel/preset-react": "^7.9.4",
47 | "@babel/preset-typescript": "^7.9.0",
48 | "@testing-library/react": "^12.1.2",
49 | "@types/enzyme": "^3.10.5",
50 | "@types/enzyme-adapter-react-16": "^1.0.6",
51 | "@types/jest": "^25.2.3",
52 | "@types/node": "^14.0.5",
53 | "@types/react": "^16.9.35",
54 | "@types/react-dom": "^16.9.8",
55 | "enzyme": "^3.11.0",
56 | "enzyme-adapter-react-16": "^1.15.2",
57 | "jest": "^26.0.1",
58 | "lerna": "^3.22.0",
59 | "react": "^16.13.1",
60 | "react-dom": "^16.13.1",
61 | "rimraf": "^3.0.2",
62 | "rollup": "^2.10.9",
63 | "rollup-plugin-typescript2": "^0.27.1",
64 | "typescript": "^3.9.3"
65 | },
66 | "publishConfig": {
67 | "access": "public"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import typescript from 'rollup-plugin-typescript2';
3 | import pkg, { peerDependencies } from './package.json';
4 |
5 | export default {
6 | input: pkg.source,
7 | output: [{
8 | file: pkg.main,
9 | format: 'cjs'
10 | },{
11 | file: pkg.module,
12 | format: 'es'
13 | }],
14 | external: Object.keys(peerDependencies),
15 | plugins: [ typescript() ]
16 | }
17 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 | import { ReactCurrentDispatcher } from './lib/react-shared-internals'
3 | import { useForceUpdate } from './lib/use-force-update'
4 |
5 | const notImplemented = (name: string) => () => {
6 | const msg = `Hook "${name}" no possible to using inside useBetween scope.`
7 | console.error(msg)
8 | throw new Error(msg)
9 | }
10 |
11 | const equals = (a: any, b: any) => Object.is(a, b)
12 | const shouldUpdate = (a: any[], b: any[]) => (
13 | (!a || !b) ||
14 | (a.length !== b.length) ||
15 | a.some((dep: any, index: any) => !equals(dep, b[index]))
16 | )
17 |
18 | const detectServer = () => typeof window === 'undefined'
19 |
20 | const instances = new Map()
21 |
22 | let boxes = [] as any[]
23 | let pointer = 0
24 | let useEffectQueue = [] as any[]
25 | let useLayoutEffectQueue = [] as any[]
26 | let nextTick = () => {}
27 |
28 | let isServer = detectServer()
29 | let initialData = undefined as any
30 |
31 | const nextBox = () => {
32 | const index = pointer ++
33 | return (boxes[index] = boxes[index] || {})
34 | }
35 |
36 | const ownDisptacher = {
37 | useState(initialState?: any) {
38 | const box = nextBox()
39 | const tick = nextTick
40 |
41 | if (!box.initialized) {
42 | box.state = typeof initialState === "function" ? initialState() : initialState
43 | box.set = (fn: any) => {
44 | if (typeof fn === 'function') {
45 | return box.set(fn(box.state))
46 | }
47 | if (!equals(fn, box.state)) {
48 | box.state = fn
49 | tick()
50 | }
51 | }
52 | box.initialized = true
53 | }
54 |
55 | return [ box.state, box.set ]
56 | },
57 |
58 | useReducer(reducer: any, initialState?: any, init?: any) {
59 | const box = nextBox()
60 | const tick = nextTick
61 |
62 | if (!box.initialized) {
63 | box.state = init ? init(initialState) : initialState
64 | box.dispatch = (action: any) => {
65 | const state = reducer(box.state, action)
66 | if (!equals(state, box.state)) {
67 | box.state = state
68 | tick()
69 | }
70 | }
71 | box.initialized = true
72 | }
73 |
74 | return [ box.state, box.dispatch ]
75 | },
76 |
77 | useEffect(fn: any, deps: any[]) {
78 | if (isServer) return
79 | const box = nextBox()
80 |
81 | if (!box.initialized) {
82 | box.deps = deps
83 | box.initialized = true
84 | useEffectQueue.push([box, deps, fn])
85 | }
86 | else if (shouldUpdate(box.deps, deps)) {
87 | box.deps = deps
88 | useEffectQueue.push([box, deps, fn])
89 | }
90 | },
91 |
92 | useLayoutEffect(fn: any, deps: any[]) {
93 | if (isServer) return
94 | const box = nextBox()
95 |
96 | if (!box.initialized) {
97 | box.deps = deps
98 | box.initialized = true
99 | useLayoutEffectQueue.push([box, deps, fn])
100 | }
101 | else if (shouldUpdate(box.deps, deps)) {
102 | box.deps = deps
103 | useLayoutEffectQueue.push([box, deps, fn])
104 | }
105 | },
106 |
107 | useCallback(fn: any, deps: any[]) {
108 | const box = nextBox()
109 |
110 | if (!box.initialized) {
111 | box.fn = fn
112 | box.deps = deps
113 | box.initialized = true
114 | }
115 | else if (shouldUpdate(box.deps, deps)) {
116 | box.deps = deps
117 | box.fn = fn
118 | }
119 |
120 | return box.fn
121 | },
122 |
123 | useMemo(fn: any, deps: any[]) {
124 | const box = nextBox()
125 |
126 | if (!box.initialized) {
127 | box.deps = deps
128 | box.state = fn()
129 | box.initialized = true
130 | }
131 | else if (shouldUpdate(box.deps, deps)) {
132 | box.deps = deps
133 | box.state = fn()
134 | }
135 |
136 | return box.state
137 | },
138 |
139 | useRef(initialValue: any) {
140 | const box = nextBox()
141 |
142 | if (!box.initialized) {
143 | box.state = { current: initialValue }
144 | box.initialized = true
145 | }
146 |
147 | return box.state
148 | },
149 |
150 | useImperativeHandle(ref: any, fn: any, deps: any[]) {
151 | if (isServer) return
152 | const box = nextBox()
153 |
154 | if (!box.initialized) {
155 | box.deps = deps
156 | box.initialized = true
157 | useLayoutEffectQueue.push([box, deps, () => {
158 | typeof ref === 'function' ? ref(fn()) : ref.current = fn()
159 | }])
160 | }
161 | else if (shouldUpdate(box.deps, deps)) {
162 | box.deps = deps
163 | useLayoutEffectQueue.push([box, deps, () => {
164 | typeof ref === 'function' ? ref(fn()) : ref.current = fn()
165 | }])
166 | }
167 | }
168 | }
169 | ;[
170 | 'readContext',
171 | 'useContext',
172 | 'useDebugValue',
173 | 'useResponder',
174 | 'useDeferredValue',
175 | 'useTransition'
176 | ].forEach(key => (ownDisptacher as any)[key] = notImplemented(key))
177 |
178 | const factory = (hook: any, options?: any) => {
179 | const scopedBoxes = [] as any[]
180 | let syncs = [] as any[]
181 | let state = undefined as any
182 | let unsubs = [] as any[]
183 | let mocked = false
184 |
185 | if (options && options.mock) {
186 | state = options.mock
187 | mocked = true
188 | }
189 |
190 | const sync = () => {
191 | syncs.slice().forEach(fn => fn())
192 | }
193 |
194 | const tick = () => {
195 | if (mocked) return
196 |
197 | const originDispatcher = ReactCurrentDispatcher.current
198 | const originState = [
199 | pointer,
200 | useEffectQueue,
201 | useLayoutEffectQueue,
202 | boxes,
203 | nextTick
204 | ] as any
205 |
206 | let tickAgain = false
207 | let tickBody = true
208 |
209 | pointer = 0
210 | useEffectQueue = []
211 | useLayoutEffectQueue = []
212 | boxes = scopedBoxes
213 |
214 | nextTick = () => {
215 | if (tickBody) {
216 | tickAgain = true
217 | } else {
218 | tick()
219 | }
220 | }
221 |
222 | ReactCurrentDispatcher.current = ownDisptacher as any
223 | state = hook(initialData)
224 |
225 | ;[ useLayoutEffectQueue, useEffectQueue ].forEach(queue => (
226 | queue.forEach(([box, deps, fn]) => {
227 | box.deps = deps
228 | if (box.unsub) {
229 | const unsub = box.unsub
230 | unsubs = unsubs.filter(fn => fn !== unsub)
231 | unsub()
232 | }
233 | const unsub = fn()
234 | if (typeof unsub === "function") {
235 | unsubs.push(unsub)
236 | box.unsub = unsub
237 | } else {
238 | box.unsub = null
239 | }
240 | })
241 | ))
242 |
243 | ;[
244 | pointer,
245 | useEffectQueue,
246 | useLayoutEffectQueue,
247 | boxes,
248 | nextTick
249 | ] = originState
250 | ReactCurrentDispatcher.current = originDispatcher
251 |
252 | tickBody = false
253 | if (!tickAgain) {
254 | sync()
255 | return
256 | }
257 | tick()
258 | }
259 |
260 | const sub = (fn: any) => {
261 | if (syncs.indexOf(fn) === -1) {
262 | syncs.push(fn)
263 | }
264 | }
265 | const unsub = (fn: any) => {
266 | syncs = syncs.filter(f => f !== fn)
267 | }
268 |
269 | const mock = (obj: any) => {
270 | mocked = true
271 | state = obj
272 | sync()
273 | }
274 | const unmock = () => {
275 | mocked = false
276 | tick()
277 | }
278 |
279 | return {
280 | init: () => tick(),
281 | get: () => state,
282 | sub,
283 | unsub,
284 | unsubs: () => unsubs,
285 | mock,
286 | unmock
287 | }
288 | }
289 |
290 | const getInstance = (hook: any): any => {
291 | let inst = instances.get(hook)
292 | if (!inst) {
293 | inst = factory(hook)
294 | instances.set(hook, inst)
295 | inst.init()
296 | }
297 | return inst
298 | }
299 |
300 | type Hook = (initialData?: any) => T
301 |
302 | export const useBetween = (hook: Hook): T => {
303 | const forceUpdate = useForceUpdate()
304 | let inst = getInstance(hook)
305 | inst.sub(forceUpdate)
306 | useEffect(
307 | () => (inst.sub(forceUpdate), () => inst.unsub(forceUpdate)),
308 | [inst, forceUpdate]
309 | )
310 | return inst.get()
311 | }
312 |
313 | export const useInitial = (data?: T, server?: boolean) => {
314 | const ref = useRef()
315 | if (!ref.current) {
316 | isServer = typeof server === 'undefined' ? detectServer() : server
317 | isServer && clear()
318 | initialData = data
319 | ref.current = 1
320 | }
321 | }
322 |
323 | export const mock = (hook: Hook, state: any): () => void => {
324 | let inst = instances.get(hook)
325 | if (inst) inst.mock(state)
326 | else {
327 | inst = factory(hook, { mock: state })
328 | instances.set(hook, inst)
329 | }
330 | return inst.unmock
331 | }
332 |
333 | export const get = (hook: Hook): T => getInstance(hook).get()
334 |
335 | export const free = function(...hooks: Hook[]): void {
336 | if (!hooks.length) {
337 | hooks = []
338 | instances.forEach((_instance, hook) => hooks.push(hook))
339 | }
340 |
341 | let inst
342 | hooks.forEach((hook) => (
343 | (inst = instances.get(hook)) &&
344 | inst.unsubs().slice().forEach((fn: any) => fn())
345 | ))
346 | hooks.forEach((hook) => instances.delete(hook))
347 | }
348 |
349 | export const clear = () => instances.clear()
350 |
351 | export const on = (hook: Hook, fn: (state: T) => void): () => void => {
352 | const inst = getInstance(hook)
353 | const listener = () => fn(inst.get())
354 | inst.sub(listener)
355 | return () => inst.unsub(listener)
356 | }
357 |
358 |
--------------------------------------------------------------------------------
/src/lib/react-shared-internals.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | type AnyHook = (...args: any[]) => any;
4 | type ReactSharedInternalsType = {
5 | ReactCurrentDispatcher: {
6 | current?: {
7 | [name: string]: AnyHook
8 | };
9 | };
10 | }
11 |
12 | export const ReactSharedInternals =
13 | (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED as ReactSharedInternalsType
14 |
15 | export const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher
16 |
--------------------------------------------------------------------------------
/src/lib/use-force-update.ts:
--------------------------------------------------------------------------------
1 | import { useReducer } from 'react'
2 |
3 | export const useForceUpdate = () => (useReducer as any)(() => ({}))[1] as () => void
4 |
--------------------------------------------------------------------------------
/tests/effect.test.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { render } from '@testing-library/react'
3 | import { mount } from 'enzyme'
4 | import { clear, get, useBetween } from '../src'
5 |
6 |
7 | afterEach(clear)
8 |
9 |
10 | it('Effect should works asynchronous for hooks body', async () => {
11 | let inc = 0
12 | const effect_spy = jest.fn()
13 | const hook_finish_spy = jest.fn()
14 |
15 | const hook = () => {
16 | const [ curr, update ] = useState(0)
17 | useEffect(() => {
18 | effect_spy(++inc, curr)
19 | }, [curr])
20 | hook_finish_spy(++inc)
21 | return { update }
22 | }
23 |
24 | expect(effect_spy).toBeCalledTimes(0)
25 | expect(hook_finish_spy).toBeCalledTimes(0)
26 | get(hook)
27 | expect(effect_spy).toBeCalledTimes(1)
28 | expect(effect_spy).toBeCalledWith(2, 0)
29 | expect(hook_finish_spy).toBeCalledTimes(1)
30 | expect(hook_finish_spy).toBeCalledWith(1)
31 |
32 | get(hook).update(7)
33 | expect(effect_spy).toBeCalledTimes(2)
34 | expect(effect_spy).toHaveBeenLastCalledWith(4, 7)
35 | expect(hook_finish_spy).toBeCalledTimes(2)
36 | expect(hook_finish_spy).toHaveBeenLastCalledWith(3)
37 | })
38 |
39 | it('Effect should update state during hooks creation', async () => {
40 | const hook = () => {
41 | const [loading, setLoading] = useState(false)
42 |
43 | useEffect(() => {
44 | setLoading(true)
45 | }, [])
46 |
47 | return loading
48 | }
49 | const A = () => {
50 | const loading = useBetween(hook)
51 | return {loading ? 'loading' : ''}
52 | }
53 |
54 | const el = render()
55 | expect((await el.findByTestId('loading')).textContent).toBe('loading')
56 |
57 | clear()
58 | const ol = mount()
59 | expect(ol.find('i').text()).toBe('loading')
60 | })
61 |
62 |
--------------------------------------------------------------------------------
/tests/example.test.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react'
2 | import { clear, get, on, useBetween, mock } from '../src'
3 | import { act, render } from '@testing-library/react'
4 | import { mount } from 'enzyme'
5 |
6 | // ./shared-counter.js
7 | const useCounter = () => {
8 | const [count, setCount] = useState(0)
9 | const inc = useCallback(() => setCount((c) => c + 1), [])
10 | const dec = useCallback(() => setCount((c) => c - 1), [])
11 | return {
12 | count,
13 | inc,
14 | dec
15 | }
16 | }
17 |
18 | // ./shared-counter.test.js
19 |
20 | // Clean up after each test if necessary
21 | afterEach(clear) // or afterEach(() => clear())
22 |
23 | // Test example
24 | it('It works', async () => {
25 | expect(get(useCounter).count).toBe(0)
26 |
27 | get(useCounter).inc()
28 |
29 | // Check result
30 | expect(get(useCounter).count).toBe(1)
31 |
32 | get(useCounter).inc()
33 | expect(get(useCounter).count).toBe(2)
34 | })
35 |
36 | it('It works with spy', () => {
37 | const spy = jest.fn()
38 |
39 | // Subscribe to a state change
40 | on(useCounter, (state) => spy(state.count))
41 |
42 | get(useCounter).inc()
43 | expect(spy).toBeCalledWith(1)
44 |
45 | get(useCounter).dec()
46 | expect(spy).toHaveBeenLastCalledWith(0)
47 | })
48 |
49 | it('It works with enzyme render component', async () => {
50 | const Counter = () => {
51 | const { count } = useBetween(useCounter)
52 | return {count}
53 | }
54 |
55 | const el = mount()
56 | expect(el.find('i').text()).toBe('0')
57 |
58 | // You should use "act" from @testing-library/react
59 | act(() => {
60 | get(useCounter).inc()
61 | })
62 | expect(el.find('i').text()).toBe('1')
63 | })
64 |
65 | it('It works with testing-library render component', async () => {
66 | const Counter = () => {
67 | const { count } = useBetween(useCounter)
68 | return {count}
69 | }
70 |
71 | const el = render()
72 | expect((await el.findByTestId('count')).textContent).toBe('0')
73 |
74 | // You should use "act" from @testing-library/react
75 | act(() => {
76 | get(useCounter).dec()
77 | })
78 | expect((await el.findByTestId('count')).textContent).toBe('-1')
79 | })
80 |
81 | it('It works with testing-library render component with mock', async () => {
82 | const Counter = () => {
83 | const { count } = useBetween(useCounter)
84 | return {count}
85 | }
86 | mock(useCounter, { count: 10 })
87 |
88 | const el = render()
89 | expect((await el.findByTestId('count')).textContent).toBe('10')
90 |
91 | // You should use "act" from @testing-library/react
92 | act(() => {
93 | mock(useCounter, { count: 15 })
94 | })
95 | expect((await el.findByTestId('count')).textContent).toBe('15')
96 | })
97 |
--------------------------------------------------------------------------------
/tests/functions.test.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { get, free, clear, on, mock, useBetween } from '../src'
3 |
4 | afterEach(clear)
5 |
6 | test('Should work get function', async () => {
7 | const counter_spy = jest.fn()
8 | const useCounter = () => {
9 | counter_spy()
10 | const [count, setCount] = useState(10)
11 | return { count, setCount }
12 | }
13 |
14 | expect(counter_spy).toBeCalledTimes(0)
15 | expect(get(useCounter).count).toBe(10)
16 | expect(counter_spy).toBeCalledTimes(1)
17 | expect(get(useCounter).count).toBe(10)
18 | expect(counter_spy).toBeCalledTimes(1)
19 |
20 | get(useCounter).setCount(v => v + 5)
21 |
22 | expect(get(useCounter).count).toBe(15)
23 | expect(counter_spy).toBeCalledTimes(2)
24 |
25 | clear()
26 | expect(get(useCounter).count).toBe(10)
27 | expect(counter_spy).toBeCalledTimes(3)
28 | })
29 |
30 | test('Should work free function', () => {
31 | const effect_spy = jest.fn()
32 | const un_effect_spy = jest.fn()
33 | const useCounter = () => {
34 | const [count, setCount] = useState(10)
35 | useEffect(() => (effect_spy(count), () => un_effect_spy(count)), [count])
36 | return { count, setCount }
37 | }
38 |
39 | const { count, setCount} = get(useCounter)
40 | expect(count).toBe(10)
41 | expect(effect_spy).toBeCalledWith(10)
42 |
43 | setCount(v => v + 7)
44 | expect(effect_spy).toHaveBeenLastCalledWith(17)
45 | expect(un_effect_spy).toBeCalledWith(10)
46 |
47 | setCount(v => v + 9)
48 | expect(get(useCounter).count).toBe(26)
49 | expect(effect_spy).toHaveBeenLastCalledWith(26)
50 | expect(un_effect_spy).toHaveBeenLastCalledWith(17)
51 |
52 | free()
53 | expect(un_effect_spy).toHaveBeenLastCalledWith(26)
54 | })
55 |
56 | test('Should work few hooks free function', () => {
57 | const useA = () => useState(0)
58 | const useB = () => useState(0)
59 | const useC = () => useState(0)
60 |
61 | get(useA)[1](5)
62 | get(useB)[1](6)
63 | get(useC)[1](7)
64 |
65 | expect(get(useA)[0]).toBe(5)
66 | free(useA)
67 | expect(get(useA)[0]).toBe(0)
68 |
69 | get(useA)[1](15)
70 |
71 | expect(get(useB)[0]).toBe(6)
72 | expect(get(useC)[0]).toBe(7)
73 | free(useB, useC)
74 |
75 | expect(get(useB)[0]).toBe(0)
76 | expect(get(useC)[0]).toBe(0)
77 |
78 | expect(get(useA)[0]).toBe(15)
79 | free()
80 | expect(get(useA)[0]).toBe(0)
81 | })
82 |
83 | test('Should work on function', () => {
84 | const constr = jest.fn()
85 | const spy = jest.fn()
86 | const useA = () => (constr(), useState(0))
87 |
88 | on(useA, (state) => spy(state[0]))
89 | expect(constr).toBeCalledTimes(1)
90 | expect(spy).toBeCalledTimes(0)
91 |
92 | get(useA)[1](6)
93 | expect(spy).toBeCalledWith(6)
94 | get(useA)[1](10)
95 | expect(spy).toHaveBeenLastCalledWith(10)
96 | expect(spy).toHaveBeenCalledTimes(2)
97 |
98 | free(useA)
99 | expect(get(useA)[0]).toBe(0)
100 | expect(spy).toHaveBeenCalledTimes(2)
101 |
102 | get(useA)[1](17)
103 | expect(spy).toHaveBeenCalledTimes(2)
104 |
105 | const unsub = on(useA, (state) => spy(state[0]))
106 | get(useA)[1](18)
107 | expect(spy).toHaveBeenLastCalledWith(18)
108 | expect(spy).toHaveBeenCalledTimes(3)
109 |
110 | unsub()
111 | get(useA)[1](19)
112 | expect(spy).toHaveBeenCalledTimes(3)
113 | })
114 |
115 | test('Should work mock function', () => {
116 | const spy = jest.fn()
117 | const useA = () => useState(10)
118 | const useB = () => useBetween(useA)
119 |
120 | on(useB, (state) => spy(state[0]))
121 | expect(spy).toHaveBeenCalledTimes(0)
122 |
123 | expect(get(useB)[0]).toBe(10)
124 | get(useA)[1](5)
125 | expect(get(useB)[0]).toBe(5)
126 | expect(spy).toHaveBeenLastCalledWith(5)
127 | expect(spy).toHaveBeenCalledTimes(1)
128 |
129 | const unmock = mock(useA, [20])
130 | expect(get(useB)[0]).toBe(20)
131 | expect(spy).toHaveBeenLastCalledWith(20)
132 | expect(spy).toHaveBeenCalledTimes(2)
133 |
134 | expect(get(useA)).toStrictEqual([20])
135 |
136 | unmock()
137 | expect(get(useB)[0]).toBe(5)
138 | expect(spy).toHaveBeenLastCalledWith(5)
139 | expect(spy).toHaveBeenCalledTimes(3)
140 |
141 | get(useB)[1](9)
142 | expect(get(useB)[0]).toBe(9)
143 | expect(spy).toHaveBeenLastCalledWith(9)
144 | expect(spy).toHaveBeenCalledTimes(4)
145 | })
146 |
147 | test('Should work mock function without original function run', () => {
148 | const spy = jest.fn().mockReturnValue(1)
149 | const useA = () => spy()
150 | const unmock = mock(useA, 15)
151 |
152 | expect(get(useA)).toBe(15)
153 | expect(spy).toBeCalledTimes(0)
154 |
155 | mock(useA, 17)
156 | expect(get(useA)).toBe(17)
157 | expect(spy).toBeCalledTimes(0)
158 |
159 | unmock()
160 | expect(get(useA)).toBe(1)
161 | expect(spy).toBeCalledTimes(1)
162 | })
163 |
--------------------------------------------------------------------------------
/tests/subscription.test.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { render } from '@testing-library/react'
3 | import { clear, useBetween } from '../src'
4 |
5 |
6 | afterEach(clear)
7 |
8 | it('Subscription should be immediately in render phase', async () => {
9 | const useShared = () => {
10 | const [a, setA] = useState(0)
11 | const [b, setB] = useState(0)
12 | useEffect(() => {
13 | setB(1)
14 | }, [a])
15 |
16 | return { a, setA, b, setB }
17 | }
18 |
19 | const Child = () => {
20 | const { b } = useBetween(useShared)
21 | return {b}
22 | }
23 |
24 | const Parent = () => {
25 | const { a, setA } = useBetween(useShared)
26 | if (a === 0) {
27 | setA(1)
28 | }
29 | return a ? : null
30 | }
31 |
32 | const el = render()
33 | expect((await el.findByTestId('b')).textContent).toBe('1')
34 | })
35 |
--------------------------------------------------------------------------------
/tests/use-between.test.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useState,
3 | useReducer,
4 | useEffect,
5 | useCallback,
6 | useLayoutEffect,
7 | useMemo,
8 | useRef,
9 | useImperativeHandle,
10 | useContext
11 | } from 'react'
12 | import { mount } from 'enzyme'
13 | import { get, useBetween } from '../src'
14 |
15 | test('Should work useState hook', () => {
16 | const useStore = () => useState(0)
17 |
18 | const A = () => {
19 | const [ a ] = useBetween(useStore)
20 | return {a}
21 | }
22 | const B = () => {
23 | const [ , set ] = useBetween(useStore)
24 | return