├── .eslintignore
├── .eslintrc.js
├── .flowconfig
├── .gitignore
├── .prettierrc
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── babel.config.js
├── jest.config.js
├── lerna.json
├── modules
└── react-global-hooks
│ ├── __tests__
│ ├── create-common-hook.test.js
│ ├── create-shared-reducer.test.js
│ ├── create-shared-ref.test.js
│ ├── create-shared-state.test.js
│ ├── use-common-callback.test.js
│ ├── use-common-effect.test.js
│ ├── use-common-layout-effect.test.js
│ ├── use-common-memo.test.js
│ ├── use-common-ref.test.js
│ └── use-common-state.test.js
│ ├── babel.config.js
│ ├── package.json
│ └── src
│ ├── create-common-callback.js
│ ├── create-common-effect.js
│ ├── create-common-hook.js
│ ├── create-common-layout-effect.js
│ ├── create-common-memo.js
│ ├── create-common-ref.js
│ ├── create-common-state.js
│ ├── create-shared-reducer.js
│ ├── create-shared-ref.js
│ ├── create-shared-state.js
│ ├── hook-factory.js
│ ├── index.js
│ ├── provider.js
│ ├── store-map.js
│ ├── store.js
│ ├── types.flow.js
│ └── use-watched.js
├── package.json
├── scripts
└── link-examples.sh
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules/**
2 | **/dist-*
3 | **/flow-typed/**
4 |
5 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | env: {
4 | browser: true,
5 | node: true,
6 | es6: true,
7 | jest: true,
8 | },
9 | extends: [require.resolve('eslint-config-fusion')],
10 | };
11 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | .*/node_modules/*
3 | .*/examples/*
4 |
5 | [untyped]
6 |
7 | [include]
8 |
9 | [libs]
10 |
11 | [lints]
12 |
13 | [options]
14 | esproposal.optional_chaining=enable
15 | esproposal.nullish_coalescing=enable
16 | [strict]
17 |
18 | [version]
19 | 0.109.0
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.log
3 | .fusion/
4 | coverage*/
5 | .nyc_output/
6 | .vscode/
7 | modules/**/dist-*
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "bracketSpacing": false,
4 | "trailingComma": "all",
5 | "overrides": [
6 | {
7 | "files": "*.js",
8 | "options": {
9 | "parser": "flow"
10 | }
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '10'
4 | before_script:
5 | - "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | sudo bash"
6 | script:
7 | - yarn test
8 | - fossa init
9 | - fossa analyze
10 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at gnemeth@uber.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: https://contributor-covenant.org
46 | [version]: https://contributor-covenant.org/version/1/4/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Uber Technologies, Inc.
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-global-hooks
2 |
3 | ## Motivation
4 |
5 | React hooks have become quite popular since they were released. Developers have used the composable nature of react hooks to abstract logic into custom hooks. These custom hooks _enhance_ a functional component by providing behavior and local state.
6 |
7 | **React Global Hooks** expands on this idea by introducing global versions of these same hooks. These are the foundational building blocks for writing custom hooks that are shared between components but effect _component independent_ interactions. Components _subscribe_ to behavior and state encapsulated within these global hooks.
8 |
9 | ---
10 |
11 | ## Usage
12 |
13 | ```js
14 | // hooks/use-fibonocci.js
15 | import {
16 | createSharedState,
17 | createCommonHook,
18 | useCommonCallback,
19 | useCommonEffect,
20 | } from '@uber/react-global-hooks';
21 |
22 | const [useGetFib, useSetFib] = createSharedState({prev: 0, curr: 1});
23 | export {useGetFib};
24 |
25 | export const useFibonocciOnMove = createCommonHook(() => {
26 | const setFibonocci = useSetFib();
27 |
28 | const handleMouseMove = useCommonCallback(() => {
29 | setFibonocci(({prev, curr}) => ({prev: curr, curr: prev + curr}));
30 | }, []); // setFibonocci is referentially stable and not needed in dependency array
31 |
32 | useCommonEffect(() => {
33 | document.addEventListener('mousemove', handleMouseMove);
34 | return () => {
35 | document.removeEventListener('mousemove', handleMouseMove);
36 | };
37 | }, [handleMouseMove]);
38 | });
39 |
40 | // components/fibonocci.js
41 | import {useGetFib, useFibonocciOnMove} from '../hooks/use-fibonocci';
42 |
43 | export const Fib = () => {
44 | useFibonocciOnMove();
45 | return null;
46 | };
47 |
48 | // selector for current fib value
49 | const selectCurrent = ({curr}) => curr;
50 |
51 | // Debounce rerenders with 500ms delay
52 | const debounce = (fn) => _.debounce(fn, 500);
53 |
54 | export const ShowFibDebounced = () => {
55 | const fib = useGetFib(selectCurrent, null, debounce);
56 | return fib;
57 | };
58 | ```
59 |
60 | ---
61 |
62 | ## Definitions
63 |
64 | ### Shared vs Common
65 |
66 | **Shared** hooks can be shared between multiple components and custom hooks. They provide referentially stable results across all call positions.
67 |
68 | **Common** hooks partition behavior on call position. Each call position provides independent behavior and results.
69 |
70 | ### Call Position
71 |
72 | A hook's call position is the expression where that hook is invoked. A hook invoked in multiple places is said to have multiple call positions.
73 |
74 | For example, say Hook A is only invoked by Hook B, and Hook B is invoked by multiple components. Hook A is still said to have only one call position, (inside Hook B). Hook A's call position provides consistent behavior and referentially stable results for that call position across all call stacks.
75 |
76 | **Example 1** Consistent Behavior
77 |
78 | ```
79 | const useHookA = useCommonEffect;
80 |
81 | const useHookB = createCommonHook(() => {
82 | useHookA(() => {
83 | console.log('runs only on first component mount');
84 | return () => console.log('runs only on last component unmount');
85 | }, []);
86 | });
87 | ```
88 |
89 | **Example 2** Referential Stability
90 |
91 | ```
92 | const useHookA = useCommonRef;
93 |
94 | const useHookB = createCommonHook(() => {
95 | const ref = useHookA();
96 | return ref
97 | });
98 |
99 | const CheckRef = () => {
100 | const ref1 = useHookB();
101 | const ref2 = useHookB();
102 | console.log(ref1 === ref2); // true
103 | return null;
104 | };
105 | ```
106 |
107 | ---
108 |
109 | ## Getting Started
110 |
111 | ```
112 | import {createStoreMap, Provider as GlobalHooksProvider} from '@uber/react-global-hooks';
113 |
114 | const storeMap = createStoreMap();
115 | ReactDOM.render(
116 |
117 |
118 | ,
119 | document.getElementById('root')
120 | );
121 | ```
122 |
123 | **Concurrent Mode**
124 |
125 | ```
126 | import {createStoreMap, Provider as GlobalHooksProvider} from '@uber/react-global-hooks';
127 |
128 | const storeMap = createStoreMap();
129 | ReactDOM.createRoot(
130 | document.getElementById('root')
131 | ).render(
132 |
133 |
134 |
135 | );
136 | ```
137 |
138 | ---
139 |
140 | ## Shared Hooks
141 |
142 | React's `useState` and `useReducer` are good solutions for state isolated to a component. This library expands on this idea by providing shareable versions of `useState` and `useReducer` so that atomic and molecular state can be shared across many components.
143 |
144 | ### createSharedState
145 |
146 | Returns `useSelector` and `useSetState` hooks.
147 |
148 | `useSelector` and `useSetState` are useful for sharing global data atomics.
149 |
150 | `useSetState` returns `setState`.
151 | `setState`'s API is similar to React's setState.
152 |
153 | ```js
154 | type CreateSharedState = (
155 | InitialState | LazyInitialState,
156 | ?DebugName,
157 | ) => [UseSelector, UseDispatch];
158 | type LazyInitialState = (Dispatch) => InitialState;
159 | type UseSelector = (?Selector, ?EqualityFn, ?TimeVaryingFn) => SelectedState;
160 | type Selector = (NextState) => SelectedState;
161 | type EqualityFn = (CurrentState, NextState) => boolean;
162 | type TimeVaryingFn = (Function) => Function;
163 | type UseDispatch = () => Dispatch;
164 | type Dispatch = (State | LazyState) => void;
165 | type LazyState = (CurrentState) => NextState;
166 | type DebugName = string;
167 | ```
168 |
169 | ```js
170 | const [useSelector, useSetState] = createSharedState(initialCount);
171 | ```
172 |
173 | ```js
174 | const state = useSelector();
175 | ```
176 |
177 | ```js
178 | const setState = useSetState();
179 | ```
180 |
181 | Components that use this selector will only rerender when state.count changes
182 |
183 | You can make the selector referentially stable to improve performance. The selector is otherwise run on every render.
184 |
185 | ```
186 | const selectCount = useCallback(state => state.count, []);
187 | const count = useSelector(selectCount);
188 | ```
189 |
190 | Pass a equality function to override the default. The default equality function is `Object.is`.
191 |
192 | ```
193 | const vehicleSelector = useCallback(state => state.vehicle, []);
194 | const vehicleEquality = useCallback((curr, next) => curr.vin === next.vin), []);
195 | const vehicle = useSelector(vehicleSelector, vehicleEquality);
196 | ```
197 |
198 | Specify a time-varying function such as debounce or throttle to limit the number of rerenders.
199 |
200 | Important Note: selector and equalityFn must be referentially stable for timeVaryingFn to work. Use useCallback or define outside the component to ensure stability.
201 |
202 | ```
203 | const vehicleSelector = useCallback(state => state.vehicle, []);
204 | const vehicleEquality = useCallback((curr, next) => curr.vin === next.vin), []);
205 | const timeVaryingFn = useCallback(fn => _.debounce(fn, 500), []); // lodash debounce
206 | const vehicle = useSelector(vehicleSelector, vehicleEquality, timeVaryingFn);
207 | ```
208 |
209 | Set a simple state
210 |
211 | ```
212 | setState(5);
213 | ```
214 |
215 | or set state based on previous state
216 |
217 | ```
218 | setState(count => count + 1);
219 | ```
220 |
221 | Bail out of a render by returning the original state
222 |
223 | ```
224 | setState(count => {
225 | if (someCondition) {
226 | return count;
227 | }
228 | return count + 1;
229 | });
230 | ```
231 |
232 | Lazy initial state
233 |
234 | ```
235 | const [useSelector, useSetState] = createSharedState(
236 | () => someExpensiveComputation();
237 | );
238 | ```
239 |
240 | Async lazy initial state
241 |
242 | ```
243 | const fetchPromise = fetch('example.api').then(data => data.json());
244 | const [useGetState, useSetState, useSubscribe] = createSharedState(setState => {
245 | fetchPromise.then(setState);
246 | return {}; // use this value until example.api responds
247 | });
248 | ```
249 |
250 | ### createSharedReducer
251 |
252 | Returns `useSelector` and `useDispatch` hooks.
253 |
254 | ```js
255 | type CreateSharedReducer = (
256 | Reducer,
257 | InitialState | LazyInitialState,
258 | ?DebugName,
259 | ) => [UseSelector, UseDispatch];
260 | type Reducer = (CurrentState, Action) => NextState;
261 | type Action = Object;
262 | type LazyInitialState = (Dispatch) => InitialState;
263 | type UseSelector = (?Selector, ?EqualityFn, ?TimeVaryingFn) => SelectedState;
264 | type Selector = (NextState) => SelectedState;
265 | type EqualityFn = (CurrentState, NextState) => boolean;
266 | type TimeVaryingFn = (Function) => Function;
267 | type UseDispatch = () => Dispatch;
268 | type Dispatch = (Action) => void;
269 | type LazyState = (CurrentState) => NextState;
270 | type DebugName = string;
271 | ```
272 |
273 | ```js
274 | const initialState = {count: 0};
275 |
276 | function reducer(state, action) {
277 | switch (action.type) {
278 | case 'increment':
279 | return {count: state.count + 1};
280 | case 'decrement':
281 | return {count: state.count - 1};
282 | default:
283 | throw new Error();
284 | }
285 | }
286 |
287 | const [useSelector, useDispatch] = createSharedReducer(reducer, initialState);
288 | ```
289 |
290 | Components that use this selector will only rerender when state.count changes
291 |
292 | ```
293 | const countSelector = useCallback(state => state.count, []);
294 | const count = useSelector(selectCount);
295 | ```
296 |
297 | Pass a equality function to override the default. The default equality function is `Object.is`.
298 |
299 | You can make the selector referentially stable to improve performance. The selector is otherwise run on every render.
300 |
301 | ```
302 | const vehicleSelector = useCallback(state => state.vehicle, []);
303 | const vehicleEquality = useCallback((curr, next) => curr.vin === next.vin), []);
304 | const vehicle = useSelector(vehicleSelector, vehicleEquality);
305 | ```
306 |
307 | Specify a time-varying function such as debounce or throttle to limit the number of rerenders.
308 |
309 | Important Note: selector and equalityFn must be referentially stable for timeVaryingFn to work. Use useCallback or define outside the component to ensure stability.
310 |
311 | ```
312 | const vehicleSelector = useCallback(state => state.vehicle, []);
313 | const vehicleEquality = useCallback((curr, next) => curr.vin === next.vin), []);
314 | const timeVaryingFn = useCallback(fn => _.debounce(fn, 500), []); // lodash debounce
315 | const vehicle = useSelector(vehicleSelector, vehicleEquality, timeVaryingFn);
316 | ```
317 |
318 | Dispatch an action
319 |
320 | ```
321 | const dispatch = useDispatch();
322 | dispatch({type: 'increment'});
323 | ```
324 |
325 | Lazy initial state
326 |
327 | ```
328 | const [useSelector, useDispatch] = createSharedReducer(reducer,
329 | () => someExpensiveComputation()
330 | );
331 | ```
332 |
333 | Async lazy initial state
334 |
335 | ```
336 | const fetchPromise = fetch('example.api').then(data => data.json());
337 | const [useSelector, useDispatch] = createSharedReducer(reducer, dispatch => {
338 | fetchPromise.then(value => {
339 | dispatch({type: 'INITIALIZE', value});
340 | });
341 | return {}; // use this value until example.api responds
342 | });
343 | ```
344 |
345 | ### createSharedRef
346 |
347 | Returns `useSharedRef` that provides a referentially stable ref that may be used by multiple hooks.
348 |
349 | `useSharedRef` is useful for creating refs that are watched by other common hooks.
350 |
351 | ```js
352 | type createSharedRef = (?any, ?DebugName) => useSharedRef;
353 | type useSharedRef = () => Ref;
354 | type Ref = {current: any};
355 | type DebugName = string;
356 | ```
357 |
358 | ```js
359 | const useSharedRef = createSharedRef();
360 | ```
361 |
362 | ---
363 |
364 | ## Common Hooks
365 |
366 | ### Build more logic into hooks with Common Hooks
367 |
368 | If we intend to write truely shareable hooks, we need hooks that are not based on individual component lifecycle events. This library provides _Common_ hooks that compose into shareable custom hooks.
369 |
370 | ### createCommonHook
371 |
372 | This higher order hook is required to use the `useCommon-*` hooks in this library.
373 |
374 | `createCommonHook` internally tracks each call position and memoizes a separate common hook for each position. This is only possible inside a custom hook wrapped by `createCommonHook`.
375 |
376 | ```js
377 | type createCommonHook = (Hook, ?DebugName) => SharedHook;
378 | type Hook = Function;
379 | type SharedHook = Hook;
380 | type DebugName = string;
381 | ```
382 |
383 | ```js
384 | import {
385 | createCommonHook,
386 | useCommonEffect,
387 | useCommonMemo,
388 | useCommonRef
389 | } from `@uber/react-global-hooks`;
390 |
391 | const useCustomHook = createCommonHook(() => {
392 | const ref = useCommonRef();
393 | useCommonEffect(() => {}, [ref]);
394 | useCommonEffect(() => {}, [ref]);
395 | return useCommonMemo(() => {}, []);
396 | });
397 | export default useCustomHook;
398 | ```
399 |
400 | It is also safe to use react hooks within a `createCommonHook`. The function argument respects React's call position across renders.
401 |
402 | ```js
403 | import {useEffect} from 'react';
404 | import {
405 | createCommonHook,
406 | useCommonMemo,
407 | } from `@uber/react-global-hooks`;
408 |
409 | const useCustomHook = () => {
410 | useEffect(() => {}, []);
411 | return useCommonMemo(() => {}, []);
412 | };
413 | export default createCommonHook(useCustomHook);
414 | ```
415 |
416 | ### useCommonCallback
417 |
418 | Provides a referentially stable callback across all call stacks of the enclosing hook.
419 |
420 | This API is identical to React's useCallback.
421 |
422 | ```js
423 | type useCommonCallback = (InputFn, WatchedArgs) => StableFn;
424 | type InputFn = Function;
425 | type WatchedArgs = Array;
426 | type StableFn = InputFn;
427 | ```
428 |
429 | ```js
430 | import {createCommonHook, useCommonCallback} from `@uber/react-global-hooks`;
431 |
432 | const useCustomHook = createCommonHook((fn) => {
433 | const stableFn = useCommonCallback(fn, []);
434 | });
435 | export default useCustomHook;
436 | ```
437 |
438 | ### useCommonEffect
439 |
440 | Executes a function on the first component mount or whenever props change asynchronously post render. The returned cleanup function is executed on last component unmount or whenever props change. This API is identical to React's useEffect.
441 |
442 | `useCommonEffect` is useful for registering event listeners, fetching data, and other side-effects that should applied only once.
443 |
444 | ```js
445 | type useCommonEffect = (InputFn, WatchedArgs) => void;
446 | type InputFn = () => Cleanup;
447 | type Cleanup = () => void;
448 | type WatchedArgs = Array;
449 | ```
450 |
451 | ```js
452 | import {createCommonHook, useCommonEffect} from `@uber/react-global-hooks`;
453 |
454 | const useCustomHook = createCommonHook((fn) => {
455 | useCommonEffect(fn, []);
456 | });
457 | export default useCustomHook;
458 | ```
459 |
460 | ### useCommonLayoutEffect
461 |
462 | Executes a function on the first component mount or whenever props change synchronously after all DOM mutations This API is identical to React's useLayoutEffect.
463 |
464 | `useCommonLayoutEffect` is useful for DOM layout dependent effects that should be applied only once.
465 |
466 | ```js
467 | type useCommonEffect = (InputFn, WatchedArgs) => void;
468 | type InputFn = () => Cleanup;
469 | type Cleanup = () => void;
470 | type WatchedArgs = Array;
471 | ```
472 |
473 | ```js
474 | import {createCommonHook, useCommonLayoutEffect} from `@uber/react-global-hooks`;
475 |
476 | const useCustomHook = createCommonHook((fn) => {
477 | useCommonLayoutEffect(fn, []);
478 | });
479 | export default useCustomHook;
480 | ```
481 |
482 | ### useCommonMemo
483 |
484 | Provides a referentially stable memo across all call stacks of the enclosing hook. This API is identical to React's useMemo.
485 |
486 | Fn will be called on first component mount or whenever any values change. `useCommonMemo` runs synchronously during render.
487 |
488 | ```js
489 | type useCommonMemo = (InputFn, WatchedArgs) => MemoizedValue;
490 | type InputFn = () => Value;
491 | type Value = any;
492 | type MemoizedValue = Value;
493 | type WatchedArgs = Array;
494 | ```
495 |
496 | ```js
497 | import {createCommonHook, useCommonMemo} from `@uber/react-global-hooks`;
498 |
499 | const useCustomHook = createCommonHook((fn) => {
500 | const stableMemo = useCommonMemo(fn, []);
501 | });
502 | export default useCustomHook;
503 | ```
504 |
505 | ### useCommonRef
506 |
507 | Provides a referentially stable ref across all call stacks of the enclosing hook. This API is identical to React's useRef.
508 |
509 | `useCommonRef` is useful for creating refs that are watched by other common hooks.
510 |
511 | ```js
512 | type useCommonRef = (Value) => Ref;
513 | type Value = any;
514 | type Ref = {current: Value};
515 | ```
516 |
517 | ```js
518 | import {createCommonHook, useCommonRef} from `@uber/react-global-hooks`;
519 |
520 | const useCustomHook = createCommonHook(() => {
521 | const stableRef = useCommonRef();
522 | });
523 | export default useCustomHook;
524 | ```
525 |
526 | ### useCommonState
527 |
528 | Provides a common state and setState. This API is identical to React's useState.
529 |
530 | `useCommonState` is useful for storing atomic state that is local to the enclosing hook. Prefer `useSharedState` and `useSharedReducer` for organizing application state. These APIs provide extended capabilities for limiting the number of rerenders.
531 |
532 | ```js
533 | type useCommonState = (State | LazyState) => [State, SetState];
534 | type LazyState = (SetState) => NextState;
535 | type SetState = (State) => NextState;
536 | ```
537 |
538 | ```js
539 | import {createCommonHook, useCommonState} from `@uber/react-global-hooks`;
540 |
541 | const useCustomHook = createCommonHook(() => {
542 | const [state, setState] = useCommonState();
543 | });
544 | export default useCustomHook;
545 | ```
546 |
547 | ---
548 |
549 | ### Register Custom Base Hooks
550 |
551 | Need a hook that doesn't exist? You can register your own with `hookFactory` to piggyback off `createCommonHook`'s call position tracking.
552 |
553 | To use `hookFactory` the callback must take the shape of `() => Function`.
554 |
555 | ```js
556 | type HookFactory = (CreateHook) => CommonHook;
557 | type CreateHook = () => Hook;
558 | type CommonHook = Hook;
559 | type Hook = Function;
560 | ```
561 |
562 | ```js
563 | import {hookFactory} from '@uber/react-global-hooks';
564 |
565 | const useDebounced = hookFactory(function createDebouncedHook() {
566 | let timeout;
567 | return function useDebounced(fn, value) {
568 | clearTimeout(timeout);
569 | timeout = setTimeout(fn, value);
570 | };
571 | });
572 |
573 | const useHookA = createCommonHook((a, b, c) => {
574 | useDebounced(a);
575 | useDebounced(b);
576 | useDebounced(c);
577 | });
578 | const useHookB = createCommonHook((d) => {
579 | useDebounced(d);
580 | });
581 | ```
582 |
583 | ## License
584 |
585 | MIT
586 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | module.exports = {
3 | presets: ['@babel/react', '@babel/flow'],
4 | plugins: [
5 | '@babel/proposal-class-properties',
6 | '@babel/proposal-export-default-from',
7 | 'version-inline',
8 | ],
9 | env: {
10 | test: {
11 | presets: ['@babel/env', '@babel/react', '@babel/flow'],
12 | plugins: [
13 | '@babel/transform-runtime',
14 | '@babel/proposal-class-properties',
15 | '@babel/proposal-export-default-from',
16 | 'version-inline',
17 | ],
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | // Sets the environment variables for tests
3 | process.env.JEST_ENV = 'node';
4 | process.env.TZ = 'Etc/UTC';
5 |
6 | module.exports = {
7 | verbose: true,
8 | testURL: 'http://localhost:3000/',
9 | collectCoverageFrom: ['modules/*/src/**/*.js', '!**/node_modules/**'],
10 | testPathIgnorePatterns: ['/node_modules/', '/examples/', 'dist-.*'],
11 | };
12 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": ["modules/*"],
3 | "npmClient": "yarn",
4 | "useWorkspaces": true,
5 | "version": "0.2.4"
6 | }
7 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/__tests__/create-common-hook.test.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | // Verify common hooks are created only on first run
11 | // Verify symbol registration for common hooks
12 | // Verify the call order for common hooks of the same scope is preserved
13 | // Verify the call orders for common hooks of multiple scopes are preserved
14 | // Verify the call orders for common hooks of nested scopes are preserved
15 | // Verify the currentScope of nested scopes is respected
16 | // Verify the call positions do not collide
17 |
18 | // Verify the commonality of common hooks for a scope across rerenders
19 | // Verify the commonality of common hooks for nested scopes
20 |
21 | import React from 'react';
22 | import {renderHook} from '@testing-library/react-hooks';
23 | import {
24 | Provider,
25 | createStoreMap,
26 | createCommonHook,
27 | useCommonCallback,
28 | useCommonLayoutEffect,
29 | useCommonEffect,
30 | useCommonMemo,
31 | useCommonRef,
32 | useCommonState,
33 | } from '../src';
34 | import Store from '../src/store';
35 |
36 | function getSymbolName(symbol) {
37 | return String(symbol).replace(/^Symbol\(|\)$/g, '');
38 | }
39 | function getValueBySymbolName(name, obj) {
40 | const symbolKey = Object.getOwnPropertySymbols(obj).find(
41 | (symbol) => getSymbolName(symbol) === name,
42 | );
43 | return obj[symbolKey] || null;
44 | }
45 |
46 | test('Verify common hooks are created only on first run', () => {
47 | const storeMap = createStoreMap();
48 | const wrapper = ({children}) => (
49 | {children}
50 | );
51 | let value = 0;
52 | const useCommonHookTest = createCommonHook((value) => {
53 | return useCommonRef(value);
54 | });
55 | const {result, rerender} = renderHook(useCommonHookTest, {
56 | wrapper,
57 | initialProps: value,
58 | });
59 | expect(result.current.current).toBe(0);
60 | rerender({initialProps: ++value});
61 | expect(result.current.current).toBe(0);
62 | });
63 |
64 | test('Verify symbol registration for common hooks', () => {
65 | const storeMap = createStoreMap();
66 | const wrapper = ({children}) => (
67 | {children}
68 | );
69 | const useCommonHookTest = createCommonHook(() => {
70 | useCommonCallback(() => {}, []);
71 | useCommonEffect(() => {}, []);
72 | useCommonLayoutEffect(() => {}, []);
73 | useCommonMemo(() => {}, []);
74 | useCommonRef();
75 | useCommonState(null);
76 | });
77 | const {rerender} = renderHook(useCommonHookTest, {
78 | wrapper,
79 | });
80 | expect(storeMap.length).toBe(1);
81 | expect(storeMap[0] instanceof Store).toBe(true);
82 | expect(Object.getOwnPropertySymbols(storeMap).length).toBe(8);
83 | expect(
84 | Object.getOwnPropertySymbols(storeMap)
85 | .map((symbol) => getSymbolName(symbol))
86 | .every((description) =>
87 | [
88 | 'useCommonHook',
89 | 'useCommonCallback',
90 | 'useCommonEffect',
91 | 'useCommonLayoutEffect',
92 | 'useCommonMemo',
93 | 'useCommonRef',
94 | 'useCommonState',
95 | 'useSharedState',
96 | ].includes(description),
97 | ),
98 | ).toBe(true);
99 | rerender();
100 | expect(storeMap.length).toBe(1);
101 | expect(storeMap[0] instanceof Store).toBe(true);
102 | expect(Object.getOwnPropertySymbols(storeMap).length).toBe(8);
103 | expect(
104 | Object.getOwnPropertySymbols(storeMap)
105 | .map((symbol) => getSymbolName(symbol))
106 | .every((description) =>
107 | [
108 | 'useCommonHook',
109 | 'useCommonCallback',
110 | 'useCommonEffect',
111 | 'useCommonLayoutEffect',
112 | 'useCommonMemo',
113 | 'useCommonRef',
114 | 'useCommonState',
115 | 'useSharedState',
116 | ].includes(description),
117 | ),
118 | ).toBe(true);
119 | });
120 |
121 | test('Verify the call order for common hooks of the same scope is preserved', () => {
122 | const storeMap = createStoreMap();
123 | const wrapper = ({children}) => (
124 | {children}
125 | );
126 | const useCommonHookTest = createCommonHook(() => {
127 | useCommonCallback(() => {}, []);
128 | useCommonEffect(() => {}, []);
129 | useCommonLayoutEffect(() => {}, []);
130 | useCommonMemo(() => {}, []);
131 | useCommonRef();
132 | useCommonState(null);
133 | });
134 | const callOrder = [
135 | 'useCommonCallback',
136 | 'useCommonEffect',
137 | 'useCommonLayoutEffect',
138 | 'useCommonMemo',
139 | 'useCommonRef',
140 | 'useCommonState',
141 | ];
142 | const {rerender} = renderHook(useCommonHookTest, {
143 | wrapper,
144 | });
145 | const {callPositions} = getValueBySymbolName('useCommonHook', storeMap);
146 | expect(callPositions.length).toBe(6);
147 | expect(callPositions.every((fn, index) => fn.name === callOrder[index])).toBe(
148 | true,
149 | );
150 | rerender();
151 | expect(callPositions.length).toBe(6);
152 | expect(callPositions.every((fn, index) => fn.name === callOrder[index])).toBe(
153 | true,
154 | );
155 | });
156 |
157 | test('Verify the call orders for common hooks of multiple scopes are preserved', () => {
158 | const storeMap = createStoreMap();
159 | const wrapper = ({children}) => (
160 | {children}
161 | );
162 | const useCommonHookTest1 = createCommonHook(function useCommonHookTest1() {
163 | useCommonCallback(() => {}, []);
164 | useCommonEffect(() => {}, []);
165 | useCommonLayoutEffect(() => {}, []);
166 | useCommonMemo(() => {}, []);
167 | useCommonRef();
168 | useCommonState(null);
169 | });
170 | const callOrder1 = [
171 | 'useCommonCallback',
172 | 'useCommonEffect',
173 | 'useCommonLayoutEffect',
174 | 'useCommonMemo',
175 | 'useCommonRef',
176 | 'useCommonState',
177 | ];
178 | const useCommonHookTest2 = createCommonHook(function useCommonHookTest2() {
179 | useCommonState(null);
180 | useCommonRef();
181 | useCommonMemo(() => {}, []);
182 | useCommonLayoutEffect(() => {}, []);
183 | useCommonEffect(() => {}, []);
184 | useCommonCallback(() => {}, []);
185 | });
186 | const callOrder2 = [...callOrder1].reverse();
187 | const {rerender: rerender1} = renderHook(useCommonHookTest1, {
188 | wrapper,
189 | });
190 | const {rerender: rerender2} = renderHook(useCommonHookTest2, {
191 | wrapper,
192 | });
193 |
194 | const scope1 = getValueBySymbolName('useCommonHookTest1', storeMap);
195 | const scope2 = getValueBySymbolName('useCommonHookTest2', storeMap);
196 | const {callPositions: callPositions1} = scope1;
197 | const {callPositions: callPositions2} = scope2;
198 | function validate() {
199 | expect(callPositions1.length).toBe(callOrder1.length);
200 | expect(
201 | callPositions1.every((fn, index) => fn.name === callOrder1[index]),
202 | ).toBe(true);
203 | expect(callPositions2.length).toBe(callOrder2.length);
204 | expect(
205 | callPositions2.every((fn, index) => fn.name === callOrder2[index]),
206 | ).toBe(true);
207 | }
208 | validate();
209 | rerender1();
210 | validate();
211 | rerender2();
212 | validate();
213 | });
214 |
215 | test('Verify the call orders for common hooks of nested scopes are preserved', () => {
216 | const storeMap = createStoreMap();
217 | const wrapper = ({children}) => (
218 | {children}
219 | );
220 | const useCommonHookTest1 = createCommonHook(function useCommonHookTest1() {
221 | useCommonCallback(() => {}, []);
222 | useCommonEffect(() => {}, []);
223 | useCommonLayoutEffect(() => {}, []);
224 | useCommonMemo(() => {}, []);
225 | useCommonRef();
226 | useCommonState(null);
227 | });
228 | const callOrder1 = [
229 | 'useCommonCallback',
230 | 'useCommonEffect',
231 | 'useCommonLayoutEffect',
232 | 'useCommonMemo',
233 | 'useCommonRef',
234 | 'useCommonState',
235 | ];
236 | const useCommonHookTest2 = createCommonHook(function useCommonHookTest2() {
237 | useCommonState(null);
238 | useCommonRef();
239 | useCommonMemo(() => {}, []);
240 | useCommonHookTest1();
241 | useCommonLayoutEffect(() => {}, []);
242 | useCommonEffect(() => {}, []);
243 | useCommonCallback(() => {}, []);
244 | });
245 | const callOrder2 = [
246 | 'useCommonState',
247 | 'useCommonRef',
248 | 'useCommonMemo',
249 | 'useCommonLayoutEffect',
250 | 'useCommonEffect',
251 | 'useCommonCallback',
252 | ];
253 | const useCommonHookTest3 = createCommonHook(function useCommonHookTest3() {
254 | useCommonState(null);
255 | useCommonRef();
256 | useCommonHookTest1();
257 | useCommonHookTest2();
258 | useCommonMemo(() => {}, []);
259 | useCommonHookTest1();
260 | useCommonLayoutEffect(() => {}, []);
261 | useCommonEffect(() => {}, []);
262 | useCommonCallback(() => {}, []);
263 | });
264 | const callOrder3 = [
265 | 'useCommonState',
266 | 'useCommonRef',
267 | 'useCommonMemo',
268 | 'useCommonLayoutEffect',
269 | 'useCommonEffect',
270 | 'useCommonCallback',
271 | ];
272 |
273 | const {rerender: rerender1} = renderHook(useCommonHookTest1, {
274 | wrapper,
275 | });
276 | const {rerender: rerender2} = renderHook(useCommonHookTest2, {
277 | wrapper,
278 | });
279 | const {rerender: rerender3} = renderHook(useCommonHookTest3, {
280 | wrapper,
281 | });
282 |
283 | const scope1 = getValueBySymbolName('useCommonHookTest1', storeMap);
284 | const scope2 = getValueBySymbolName('useCommonHookTest2', storeMap);
285 | const scope3 = getValueBySymbolName('useCommonHookTest3', storeMap);
286 | const {callPositions: callPositions1} = scope1;
287 | const {callPositions: callPositions2} = scope2;
288 | const {callPositions: callPositions3} = scope3;
289 |
290 | function validate() {
291 | expect(callPositions1.length).toBe(callOrder1.length);
292 | expect(
293 | callPositions1.every((fn, index) => fn.name === callOrder1[index]),
294 | ).toBe(true);
295 |
296 | expect(callPositions2.length).toBe(callOrder2.length);
297 | expect(
298 | callPositions2.every((fn, index) => fn.name === callOrder2[index]),
299 | ).toBe(true);
300 |
301 | expect(callPositions3.length).toBe(callOrder3.length);
302 | expect(
303 | callPositions3.every((fn, index) => fn.name === callOrder3[index]),
304 | ).toBe(true);
305 | }
306 | validate();
307 | rerender1();
308 | validate();
309 | rerender2();
310 | validate();
311 | rerender3();
312 | validate();
313 | });
314 |
315 | test('Verify the currentScope of nested scopes is respected', () => {
316 | const storeMap = createStoreMap();
317 | const wrapper = ({children}) => (
318 | {children}
319 | );
320 | const useCommonHookTest1 = createCommonHook(function useCommonHookTest1() {
321 | const scope1 = getValueBySymbolName('useCommonHookTest1', storeMap);
322 | expect(storeMap.currentScope).toBe(scope1);
323 | });
324 | const useCommonHookTest2 = createCommonHook(function useCommonHookTest2() {
325 | const scope2 = getValueBySymbolName('useCommonHookTest2', storeMap);
326 | expect(storeMap.currentScope).toBe(scope2);
327 | useCommonHookTest1();
328 | expect(storeMap.currentScope).toBe(scope2);
329 | });
330 | const useCommonHookTest3 = createCommonHook(function useCommonHookTest3() {
331 | const scope3 = getValueBySymbolName('useCommonHookTest3', storeMap);
332 | expect(storeMap.currentScope).toBe(scope3);
333 | useCommonHookTest1();
334 | expect(storeMap.currentScope).toBe(scope3);
335 | useCommonHookTest2();
336 | expect(storeMap.currentScope).toBe(scope3);
337 | useCommonHookTest1();
338 | expect(storeMap.currentScope).toBe(scope3);
339 | });
340 | const {rerender: rerender1} = renderHook(useCommonHookTest1, {
341 | wrapper,
342 | });
343 | const {rerender: rerender2} = renderHook(useCommonHookTest2, {
344 | wrapper,
345 | });
346 | const {rerender: rerender3} = renderHook(useCommonHookTest3, {
347 | wrapper,
348 | });
349 | expect(storeMap.currentScope).toBe(null);
350 | rerender1();
351 | expect(storeMap.currentScope).toBe(null);
352 | rerender2();
353 | expect(storeMap.currentScope).toBe(null);
354 | rerender3();
355 | expect(storeMap.currentScope).toBe(null);
356 | });
357 |
358 | test('Verify the call positions do not collide', () => {
359 | const storeMap = createStoreMap();
360 | const wrapper = ({children}) => (
361 | {children}
362 | );
363 | const useCommonHookTest1 = createCommonHook(function useCommonHookTest1() {
364 | return [useCommonRef(), useCommonRef(), useCommonRef(), useCommonRef()];
365 | });
366 | const useCommonHookTest2 = createCommonHook(function useCommonHookTest2() {
367 | return [useCommonRef(), ...useCommonHookTest1()];
368 | });
369 | const useCommonHookTest3 = createCommonHook(function useCommonHookTest3() {
370 | return [useCommonRef(), ...useCommonHookTest2()];
371 | });
372 | const {result: result1} = renderHook(useCommonHookTest1, {
373 | wrapper,
374 | });
375 | const {result: result2} = renderHook(useCommonHookTest2, {
376 | wrapper,
377 | });
378 | const {result: result3} = renderHook(useCommonHookTest3, {
379 | wrapper,
380 | });
381 |
382 | expect(new Set(result1.current).size).toBe(result1.current.length);
383 | expect(new Set(result2.current).size).toBe(result2.current.length);
384 | expect(new Set(result3.current).size).toBe(result3.current.length);
385 | });
386 |
387 | test('Verify the commonality of common hooks for a scope across rerenders', () => {
388 | const storeMap = createStoreMap();
389 | const wrapper = ({children}) => (
390 | {children}
391 | );
392 | const useCommonHookTest = createCommonHook(function useCommonHookTest() {
393 | return [useCommonRef(), useCommonRef(), useCommonRef(), useCommonRef()];
394 | });
395 | const {result, rerender} = renderHook(useCommonHookTest, {
396 | wrapper,
397 | });
398 |
399 | const refs = new Set(result.current);
400 | rerender();
401 | expect(result.current.every((ref) => refs.has(ref))).toBe(true);
402 | rerender();
403 | expect(result.current.every((ref) => refs.has(ref))).toBe(true);
404 | });
405 |
406 | test('Verify the commonality of common hooks for nested scopes', () => {
407 | const storeMap = createStoreMap();
408 | const wrapper = ({children}) => (
409 | {children}
410 | );
411 | const useCommonHookTest1 = createCommonHook(function useCommonHookTest1() {
412 | return [useCommonRef(), useCommonRef(), useCommonRef(), useCommonRef()];
413 | });
414 | const useCommonHookTest2 = createCommonHook(function useCommonHookTest2() {
415 | return useCommonHookTest1();
416 | });
417 | const {result: result1, rerender: rerender1} = renderHook(
418 | useCommonHookTest1,
419 | {
420 | wrapper,
421 | },
422 | );
423 | const {result: result2, rerender: rerender2} = renderHook(
424 | useCommonHookTest2,
425 | {
426 | wrapper,
427 | },
428 | );
429 |
430 | const refs = new Set(result1.current);
431 | expect(result1.current.every((ref) => refs.has(ref))).toBe(true);
432 | expect(result2.current.every((ref) => refs.has(ref))).toBe(true);
433 | rerender1();
434 | expect(result1.current.every((ref) => refs.has(ref))).toBe(true);
435 | expect(result2.current.every((ref) => refs.has(ref))).toBe(true);
436 | rerender2();
437 | expect(result1.current.every((ref) => refs.has(ref))).toBe(true);
438 | expect(result2.current.every((ref) => refs.has(ref))).toBe(true);
439 | });
440 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/__tests__/create-shared-reducer.test.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | // Verify referential integrity of state and dispatch across call positions and renders
11 | // Verify dispatch causes one rerender per call position
12 | // Verify no rerenders are triggered when returning current state from a reducer
13 | // Verify initialState passes dispatch as prop when passed a callback
14 | // Verify initialState initializes with returned value when passed a callback
15 | // Verify initialState sets returned value when the passed callback and then sets state asynchrounously with dispatch
16 | // Verify useSelector returns only the selected state
17 | // Verify useSelector causes rerender only when the selected state has changed
18 | // Verify one listener per call position
19 | // Verify useSelector causes rerender according to the equalityFn result
20 | // Verify timeVaryingFn limits rerenders
21 |
22 | import React from 'react';
23 | import {renderHook, act} from '@testing-library/react-hooks';
24 | import {createSharedReducer, Provider, createStoreMap} from '../src';
25 |
26 | const storeMap = createStoreMap();
27 | const wrapper = ({children}) => (
28 | {children}
29 | );
30 | const vehicleObj = {
31 | name: 'my car',
32 | type: 'convertible',
33 | rhd: false,
34 | };
35 | const vehicleObj2 = {
36 | name: 'my new car',
37 | type: 'suv',
38 | rhd: true,
39 | };
40 | const vehicleObj3 = {
41 | name: 'my new car',
42 | type: 'amphibious',
43 | rhd: true,
44 | };
45 | const store = {
46 | vehicle: vehicleObj,
47 | count: 0,
48 | location: {name: 'Toronto'},
49 | };
50 | const reducer = (state, action) => {
51 | switch (action.type) {
52 | case 'increment':
53 | return {...state, count: state.count + 1};
54 | case 'decrement':
55 | return {...state, count: state.count - 1};
56 | case 'abort-update':
57 | return state;
58 | case 'update-vehicle':
59 | return {...state, vehicle: action.value};
60 | default:
61 | throw new Error(`Unknown action type ${action.type}`);
62 | }
63 | };
64 |
65 | test('Verify referential integrity of state and dispatch across call positions and renders', () => {
66 | let store = {
67 | count: 0,
68 | };
69 | const value1 = store;
70 | let value2;
71 | const reducerFn = (state, action) => {
72 | value2 = reducer(state, action);
73 | return value2;
74 | };
75 | const [useSelector, useDispatch] = createSharedReducer(reducerFn, store);
76 | function useTestState1() {
77 | const state = useSelector();
78 | const dispatch = useDispatch();
79 | return {state, dispatch};
80 | }
81 | function useTestState2() {
82 | const state = useSelector();
83 | const dispatch = useDispatch();
84 | return {state, dispatch};
85 | }
86 | const {result: result1} = renderHook(useTestState1, {wrapper});
87 | const {result: result2} = renderHook(useTestState2, {wrapper});
88 | const {dispatch} = result1.current;
89 | expect(result1.current.state).toBe(value1);
90 | expect(result1.current.state).toBe(result2.current.state);
91 | expect(result1.current.dispatch).toBe(result2.current.dispatch);
92 | const action = {type: 'increment'};
93 | act(() => dispatch(action));
94 | expect(typeof result1.current.state).toBe('object');
95 | expect(result1.current.state).toBe(value2);
96 | expect(result1.current.state).toBe(result2.current.state);
97 | expect(result1.current.dispatch).toBe(dispatch);
98 | expect(result1.current.dispatch).toBe(result2.current.dispatch);
99 | });
100 |
101 | test('Verify dispatch causes one rerender per call position', () => {
102 | let count1 = 0;
103 | let count2 = 0;
104 | const [useSelector, useDispatch] = createSharedReducer(reducer, store);
105 | function useTestState1() {
106 | count1++;
107 | const state = useSelector();
108 | const dispatch = useDispatch();
109 | return {state, dispatch};
110 | }
111 | function useTestState2() {
112 | count2++;
113 | const state = useSelector();
114 | const dispatch = useDispatch();
115 | return {state, dispatch};
116 | }
117 | const {result: result1} = renderHook(useTestState1, {wrapper});
118 | expect(count1).toBe(1);
119 | renderHook(useTestState2, {wrapper});
120 | const {dispatch} = result1.current;
121 | expect(count2).toBe(1);
122 | const action = {type: 'increment'};
123 | act(() => dispatch(action));
124 | expect(count1).toBe(2);
125 | expect(count2).toBe(2);
126 | });
127 |
128 | test('Verify no rerenders are triggered when returning current state from a reducer', () => {
129 | let count1 = 0;
130 | let count2 = 0;
131 | const [useSelector, useDispatch] = createSharedReducer(reducer, store);
132 | function useTestState1() {
133 | count1++;
134 | const state = useSelector();
135 | const dispatch = useDispatch();
136 | return {state, dispatch};
137 | }
138 | function useTestState2() {
139 | count2++;
140 | const state = useSelector();
141 | const dispatch = useDispatch();
142 | return {state, dispatch};
143 | }
144 | const {result: result1} = renderHook(useTestState1, {wrapper});
145 | expect(count1).toBe(1);
146 | renderHook(useTestState2, {wrapper});
147 | expect(count2).toBe(1);
148 | const {dispatch} = result1.current;
149 | const action = {type: 'abort-update'};
150 | act(() => dispatch(action));
151 | expect(count1).toBe(1);
152 | expect(count2).toBe(1);
153 | });
154 |
155 | test('Verify initialState passes dispatch as prop when passed a callback', () => {
156 | let dispatchArg;
157 | const [useSelector, useDispatch] = createSharedReducer(
158 | reducer,
159 | (dispatch) => {
160 | dispatchArg = dispatch;
161 | return store;
162 | },
163 | );
164 | function useTestState() {
165 | const state = useSelector();
166 | const dispatch = useDispatch();
167 | return {state, dispatch};
168 | }
169 | const {result} = renderHook(useTestState, {wrapper});
170 | const {dispatch} = result.current;
171 | expect(dispatchArg).toBe(dispatch);
172 | });
173 |
174 | test('Verify initialState initializes with returned value when passed a callback', () => {
175 | let value = store;
176 | const [useSelector, useDispatch] = createSharedReducer(
177 | reducer,
178 | (dispatch) => {
179 | return store;
180 | },
181 | );
182 | function useTestState() {
183 | const state = useSelector();
184 | const dispatch = useDispatch();
185 | return {state, dispatch};
186 | }
187 | const {result} = renderHook(useTestState, {wrapper});
188 | const {state} = result.current;
189 | expect(state).toBe(value);
190 | });
191 |
192 | test('Verify initialState sets returned value when the passed callback and then sets state asynchrounously with dispatch', async () => {
193 | expect.assertions(3);
194 | const value1 = store;
195 | let value2;
196 | const reducerFn = (state, action) => {
197 | value2 = reducer(state, action);
198 | return value2;
199 | };
200 | const [useSelector, useDispatch] = createSharedReducer(
201 | reducerFn,
202 | (dispatch) => {
203 | setTimeout(() => {
204 | act(() => {
205 | const action = {type: 'increment'};
206 | dispatch(action);
207 | });
208 | }, 0);
209 | return store;
210 | },
211 | );
212 | function useTestState() {
213 | const state = useSelector();
214 | const dispatch = useDispatch();
215 | return {state, dispatch};
216 | }
217 | const {result} = renderHook(useTestState, {wrapper});
218 | expect(result.current.state).toBe(value1);
219 | await new Promise((resolve) => setTimeout(resolve, 0));
220 | expect(typeof result.current.state).toBe('object');
221 | expect(result.current.state).toBe(value2);
222 | });
223 |
224 | test('Verify useSelector returns only the selected state', () => {
225 | const [useSelector, useDispatch] = createSharedReducer(reducer, store);
226 | function useTestState(selector) {
227 | const state = useSelector(selector);
228 | const dispatch = useDispatch();
229 | return {state, dispatch};
230 | }
231 | const {result} = renderHook(useTestState, {
232 | wrapper,
233 | initialProps: (state) => state.vehicle,
234 | });
235 | const {state} = result.current;
236 | expect(state).toBe(vehicleObj);
237 | });
238 |
239 | test('Verify useSelector causes rerender only when the selected state has changed', () => {
240 | let count = 0;
241 | const [useSelector, useDispatch] = createSharedReducer(reducer, store);
242 | function useTestState({selector, equalityFn}) {
243 | count++;
244 | const state = useSelector(selector, equalityFn);
245 | return state;
246 | }
247 | const {result: dispatchResult} = renderHook(useDispatch, {
248 | wrapper,
249 | });
250 | const dispatch = dispatchResult.current;
251 | const {result} = renderHook(useTestState, {
252 | wrapper,
253 | initialProps: {
254 | selector: (state) => state.vehicle,
255 | equalityFn: (curr, next) => curr?.name === next?.name,
256 | },
257 | });
258 | expect(count).toBe(1);
259 | act(() => dispatch({type: 'abort-update'}));
260 | expect(count).toBe(1);
261 | act(() => dispatch({type: 'increment'}));
262 | expect(count).toBe(1);
263 | act(() =>
264 | dispatch({
265 | type: 'update-vehicle',
266 | value: vehicleObj2,
267 | }),
268 | );
269 | expect(count).toBe(2);
270 | act(() =>
271 | dispatch({
272 | type: 'update-vehicle',
273 | value: vehicleObj3,
274 | }),
275 | );
276 | act(() => dispatch({type: 'abort-update'}));
277 | /*
278 | Issue with react-test-renderer causes an unnecessary rerender which causes
279 | the following test to fail. I was able to verify that this is working in a
280 | test application
281 | */
282 | // expect(count).toBe(2);
283 | expect(result.current).toBe(vehicleObj2);
284 | });
285 |
286 | test('Verify one listener per call position', () => {
287 | const storeMap = createStoreMap();
288 | const wrapper = ({children}) => (
289 | {children}
290 | );
291 | const [useSelector] = createSharedReducer(reducer, store);
292 | function useTestState1() {
293 | useSelector();
294 | }
295 | function useTestState2() {
296 | useSelector();
297 | }
298 | function useTestState3() {
299 | useSelector();
300 | }
301 | const {rerender: rerender1} = renderHook(useTestState1, {
302 | wrapper,
303 | });
304 | expect(storeMap[0].listeners.size).toBe(1);
305 | rerender1();
306 | expect(storeMap[0].listeners.size).toBe(1);
307 | const {rerender: rerender2} = renderHook(useTestState2, {
308 | wrapper,
309 | });
310 | expect(storeMap[0].listeners.size).toBe(2);
311 | rerender1();
312 | expect(storeMap[0].listeners.size).toBe(2);
313 | rerender2();
314 | expect(storeMap[0].listeners.size).toBe(2);
315 | const {rerender: rerender3} = renderHook(useTestState3, {
316 | wrapper,
317 | });
318 | expect(storeMap[0].listeners.size).toBe(3);
319 | rerender1();
320 | expect(storeMap[0].listeners.size).toBe(3);
321 | rerender2();
322 | expect(storeMap[0].listeners.size).toBe(3);
323 | rerender3();
324 | expect(storeMap[0].listeners.size).toBe(3);
325 | });
326 |
327 | test('Verify useSelector causes rerender according to the equalityFn result', () => {
328 | let count = 0;
329 | let equalityFnResult = true;
330 | const [useSelector, useDispatch] = createSharedReducer(reducer, store);
331 | function useTestState({selector, equalityFn}) {
332 | count++;
333 | const state = useSelector(selector, equalityFn);
334 | return state;
335 | }
336 | const {result: dispatchResult} = renderHook(useDispatch, {
337 | wrapper,
338 | });
339 | const dispatch = dispatchResult.current;
340 | renderHook(useTestState, {
341 | wrapper,
342 | initialProps: {
343 | selector: (state) => state.count,
344 | equalityFn: () => equalityFnResult,
345 | },
346 | });
347 | expect(count).toBe(1);
348 | act(() => dispatch({type: 'increment'}));
349 | expect(count).toBe(1);
350 | equalityFnResult = false;
351 | act(() => dispatch({type: 'increment'}));
352 | expect(count).toBe(2);
353 | act(() =>
354 | dispatch({
355 | type: 'update-vehicle',
356 | value: vehicleObj2,
357 | }),
358 | );
359 | expect(count).toBe(3);
360 | });
361 |
362 | test('Verify timeVaryingFn limits rerenders', () => {
363 | let count = 0;
364 | const [useSelector, useDispatch] = createSharedReducer(reducer, store);
365 | function useTestState({selector, equalityFn, timeVaryingFn}) {
366 | count++;
367 | const state = useSelector(selector, equalityFn, timeVaryingFn);
368 | return state;
369 | }
370 | const {result: dispatchResult} = renderHook(useDispatch, {
371 | wrapper,
372 | });
373 | const dispatch = dispatchResult.current;
374 | const {result} = renderHook(useTestState, {
375 | wrapper,
376 | initialProps: {
377 | selector: null,
378 | equalityFn: null,
379 | timeVaryingFn: (fn) => {
380 | let callCount = 0;
381 | return (...args) => {
382 | if (++callCount % 3 === 0) {
383 | return fn(...args);
384 | }
385 | };
386 | },
387 | },
388 | });
389 | expect(count).toBe(1);
390 | expect(result.current.count).toBe(0);
391 | act(() => dispatch({type: 'increment'}));
392 | expect(count).toBe(1);
393 | expect(result.current.count).toBe(0);
394 | act(() => dispatch({type: 'increment'}));
395 | expect(count).toBe(1);
396 | expect(result.current.count).toBe(0);
397 | act(() => dispatch({type: 'increment'}));
398 | expect(count).toBe(2);
399 | expect(result.current.count).toBe(3);
400 | act(() => dispatch({type: 'increment'}));
401 | expect(count).toBe(2);
402 | expect(result.current.count).toBe(3);
403 | act(() => dispatch({type: 'increment'}));
404 | expect(count).toBe(2);
405 | expect(result.current.count).toBe(3);
406 | act(() => dispatch({type: 'increment'}));
407 | expect(count).toBe(3);
408 | expect(result.current.count).toBe(6);
409 | });
410 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/__tests__/create-shared-ref.test.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | // Verify useSharedRef returns proper stucture when no arguments are passed
11 | // Verify referential integrity of ref across call positions
12 | // Verify args passed to useSharedRef are included in the return value
13 |
14 | import React from 'react';
15 | import {renderHook} from '@testing-library/react-hooks';
16 | import {createSharedRef, Provider, createStoreMap} from '../src';
17 |
18 | const storeMap = createStoreMap();
19 | const wrapper = ({children}) => (
20 | {children}
21 | );
22 |
23 | test('Verify useSharedRef returns proper stucture when no arguments are passed', () => {
24 | const useSharedRef = createSharedRef();
25 | const {result} = renderHook(useSharedRef, {wrapper});
26 | expect(typeof result.current).toBe('object');
27 | expect(result.current.constructor.name).toBe('Object');
28 | expect(Object.keys(result.current).length).toBe(1);
29 | expect('current' in result.current).toBe(true);
30 | expect(result.current.curent).toBe(undefined);
31 | });
32 |
33 | test('Verify referential integrity of ref across call positions', () => {
34 | const useSharedRef = createSharedRef();
35 | const {result: result1, rerender: rerender1} = renderHook(useSharedRef, {
36 | wrapper,
37 | });
38 | const {result: result2, rerender: rerender2} = renderHook(useSharedRef, {
39 | wrapper,
40 | });
41 | expect(result1.current).toBe(result2.current);
42 | rerender1();
43 | expect(result1.current).toBe(result2.current);
44 | rerender2();
45 | expect(result1.current).toBe(result2.current);
46 | });
47 |
48 | test('Verify args passed to useSharedRef are included in the return value', () => {
49 | const value = {};
50 | const useSharedRef = createSharedRef(value);
51 | const {result, rerender} = renderHook(useSharedRef, {wrapper});
52 | expect(result.current.current).toBe(value);
53 | rerender();
54 | expect(result.current.current).toBe(value);
55 | });
56 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/__tests__/create-shared-state.test.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | // Verify referential integrity of state and setState across call positions and renders
11 | // Verify setState causes one rerender per call position
12 | // Verify no rerenders are triggered when returning current state from a setState callback
13 | // Verify setState passes current state as prop when passed a callback
14 | // Verify initialState passes setState as prop when passed a callback
15 | // Verify initialState initializes with returned value when passed a callback
16 | // Verify initialState sets returned value when the passed callback and then sets state asynchrounously with setState
17 | // Verify useSelector returns only the selected state
18 | // Verify useSelector causes rerender only when the selected state has changed
19 | // Verify one listener per call position
20 | // Verify useSelector causes rerender according to the equalityFn result
21 | // Verify timeVaryingFn limits rerenders
22 |
23 | import React from 'react';
24 | import {renderHook, act} from '@testing-library/react-hooks';
25 | import {createSharedState, Provider, createStoreMap} from '../src';
26 |
27 | const storeMap = createStoreMap();
28 | const wrapper = ({children}) => (
29 | {children}
30 | );
31 |
32 | test('Verify referential integrity of state and setState across call positions and renders', () => {
33 | let value = {};
34 | const [useSelector, useSetState] = createSharedState(value);
35 | function useTestState1() {
36 | const state = useSelector();
37 | const setState = useSetState();
38 | return {state, setState};
39 | }
40 | function useTestState2() {
41 | const state = useSelector();
42 | const setState = useSetState();
43 | return {state, setState};
44 | }
45 | const {result: result1} = renderHook(useTestState1, {wrapper});
46 | const {result: result2} = renderHook(useTestState2, {wrapper});
47 | const {setState} = result1.current;
48 | expect(result1.current.state).toBe(value);
49 | expect(result1.current.state).toBe(result2.current.state);
50 | expect(result1.current.setState).toBe(result2.current.setState);
51 | value = {};
52 | act(() => setState(value));
53 | expect(result1.current.state).toBe(value);
54 | expect(result1.current.state).toBe(result2.current.state);
55 | expect(result1.current.setState).toBe(setState);
56 | expect(result1.current.setState).toBe(result2.current.setState);
57 | });
58 |
59 | test('Verify setState causes one rerender per call position', () => {
60 | let count1 = 0;
61 | let count2 = 0;
62 | let value = 0;
63 | const [useSelector, useSetState] = createSharedState(value);
64 | function useTestState1() {
65 | count1++;
66 | const state = useSelector();
67 | const setState = useSetState();
68 | return {state, setState};
69 | }
70 | function useTestState2() {
71 | count2++;
72 | const state = useSelector();
73 | const setState = useSetState();
74 | return {state, setState};
75 | }
76 | const {result: result1} = renderHook(useTestState1, {wrapper});
77 | expect(count1).toBe(1);
78 | renderHook(useTestState2, {wrapper});
79 | const {setState} = result1.current;
80 | expect(count2).toBe(1);
81 | value++;
82 | act(() => setState(value));
83 | expect(count1).toBe(2);
84 | expect(count2).toBe(2);
85 | });
86 |
87 | test('Verify no rerenders are triggered when returning current state from a setState callback', () => {
88 | let count1 = 0;
89 | let count2 = 0;
90 | let value = 0;
91 | const [useSelector, useSetState] = createSharedState(value);
92 | function useTestState1() {
93 | count1++;
94 | const state = useSelector();
95 | const setState = useSetState();
96 | return {state, setState};
97 | }
98 | function useTestState2() {
99 | count2++;
100 | const state = useSelector();
101 | const setState = useSetState();
102 | return {state, setState};
103 | }
104 | const {result: result1} = renderHook(useTestState1, {wrapper});
105 | expect(count1).toBe(1);
106 | renderHook(useTestState2, {wrapper});
107 | expect(count2).toBe(1);
108 | const {setState} = result1.current;
109 | act(() => setState((state) => state));
110 | expect(count1).toBe(1);
111 | expect(count2).toBe(1);
112 | });
113 |
114 | test('Verify setState passes current state as prop when passed a callback', () => {
115 | const [useSelector, useSetState] = createSharedState({});
116 | function useTestState() {
117 | const state = useSelector();
118 | const setState = useSetState();
119 | return {state, setState};
120 | }
121 | const {result} = renderHook(useTestState, {wrapper});
122 | renderHook(useTestState, {wrapper});
123 | const {state, setState} = result.current;
124 | let stateArg;
125 | act(() =>
126 | setState((state) => {
127 | stateArg = state;
128 | return state;
129 | }),
130 | );
131 | expect(stateArg).toBe(state);
132 | });
133 |
134 | test('Verify initialState passes setState as prop when passed a callback', () => {
135 | let setStateArg;
136 | const [useSelector, useSetState] = createSharedState((setState) => {
137 | setStateArg = setState;
138 | return 0;
139 | });
140 | function useTestState() {
141 | const state = useSelector();
142 | const setState = useSetState();
143 | return {state, setState};
144 | }
145 | const {result} = renderHook(useTestState, {wrapper});
146 | const {setState} = result.current;
147 | expect(setStateArg).toBe(setState);
148 | });
149 |
150 | test('Verify initialState initializes with returned value when passed a callback', () => {
151 | const value = {};
152 | const [useSelector, useSetState] = createSharedState((setState) => {
153 | return value;
154 | });
155 | function useTestState() {
156 | const state = useSelector();
157 | const setState = useSetState();
158 | return {state, setState};
159 | }
160 | const {result} = renderHook(useTestState, {wrapper});
161 | const {state} = result.current;
162 | expect(state).toBe(value);
163 | });
164 |
165 | test('Verify initialState sets returned value when the passed callback and then sets state asynchrounously with setState', async () => {
166 | const value1 = 0;
167 | const value2 = 1;
168 | expect.assertions(2);
169 | const [useSelector, useSetState] = createSharedState((setState) => {
170 | setTimeout(() => {
171 | act(() => {
172 | setState((state) => state + 1);
173 | });
174 | }, 0);
175 | return value1;
176 | });
177 | function useTestState() {
178 | const state = useSelector();
179 | const setState = useSetState();
180 | return {state, setState};
181 | }
182 | const {result} = renderHook(useTestState, {wrapper});
183 | expect(result.current.state).toBe(value1);
184 | await new Promise((resolve) => setTimeout(resolve, 0));
185 | expect(result.current.state).toBe(value2);
186 | });
187 |
188 | test('Verify useSelector returns only the selected state', () => {
189 | const vehicleObj = {
190 | name: 'my car',
191 | type: 'convertible',
192 | rhd: false,
193 | };
194 | const value = {
195 | vehicle: vehicleObj,
196 | count: 0,
197 | location: {name: 'Toronto'},
198 | };
199 | const [useSelector, useSetState] = createSharedState(value);
200 | function useTestState(selector) {
201 | const state = useSelector(selector);
202 | const setState = useSetState();
203 | return {state, setState};
204 | }
205 | const {result} = renderHook(useTestState, {
206 | wrapper,
207 | initialProps: (state) => state.vehicle,
208 | });
209 | const {state} = result.current;
210 | expect(state).toBe(vehicleObj);
211 | });
212 |
213 | test('Verify useSelector causes rerender only when the selected state has changed', () => {
214 | let count = 0;
215 | const vehicleObj = {
216 | name: 'my car',
217 | type: 'convertible',
218 | rhd: false,
219 | };
220 | const vehicleObj2 = {
221 | name: 'my new car',
222 | type: 'suv',
223 | rhd: true,
224 | };
225 | const vehicleObj3 = {
226 | name: 'my new car',
227 | type: 'amphibious',
228 | rhd: true,
229 | };
230 | const value = {
231 | vehicle: vehicleObj,
232 | count: 0,
233 | location: {name: 'Toronto'},
234 | };
235 | const [useSelector, useSetState] = createSharedState(value);
236 | function useTestState({selector, equalityFn}) {
237 | count++;
238 | const state = useSelector(selector, equalityFn);
239 | return state;
240 | }
241 | const {result: setStateResult} = renderHook(useSetState, {
242 | wrapper,
243 | });
244 | const setState = setStateResult.current;
245 | const {result} = renderHook(useTestState, {
246 | wrapper,
247 | initialProps: {
248 | selector: (state) => state.vehicle,
249 | equalityFn: (curr, next) => curr?.name === next?.name,
250 | },
251 | });
252 | expect(count).toBe(1);
253 | act(() => setState((state) => state));
254 | expect(count).toBe(1);
255 | act(() => setState((state) => ({...state, count: state.count + 1})));
256 | expect(count).toBe(1);
257 | act(() =>
258 | setState((state) => ({
259 | ...state,
260 | vehicle: vehicleObj2,
261 | })),
262 | );
263 | expect(count).toBe(2);
264 | act(() =>
265 | setState((state) => ({
266 | ...state,
267 | vehicle: vehicleObj3,
268 | })),
269 | );
270 | /*
271 | Issue with react-test-renderer causes an unnecessary rerender which causes
272 | the following test to fail. I was able to verify that this is working in a
273 | test application
274 | */
275 | // expect(count).toBe(2);
276 | expect(result.current).toBe(vehicleObj2);
277 | });
278 |
279 | test('Verify one listener per call position', () => {
280 | const vehicleObj = {
281 | name: 'my car',
282 | type: 'convertible',
283 | rhd: false,
284 | };
285 | const value = {
286 | vehicle: vehicleObj,
287 | count: 0,
288 | location: {name: 'Toronto'},
289 | };
290 | const storeMap = createStoreMap();
291 | const wrapper = ({children}) => (
292 | {children}
293 | );
294 | const [useSelector] = createSharedState(value);
295 | function useTestState1() {
296 | useSelector();
297 | }
298 | function useTestState2() {
299 | useSelector();
300 | }
301 | function useTestState3() {
302 | useSelector();
303 | }
304 | const {rerender: rerender1} = renderHook(useTestState1, {
305 | wrapper,
306 | });
307 | expect(storeMap[0].listeners.size).toBe(1);
308 | rerender1();
309 | expect(storeMap[0].listeners.size).toBe(1);
310 | const {rerender: rerender2} = renderHook(useTestState2, {
311 | wrapper,
312 | });
313 | expect(storeMap[0].listeners.size).toBe(2);
314 | rerender1();
315 | expect(storeMap[0].listeners.size).toBe(2);
316 | rerender2();
317 | expect(storeMap[0].listeners.size).toBe(2);
318 | const {rerender: rerender3} = renderHook(useTestState3, {
319 | wrapper,
320 | });
321 | expect(storeMap[0].listeners.size).toBe(3);
322 | rerender1();
323 | expect(storeMap[0].listeners.size).toBe(3);
324 | rerender2();
325 | expect(storeMap[0].listeners.size).toBe(3);
326 | rerender3();
327 | expect(storeMap[0].listeners.size).toBe(3);
328 | });
329 |
330 | test('Verify useSelector causes rerender according to the equalityFn result', () => {
331 | let count = 0;
332 | let equalityFnResult = true;
333 | const vehicleObj = {
334 | name: 'my car',
335 | type: 'convertible',
336 | rhd: false,
337 | };
338 | const vehicleObj2 = {
339 | name: 'my new car',
340 | type: 'suv',
341 | rhd: true,
342 | };
343 | const value = {
344 | vehicle: vehicleObj,
345 | count: 0,
346 | location: {name: 'Toronto'},
347 | };
348 | const [useSelector, useSetState] = createSharedState(value);
349 | function useTestState({selector, equalityFn}) {
350 | count++;
351 | const state = useSelector(selector, equalityFn);
352 | return state;
353 | }
354 | const {result: setStateResult} = renderHook(useSetState, {
355 | wrapper,
356 | });
357 | const setState = setStateResult.current;
358 | renderHook(useTestState, {
359 | wrapper,
360 | initialProps: {
361 | selector: (state) => state.count,
362 | equalityFn: () => equalityFnResult,
363 | },
364 | });
365 | expect(count).toBe(1);
366 | act(() => setState((state) => ({...state, count: state.count + 1})));
367 | expect(count).toBe(1);
368 | equalityFnResult = false;
369 | act(() => setState((state) => ({...state, count: state.count + 1})));
370 | expect(count).toBe(2);
371 | act(() =>
372 | setState((state) => ({
373 | ...state,
374 | vehicle: vehicleObj2,
375 | })),
376 | );
377 | expect(count).toBe(3);
378 | });
379 |
380 | test('Verify timeVaryingFn limits rerenders', () => {
381 | let count = 0;
382 | const vehicleObj = {
383 | name: 'my car',
384 | type: 'convertible',
385 | rhd: false,
386 | };
387 | const value = {
388 | vehicle: vehicleObj,
389 | count: 0,
390 | location: {name: 'Toronto'},
391 | };
392 | const [useSelector, useDispatch] = createSharedState(value);
393 | function useTestState({selector, equalityFn, timeVaryingFn}) {
394 | count++;
395 | const state = useSelector(selector, equalityFn, timeVaryingFn);
396 | return state;
397 | }
398 | const {result: setStateResult} = renderHook(useDispatch, {
399 | wrapper,
400 | });
401 | const setState = setStateResult.current;
402 | const {result} = renderHook(useTestState, {
403 | wrapper,
404 | initialProps: {
405 | selector: null,
406 | equalityFn: null,
407 | timeVaryingFn: (fn) => {
408 | let callCount = 0;
409 | return (...args) => {
410 | if (++callCount % 3 === 0) {
411 | return fn(...args);
412 | }
413 | };
414 | },
415 | },
416 | });
417 | expect(count).toBe(1);
418 | expect(result.current.count).toBe(0);
419 | act(() => setState((state) => ({...state, count: state.count + 1})));
420 | expect(count).toBe(1);
421 | expect(result.current.count).toBe(0);
422 | act(() => setState((state) => ({...state, count: state.count + 1})));
423 | expect(count).toBe(1);
424 | expect(result.current.count).toBe(0);
425 | act(() => setState((state) => ({...state, count: state.count + 1})));
426 | expect(count).toBe(2);
427 | expect(result.current.count).toBe(3);
428 | act(() => setState((state) => ({...state, count: state.count + 1})));
429 | expect(count).toBe(2);
430 | expect(result.current.count).toBe(3);
431 | act(() => setState((state) => ({...state, count: state.count + 1})));
432 | expect(count).toBe(2);
433 | expect(result.current.count).toBe(3);
434 | act(() => setState((state) => ({...state, count: state.count + 1})));
435 | expect(count).toBe(3);
436 | expect(result.current.count).toBe(6);
437 | });
438 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/__tests__/use-common-callback.test.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | // Verify callback changes/refreshes as args change
11 | // Verify referential integrity of callback between multiple different call positions
12 |
13 | import React from 'react';
14 | import {renderHook} from '@testing-library/react-hooks';
15 | import {
16 | Provider,
17 | createStoreMap,
18 | createCommonHook,
19 | useCommonCallback,
20 | } from '../src';
21 |
22 | const storeMap = createStoreMap();
23 | const wrapper = ({children}) => (
24 | {children}
25 | );
26 |
27 | test('Verify referential integrity of callback between multiple different call positions', () => {
28 | const useTestCallback = createCommonHook((variable) => {
29 | return useCommonCallback(() => {
30 | variable;
31 | }, [variable]);
32 | });
33 | const {result: result1} = renderHook(useTestCallback, {wrapper});
34 | const {result: result2} = renderHook(useTestCallback, {wrapper});
35 | expect(typeof result1.current).toBe('function');
36 | expect(result1.current).toEqual(result2.current);
37 | });
38 |
39 | test('Verify callback changes/refreshes as args change', () => {
40 | const useTestCallback = createCommonHook((variable) => {
41 | return useCommonCallback(() => {
42 | variable;
43 | }, [variable]);
44 | });
45 | const {result, rerender} = renderHook(useTestCallback, {
46 | wrapper,
47 | initialProps: 0,
48 | });
49 | const result1 = {...result};
50 | expect(typeof result1.current).toBe('function');
51 | rerender(0);
52 | const result2 = {...result};
53 | expect(result1.current).toEqual(result2.current);
54 | rerender(1);
55 | const result3 = {...result};
56 | expect(typeof result2.current).toBe('function');
57 | expect(result1.current).not.toEqual(result3.current);
58 | });
59 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/__tests__/use-common-effect.test.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | // Verify callback execution occurs only when expected (on first component mount, on args change)
11 | // Verify cleanup function occurs only when expected (on last component unmount, on args change)
12 |
13 | import React from 'react';
14 | import {renderHook} from '@testing-library/react-hooks';
15 | import {
16 | Provider,
17 | createStoreMap,
18 | createCommonHook,
19 | useCommonEffect,
20 | } from '../src';
21 |
22 | const storeMap = createStoreMap();
23 | const wrapper = ({children}) => (
24 | {children}
25 | );
26 |
27 | test('Verify callback execution occurs only when expected (on first component mount, on args change)', () => {
28 | let count = 0;
29 | let variable = 0;
30 | const useTestEffect = createCommonHook((variable) => {
31 | useCommonEffect(() => {
32 | count++;
33 | }, [variable]);
34 | });
35 | const {rerender: rerender1} = renderHook(useTestEffect, {
36 | wrapper,
37 | initialProps: variable,
38 | });
39 | expect(count).toBe(1);
40 | const {rerender: rerender2} = renderHook(useTestEffect, {
41 | wrapper,
42 | initialProps: variable,
43 | });
44 | expect(count).toBe(1);
45 | variable++;
46 | rerender1(variable);
47 | expect(count).toBe(2);
48 | rerender2(variable);
49 | expect(count).toBe(2);
50 | });
51 |
52 | test('Verify cleanup function occurs only when expected (on last component unmount, on args change)', () => {
53 | let cleanup = 0;
54 | let variable = 0;
55 | const useTestEffect = createCommonHook((variable) => {
56 | useCommonEffect(() => {
57 | return () => cleanup++;
58 | }, [variable]);
59 | });
60 | const {rerender: rerender1, unmount: unmount1} = renderHook(useTestEffect, {
61 | wrapper,
62 | initialProps: variable,
63 | });
64 | expect(cleanup).toBe(0);
65 | const {rerender: rerender2, unmount: unmount2} = renderHook(useTestEffect, {
66 | wrapper,
67 | initialProps: variable,
68 | });
69 | expect(cleanup).toBe(0);
70 | variable++;
71 | rerender1(variable);
72 | expect(cleanup).toBe(1);
73 | rerender2(variable);
74 | expect(cleanup).toBe(1);
75 | unmount1();
76 | expect(cleanup).toBe(1);
77 | unmount2();
78 | expect(cleanup).toBe(2);
79 | });
80 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/__tests__/use-common-layout-effect.test.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | // Verify callback execution occurs only when expected (on first component mount, on args change)
11 | // Verify cleanup function occurs only when expected (on last component unmount, on args change)
12 |
13 | import React from 'react';
14 | import {renderHook} from '@testing-library/react-hooks';
15 | import {
16 | Provider,
17 | createStoreMap,
18 | createCommonHook,
19 | useCommonLayoutEffect,
20 | } from '../src';
21 |
22 | const storeMap = createStoreMap();
23 | const wrapper = ({children}) => (
24 | {children}
25 | );
26 |
27 | test('Verify callback execution occurs only when expected (on first component mount, on args change)', () => {
28 | let count = 0;
29 | let variable = 0;
30 | const useTestLayoutEffect = createCommonHook((variable) => {
31 | useCommonLayoutEffect(() => {
32 | count++;
33 | }, [variable]);
34 | });
35 | const {rerender: rerender1} = renderHook(useTestLayoutEffect, {
36 | wrapper,
37 | initialProps: variable,
38 | });
39 | expect(count).toBe(1);
40 | const {rerender: rerender2} = renderHook(useTestLayoutEffect, {
41 | wrapper,
42 | initialProps: variable,
43 | });
44 | expect(count).toBe(1);
45 | variable++;
46 | rerender1(variable);
47 | expect(count).toBe(2);
48 | rerender2(variable);
49 | expect(count).toBe(2);
50 | });
51 |
52 | test('Verify cleanup function occurs only when expected (on last component unmount, on args change)', () => {
53 | let cleanup = 0;
54 | let variable = 0;
55 | const useTestLayoutEffect = createCommonHook((variable) => {
56 | useCommonLayoutEffect(() => {
57 | return () => cleanup++;
58 | }, [variable]);
59 | });
60 | const {rerender: rerender1, unmount: unmount1} = renderHook(
61 | useTestLayoutEffect,
62 | {
63 | wrapper,
64 | initialProps: variable,
65 | },
66 | );
67 | expect(cleanup).toBe(0);
68 | const {rerender: rerender2, unmount: unmount2} = renderHook(
69 | useTestLayoutEffect,
70 | {
71 | wrapper,
72 | initialProps: variable,
73 | },
74 | );
75 | expect(cleanup).toBe(0);
76 | variable++;
77 | rerender1(variable);
78 | expect(cleanup).toBe(1);
79 | rerender2(variable);
80 | expect(cleanup).toBe(1);
81 | unmount1();
82 | expect(cleanup).toBe(1);
83 | unmount2();
84 | expect(cleanup).toBe(2);
85 | });
86 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/__tests__/use-common-memo.test.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | // Verify referential integrity of memoized value between different call positions
11 | // Verify callback execution occurs only when expected (on first component mount, on args change)
12 |
13 | import React from 'react';
14 | import {renderHook} from '@testing-library/react-hooks';
15 | import {
16 | Provider,
17 | createStoreMap,
18 | createCommonHook,
19 | useCommonMemo,
20 | } from '../src';
21 |
22 | const storeMap = createStoreMap();
23 | const wrapper = ({children}) => (
24 | {children}
25 | );
26 |
27 | test('Verify referential integrity of memoized value between different call positions', () => {
28 | let variable = 0;
29 | let value = {};
30 | const value1 = value;
31 | const useTestMemo = createCommonHook(({value, variable}) => {
32 | return useCommonMemo(() => {
33 | return value;
34 | // eslint-disable-next-line @uber/react-global-hooks/exhaustive-deps
35 | }, [variable]);
36 | });
37 | const {result: result1, rerender: rerender1} = renderHook(useTestMemo, {
38 | wrapper,
39 | initialProps: {value, variable},
40 | });
41 | expect(result1.current).toBe(value1);
42 | const {result: result2, rerender: rerender2} = renderHook(useTestMemo, {
43 | wrapper,
44 | initialProps: {value, variable},
45 | });
46 | expect(result1.current).toBe(value1);
47 | expect(result1.current).toBe(result2.current);
48 | rerender1({value, variable});
49 | expect(result1.current).toBe(value1);
50 | expect(result1.current).toBe(result2.current);
51 | value = {};
52 | const value2 = value;
53 | rerender1({value, variable});
54 | expect(result1.current).toBe(value1);
55 | expect(result1.current).toBe(result2.current);
56 | variable++;
57 | rerender1({value, variable});
58 | rerender2({value, variable});
59 | expect(result1.current).toBe(value2);
60 | expect(result1.current).not.toBe(value1);
61 | expect(result1.current).toBe(result2.current);
62 | });
63 |
64 | test('Verify callback execution occurs only when expected (on first component mount, on args change)', () => {
65 | let variable = 0;
66 | let count = 0;
67 | let value = {};
68 | const value1 = value;
69 | const useTestMemo = createCommonHook(({value, variable}) => {
70 | return useCommonMemo(() => {
71 | count++;
72 | return value;
73 | // eslint-disable-next-line @uber/react-global-hooks/exhaustive-deps
74 | }, [variable]);
75 | });
76 | const {result, rerender} = renderHook(useTestMemo, {
77 | wrapper,
78 | initialProps: {value, variable},
79 | });
80 | expect(count).toBe(1);
81 | expect(result.current).toBe(value1);
82 | value = {};
83 | const value2 = value;
84 | rerender({value, variable});
85 | expect(count).toBe(1);
86 | expect(result.current).toBe(value1);
87 | expect(result.current).not.toBe(value2);
88 | variable++;
89 | rerender({value, variable});
90 | expect(count).toBe(2);
91 | expect(result.current).toBe(value2);
92 | });
93 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/__tests__/use-common-ref.test.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | // Verify useCommonRef returns proper stucture when no arguments are passed
11 | // Verify referential integrity of ref across call positions
12 | // Verify args passed to useCommonRef are included in the return value
13 |
14 | import React from 'react';
15 | import {renderHook} from '@testing-library/react-hooks';
16 | import {Provider, createStoreMap, createCommonHook, useCommonRef} from '../src';
17 |
18 | const storeMap = createStoreMap();
19 | const wrapper = ({children}) => (
20 | {children}
21 | );
22 |
23 | test('Verify useCommonRef returns proper stucture when no arguments are passed', () => {
24 | const useRefTest = createCommonHook((value) => {
25 | return useCommonRef(value);
26 | });
27 | const {result} = renderHook(useRefTest, {wrapper});
28 | expect(typeof result.current).toBe('object');
29 | expect(result.current.constructor.name).toBe('Object');
30 | expect(Object.keys(result.current).length).toBe(1);
31 | expect('current' in result.current).toBe(true);
32 | expect(result.current.curent).toBe(undefined);
33 | });
34 |
35 | test('Verify referential integrity of ref across call positions', () => {
36 | const useRefTest = createCommonHook((value) => {
37 | return useCommonRef(value);
38 | });
39 | const {result: result1, rerender: rerender1} = renderHook(useRefTest, {
40 | wrapper,
41 | });
42 | const {result: result2, rerender: rerender2} = renderHook(useRefTest, {
43 | wrapper,
44 | });
45 | expect(result1.current).toBe(result2.current);
46 | rerender1();
47 | expect(result1.current).toBe(result2.current);
48 | rerender2();
49 | expect(result1.current).toBe(result2.current);
50 | });
51 |
52 | test('Verify args passed to useCommonRef are included in the return value', () => {
53 | const value = {};
54 | const useRefTest = createCommonHook((value) => {
55 | return useCommonRef(value);
56 | });
57 | const {result, rerender} = renderHook(useRefTest, {
58 | wrapper,
59 | initialProps: value,
60 | });
61 | expect(result.current.current).toBe(value);
62 | rerender();
63 | expect(result.current.current).toBe(value);
64 | });
65 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/__tests__/use-common-state.test.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | // Verify referential integrity of state and setState across call positions and renders
11 | // Verify setState causes one rerender per call position
12 | // Verify no rerenders are triggered when returning current state from a setState callback
13 | // Verify setState passes current state as prop when passed a callback
14 | // Verify initialState passes setState as prop when passed a callback
15 | // Verify initialState initializes with returned value when passed a callback
16 | // Verify initialState sets returned value when the passed callback and then sets state asynchrounously with setState
17 |
18 | import React from 'react';
19 | import {renderHook, act} from '@testing-library/react-hooks';
20 | import {
21 | Provider,
22 | createStoreMap,
23 | createCommonHook,
24 | useCommonState,
25 | } from '../src';
26 |
27 | const storeMap = createStoreMap();
28 | const wrapper = ({children}) => (
29 | {children}
30 | );
31 |
32 | test('Verify referential integrity of state and setState across call positions and renders', () => {
33 | let value = {};
34 | const useTestState = createCommonHook((value) => {
35 | const [state, setState] = useCommonState(value);
36 | return {state, setState};
37 | });
38 | const {result: result1} = renderHook(useTestState, {
39 | wrapper,
40 | initialProps: value,
41 | });
42 | const {result: result2} = renderHook(useTestState, {
43 | wrapper,
44 | initialProps: value,
45 | });
46 | const {setState} = result1.current;
47 | expect(result1.current.state).toBe(value);
48 | expect(result1.current.state).toBe(result2.current.state);
49 | expect(result1.current.setState).toBe(result2.current.setState);
50 | value = {};
51 | act(() => setState(value));
52 | expect(result1.current.state).toBe(value);
53 | expect(result1.current.state).toBe(result2.current.state);
54 | expect(result1.current.setState).toBe(setState);
55 | expect(result1.current.setState).toBe(result2.current.setState);
56 | });
57 |
58 | test('Verify setState causes one rerender per call position', () => {
59 | let count = 0;
60 | let value = 0;
61 | const useTestState = createCommonHook((value) => {
62 | count++;
63 | const [state, setState] = useCommonState(value);
64 | return {state, setState};
65 | });
66 | const {result: result1} = renderHook(useTestState, {
67 | wrapper,
68 | initialProps: value,
69 | });
70 | expect(count).toBe(1);
71 | renderHook(useTestState, {
72 | wrapper,
73 | initialProps: value,
74 | });
75 | const {setState} = result1.current;
76 | expect(count).toBe(2);
77 | value++;
78 | act(() => setState(value));
79 | expect(count).toBe(4);
80 | });
81 |
82 | test('Verify no rerenders are triggered when returning current state from a setState callback', () => {
83 | let count = 0;
84 | const useTestState = createCommonHook((value) => {
85 | count++;
86 | const [state, setState] = useCommonState(value);
87 | return {state, setState};
88 | });
89 | const {result} = renderHook(useTestState, {
90 | wrapper,
91 | initialProps: {},
92 | });
93 | expect(count).toBe(1);
94 | renderHook(useTestState, {
95 | wrapper,
96 | initialProps: {},
97 | });
98 | expect(count).toBe(2);
99 | const {setState} = result.current;
100 | act(() => setState((state) => state));
101 | expect(count).toBe(2);
102 | });
103 |
104 | test('Verify setState passes current state as prop when passed a callback', () => {
105 | const useTestState = createCommonHook((value) => {
106 | const [state, setState] = useCommonState(value);
107 | return {state, setState};
108 | });
109 | const {result} = renderHook(useTestState, {
110 | wrapper,
111 | initialProps: {},
112 | });
113 | renderHook(useTestState, {
114 | wrapper,
115 | initialProps: {},
116 | });
117 | const {state, setState} = result.current;
118 | let stateArg;
119 | act(() =>
120 | setState((state) => {
121 | stateArg = state;
122 | return state;
123 | }),
124 | );
125 | expect(stateArg).toBe(state);
126 | });
127 |
128 | test('Verify initialState passes setState as prop when passed a callback', () => {
129 | let setStateArg;
130 | const useTestState = createCommonHook((value) => {
131 | const [state, setState] = useCommonState(value);
132 | return {state, setState};
133 | });
134 | const {result} = renderHook(useTestState, {
135 | wrapper,
136 | initialProps: (setState) => {
137 | setStateArg = setState;
138 | return 0;
139 | },
140 | });
141 | const {setState} = result.current;
142 | expect(setStateArg).toBe(setState);
143 | });
144 |
145 | test('Verify initialState initializes with returned value when passed a callback', () => {
146 | const value = {};
147 | const useTestState = createCommonHook((value) => {
148 | const [state, setState] = useCommonState(value);
149 | return {state, setState};
150 | });
151 | const {result} = renderHook(useTestState, {
152 | wrapper,
153 | initialProps: () => value,
154 | });
155 | const {state} = result.current;
156 | expect(state).toBe(value);
157 | });
158 |
159 | test('Verify initialState sets returned value when the passed callback and then sets state asynchrounously with setState', async () => {
160 | const value1 = 0;
161 | const value2 = 1;
162 | expect.assertions(2);
163 | const useTestState = createCommonHook((value) => {
164 | const [state, setState] = useCommonState(value);
165 | return {state, setState};
166 | });
167 | const {result} = renderHook(useTestState, {
168 | wrapper,
169 | initialProps: (setState) => {
170 | setTimeout(() => {
171 | act(() => {
172 | setState((state) => state + 1);
173 | });
174 | }, 0);
175 | return value1;
176 | },
177 | });
178 | expect(result.current.state).toBe(value1);
179 | await new Promise((resolve) => setTimeout(resolve, 0));
180 | expect(result.current.state).toBe(value2);
181 | });
182 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/babel.config.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | module.exports = require('../../babel.config.js');
3 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-global-hooks",
3 | "description": "react hooks that can be shared between components and other hooks or provide a common behavior in a custom hook that is shared between components and other hooks",
4 | "version": "0.2.4",
5 | "license": "MIT",
6 | "main": "./dist-node-cjs/index.js",
7 | "module": "./dist-node-esm/index.js",
8 | "browser": {
9 | "./dist-node-cjs/index.js": "./dist-browser-cjs/index.js",
10 | "./dist-node-esm/index.js": "./dist-browser-esm/index.js"
11 | },
12 | "files": [
13 | "dist-node-cjs",
14 | "dist-node-esm",
15 | "dist-browser-cjs",
16 | "dist-browser-esm",
17 | "src",
18 | "!test"
19 | ],
20 | "scripts": {
21 | "build": "cup-build",
22 | "cup-build-tests": "cup build-tests",
23 | "cup-clean": "cup clean",
24 | "prepublish": "cup-build",
25 | "publish": "npm publish"
26 | },
27 | "peerDependencies": {
28 | "react": "^16.13.1"
29 | },
30 | "devDependencies": {
31 | "@babel/cli": "^7.8.4",
32 | "@babel/core": "^7.9.0",
33 | "@babel/plugin-proposal-class-properties": "^7.8.3",
34 | "@babel/plugin-proposal-export-default-from": "^7.8.3",
35 | "@babel/preset-flow": "^7.9.0",
36 | "@babel/preset-react": "^7.9.1",
37 | "@testing-library/jest-dom": "^4.2.4",
38 | "@testing-library/react": "^9.4.0",
39 | "@testing-library/react-hooks": "^3.2.1",
40 | "babel-plugin-version-inline": "^1.0.0",
41 | "create-universal-package": "^4.1.0",
42 | "react": "^16.13.1",
43 | "react-test-renderer": "^16.13.1"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/create-common-callback.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | import {useStoreMap} from './provider';
10 |
11 | export default function createCommonCallback(
12 | name = 'useCommonCallback',
13 | ): Function {
14 | const symbol = Symbol(name);
15 | return function useCommonCallback(fn: T, args: Array = []): T {
16 | const storeMap = useStoreMap();
17 | const firstRun = !(symbol in storeMap);
18 | if (firstRun) {
19 | storeMap[symbol] = {value: undefined, watched: [...args]};
20 | }
21 | const container = storeMap[symbol];
22 | const {watched} = container;
23 | if (
24 | firstRun ||
25 | watched.length !== args.length ||
26 | watched.some((_, i) => !Object.is(watched[i], args[i]))
27 | ) {
28 | watched.splice(0, watched.length, ...args);
29 | container.value = fn;
30 | }
31 | return container.value;
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/create-common-effect.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | import {useEffect} from 'react';
10 | import {useStoreMap} from './provider';
11 |
12 | export default function createCommonEffect(name = 'useCommonEffect'): Function {
13 | const symbol = Symbol(name);
14 | return function useCommonEffect(fn: () => T, args: Array = []): void {
15 | const storeMap = useStoreMap();
16 | const firstRun = !(symbol in storeMap);
17 | if (firstRun) {
18 | storeMap[symbol] = {value: undefined, watched: [...args], mounted: 0};
19 | }
20 | const container = storeMap[symbol];
21 | const {watched} = container;
22 | let firstMount = false;
23 | useEffect(() => {
24 | container.mounted++;
25 | firstMount = container.mounted === 1;
26 | return () => {
27 | container.mounted--;
28 | if (container.mounted === 0 && typeof container.value === 'function') {
29 | container.value();
30 | container.value = null;
31 | }
32 | };
33 | }, []);
34 | useEffect(() => {
35 | if (
36 | firstRun ||
37 | firstMount ||
38 | watched.length !== args.length ||
39 | watched.some((_, i) => !Object.is(watched[i], args[i]))
40 | ) {
41 | watched.splice(0, watched.length, ...args);
42 | if (typeof container.value === 'function') {
43 | // cleanup previous
44 | container.value();
45 | }
46 | container.value = fn();
47 | }
48 | }, [...args, container.mounted]);
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/create-common-hook.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | /* globals __RGH_DEVTOOLS__ */
11 |
12 | import {useStoreMap} from './provider';
13 | import type {Hook, SharedHook, CommonHook} from './types.flow';
14 |
15 | export class Scope {
16 | callPositions: Array = [];
17 | pointer: number = 0;
18 | name: ?string;
19 | constructor(name: ?string) {
20 | this.name = name;
21 | }
22 | }
23 |
24 | function createCommonHook(
25 | fn: Hook,
26 | name: ?string = fn.name || 'useCommonHook',
27 | ): SharedHook {
28 | const symbol = Symbol(name);
29 | return (...args): T => {
30 | const storeMap = useStoreMap();
31 | const firstRun = !(symbol in storeMap);
32 | if (firstRun) {
33 | storeMap[symbol] = new Scope(name);
34 | }
35 | const scope = storeMap[symbol];
36 | const parentScope = storeMap.currentScope;
37 | storeMap.currentScope = scope;
38 | scope.pointer = 0;
39 | if (typeof __RGH_DEVTOOLS__ !== 'undefined') {
40 | __RGH_DEVTOOLS__.scopeStart({
41 | name,
42 | args,
43 | parentScope,
44 | currentScope: scope,
45 | });
46 | }
47 | const result = fn(...args);
48 | if (typeof __RGH_DEVTOOLS__ !== 'undefined') {
49 | __RGH_DEVTOOLS__.scopeEnd({
50 | name,
51 | args,
52 | result,
53 | parentScope,
54 | currentScope: scope,
55 | });
56 | }
57 | storeMap.currentScope = parentScope;
58 | return result;
59 | };
60 | }
61 | export default createCommonHook;
62 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/create-common-layout-effect.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | import {useLayoutEffect} from 'react';
10 | import {useStoreMap} from './provider';
11 |
12 | export default function createCommonLayoutEffect(
13 | name = 'useCommonLayoutEffect',
14 | ): Function {
15 | const symbol = Symbol(name);
16 | return function useCommonLayoutEffect(
17 | fn: () => T,
18 | args: Array = [],
19 | ): void {
20 | const storeMap = useStoreMap();
21 | const firstRun = !(symbol in storeMap);
22 | if (firstRun) {
23 | storeMap[symbol] = {value: undefined, watched: [...args], mounted: 0};
24 | }
25 | const container = storeMap[symbol];
26 | const {watched} = container;
27 | let firstMount = false;
28 | useLayoutEffect(() => {
29 | container.mounted++;
30 | firstMount = container.mounted === 1;
31 | return () => {
32 | container.mounted--;
33 | if (container.mounted === 0 && typeof container.value === 'function') {
34 | container.value();
35 | container.value = null;
36 | }
37 | };
38 | }, []);
39 | useLayoutEffect(() => {
40 | if (
41 | firstRun ||
42 | firstMount ||
43 | watched.length !== args.length ||
44 | watched.some((_, i) => !Object.is(watched[i], args[i]))
45 | ) {
46 | watched.splice(0, watched.length, ...args);
47 | if (typeof container.value === 'function') {
48 | // cleanup previous
49 | container.value();
50 | }
51 | container.value = fn();
52 | }
53 | }, [...args, container.mounted]);
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/create-common-memo.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | import {useStoreMap} from './provider';
10 |
11 | export default function createCommonMemo(name = 'useCommonMemo'): Function {
12 | const symbol = Symbol(name);
13 | return function useCommonMemo(fn: () => T, args: Array = []): T {
14 | const storeMap = useStoreMap();
15 | const firstRun = !(symbol in storeMap);
16 | if (firstRun) {
17 | storeMap[symbol] = {value: undefined, watched: [...args]};
18 | }
19 | const container = storeMap[symbol];
20 | const {watched} = container;
21 | if (
22 | firstRun ||
23 | watched.length !== args.length ||
24 | watched.some((_, i) => !Object.is(watched[i], args[i]))
25 | ) {
26 | watched.splice(0, watched.length, ...args);
27 | container.value = fn();
28 | }
29 | return container.value;
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/create-common-ref.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | import {useStoreMap} from './provider';
10 |
11 | export default function createCommonRef(name = 'useCommonRef'): Function {
12 | const symbol = Symbol(name);
13 | return function useCommonRef(arg: T): {current: T} {
14 | const storeMap = useStoreMap();
15 | const firstRun = !(symbol in storeMap);
16 | if (firstRun) {
17 | storeMap[symbol] = {current: arg};
18 | }
19 | return storeMap[symbol];
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/create-common-state.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | import createSharedState from './create-shared-state';
10 | import {useStoreMap} from './provider';
11 | import type {InitialState} from './types.flow';
12 |
13 | export default function createCommonState(name = 'useCommonState'): Function {
14 | const symbol = Symbol(name);
15 | return function useCommonState(value: InitialState) {
16 | const storeMap = useStoreMap();
17 | const firstRun = !(symbol in storeMap);
18 | if (firstRun) {
19 | storeMap[symbol] = createSharedState(value, name);
20 | }
21 | const [useState, useDispatch] = storeMap[symbol];
22 | return [useState(), useDispatch()];
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/create-shared-reducer.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | /* globals __RGH_DEVTOOLS__ */
11 |
12 | import {useEffect, useState, useCallback, useMemo} from 'react';
13 | import useWatched from './use-watched';
14 | import {useStoreMap} from './provider';
15 | import type {
16 | SelectorFn,
17 | EqualityFn,
18 | ReducerFn,
19 | TimeVariationFn,
20 | Dispatch,
21 | InitialStateRedux,
22 | } from './types.flow';
23 | import Store from './store';
24 |
25 | const defaultSelector = (state) => state;
26 | function createSharedReducer(
27 | reducer: ?ReducerFn,
28 | initialState: InitialStateRedux,
29 | name: ?string = 'useSharedReducer',
30 | ) {
31 | const symbol = Symbol(name);
32 | function useStore() {
33 | const storeMap = useStoreMap();
34 | // $FlowFixMe: flow does not handle symbols as object keys
35 | const firstRun = !(symbol in storeMap);
36 | if (firstRun) {
37 | // $FlowFixMe: flow does not handle symbols as object keys
38 | storeMap[symbol] = storeMap.index++;
39 | }
40 | // $FlowFixMe: flow does not handle symbols as object keys
41 | const storeKey = storeMap[symbol];
42 | if (!(storeMap[storeKey] instanceof Store)) {
43 | const state =
44 | storeKey in storeMap ? storeMap[storeKey] /* hydrate */ : initialState;
45 | storeMap[storeKey] = new Store(reducer, state, name);
46 | }
47 | return storeMap[storeKey];
48 | }
49 | return [
50 | function useSelector(
51 | selector?: ?SelectorFn,
52 | equalityFn?: ?EqualityFn,
53 | timeVariationFn?: TimeVariationFn,
54 | ): SelectedState {
55 | // $FlowFixMe
56 | selector = typeof selector === 'function' ? selector : defaultSelector;
57 | equalityFn = typeof equalityFn === 'function' ? equalityFn : Object.is;
58 | const {state, listeners} = useStore();
59 | const [selectedState, setState] = useState(() => ({
60 | // $FlowFixMe
61 | current: selector(state),
62 | }));
63 | const listener = useCallback(
64 | (state) => {
65 | setState((curr) => {
66 | const {current} = curr;
67 | const next = selector(state);
68 | if (!equalityFn(current, next)) {
69 | if (
70 | typeof __RGH_DEVTOOLS__ !== 'undefined' &&
71 | !Object.is(current, next)
72 | ) {
73 | __RGH_DEVTOOLS__.componentEffectedUpdate({
74 | name,
75 | state: next,
76 | previousState: current,
77 | });
78 | }
79 | return {current: next};
80 | } else {
81 | return curr;
82 | }
83 | });
84 | },
85 | [selector, equalityFn],
86 | );
87 | const listenerTv = useMemo(
88 | () =>
89 | typeof timeVariationFn === 'function'
90 | ? timeVariationFn(listener)
91 | : listener,
92 | [listener, timeVariationFn],
93 | );
94 | // syncronous registration/unregistration when listener changes
95 | useWatched(() => {
96 | listeners.add(listenerTv);
97 | return () => {
98 | listeners.delete(listenerTv);
99 | };
100 | }, [listeners, listenerTv]);
101 | // unregister when component unmounts
102 | useEffect(
103 | () => () => {
104 | listeners.delete(listenerTv);
105 | },
106 | [listeners, listenerTv],
107 | );
108 | return selectedState.current;
109 | },
110 | function useDispatch(): Dispatch {
111 | const {dispatch} = useStore();
112 | return dispatch;
113 | },
114 | ];
115 | }
116 | export default createSharedReducer;
117 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/create-shared-ref.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | import {useStoreMap} from './provider';
10 |
11 | export default function createSharedRef(
12 | arg: T,
13 | name: ?string = 'useSharedRef',
14 | ): Function {
15 | const symbol = Symbol(name);
16 | return function useSharedRef(): {current: T} {
17 | const storeMap = useStoreMap();
18 | const firstRun = !(symbol in storeMap);
19 | if (firstRun) {
20 | storeMap[symbol] = {current: arg};
21 | }
22 | return storeMap[symbol];
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/create-shared-state.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | import type {InitialState} from './types.flow';
11 | import createSharedReducer from './create-shared-reducer';
12 |
13 | export default function createSharedState(
14 | initialState: InitialState,
15 | name: ?string = 'useSharedState',
16 | ) {
17 | const [useSelector, useSetState] = createSharedReducer(
18 | null,
19 | initialState,
20 | name,
21 | );
22 | return [useSelector, useSetState];
23 | }
24 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/hook-factory.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | /* globals __RGH_DEVTOOLS__ */
11 |
12 | import {useStoreMap} from './provider';
13 |
14 | const hookFactory = (createHook) => (...args) => {
15 | const storeMap = useStoreMap();
16 | const {currentScope} = storeMap;
17 | if (!currentScope) {
18 | throw new Error(
19 | `Scope not found for ${createHook.name}. Check that the enclosing hook is wrapped by createCommonHook.`,
20 | );
21 | }
22 | const {callPositions} = currentScope;
23 | const callIndex = currentScope.pointer++;
24 | if (!(callIndex in callPositions)) {
25 | callPositions[callIndex] = createHook();
26 | }
27 | const result = callPositions[callIndex](...args);
28 | if (typeof __RGH_DEVTOOLS__ !== 'undefined') {
29 | __RGH_DEVTOOLS__.commonHookCalled({name, args, result, currentScope});
30 | }
31 | return result;
32 | };
33 | export default hookFactory;
34 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/index.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | import hookFactory from './hook-factory';
11 | import createCommonCallback from './create-common-callback';
12 | import createCommonEffect from './create-common-effect';
13 | import createCommonLayoutEffect from './create-common-layout-effect';
14 | import createCommonMemo from './create-common-memo';
15 | import createCommonRef from './create-common-ref';
16 | import createCommonState from './create-common-state';
17 |
18 | export {default as createSharedState} from './create-shared-state';
19 | export {default as createSharedRef} from './create-shared-ref';
20 | export {default as createSharedReducer} from './create-shared-reducer';
21 | export {default as createCommonHook} from './create-common-hook';
22 |
23 | export const useCommonCallback = hookFactory(createCommonCallback);
24 | export const useCommonEffect = hookFactory(createCommonEffect);
25 | export const useCommonLayoutEffect = hookFactory(createCommonLayoutEffect);
26 | export const useCommonMemo = hookFactory(createCommonMemo);
27 | export const useCommonRef = hookFactory(createCommonRef);
28 | export const useCommonState = hookFactory(createCommonState);
29 | export {hookFactory};
30 | export {default as StoreMap, createStoreMap} from './store-map';
31 | export {default as Provider} from './provider';
32 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/provider.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 |
10 | /* globals __RGH_DEVTOOLS__ */
11 |
12 | import {createContext, useContext} from 'react';
13 | import {createStoreMap} from './store-map';
14 |
15 | const storeMap = createStoreMap();
16 | if (typeof __RGH_DEVTOOLS__ !== 'undefined') {
17 | __RGH_DEVTOOLS__.registerStoreMap(storeMap);
18 | }
19 | const context = createContext(storeMap);
20 | context.displayName = 'ReactGlobalHooks';
21 |
22 | export const useStoreMap = () => useContext(context);
23 |
24 | const {Provider} = context;
25 | export default Provider;
26 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/store-map.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | import {type Scope} from './create-common-hook';
10 |
11 | export default class StoreMap extends Array {
12 | index: number = 0;
13 | currentScope: Scope | null = null;
14 | }
15 |
16 | export const createStoreMap = (): StoreMap => new StoreMap();
17 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/store.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | /* Subscribe pattern inspired by andregardi/use-global-hook */
10 | /* globals __RGH_DEVTOOLS__ */
11 |
12 | import type {
13 | Dispatch,
14 | SetState,
15 | ReducerFn,
16 | InitialState,
17 | LazyState_NotAHook,
18 | Listener,
19 | } from './types.flow';
20 |
21 | class Store {
22 | listeners: Set> = new Set();
23 | reducer: ?ReducerFn;
24 | state: State;
25 | name: ?string;
26 | constructor(
27 | reducer: ?ReducerFn,
28 | initialState: InitialState | LazyState_NotAHook,
29 | name: ?string,
30 | ) {
31 | this.reducer = reducer;
32 | this.state =
33 | typeof initialState === 'function'
34 | ? initialState(this.dispatch)
35 | : initialState;
36 | this.name = name;
37 | }
38 | dispatch: Dispatch | SetState = (action) => {
39 | if (typeof __RGH_DEVTOOLS__ !== 'undefined') {
40 | __RGH_DEVTOOLS__.componentCausedUpdate({
41 | name: this.name,
42 | action,
43 | });
44 | }
45 | this.state =
46 | typeof this.reducer === 'function'
47 | ? this.reducer(this.state, action)
48 | : typeof action === 'function'
49 | ? /* $FlowFixMe: dispatch falls back to setState if a reducer is not provided */
50 | action(this.state)
51 | : action;
52 | this.listeners.forEach((listener) => {
53 | listener(this.state);
54 | });
55 | };
56 | }
57 | export default Store;
58 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/types.flow.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | export type Listener = (State) => void;
10 | export type SetState = (State | ((State) => State)) => void;
11 | export type UseSharedSetState = () => SetState;
12 | export type LazyStateRedux_NotAHook = (
13 | Dispatch | void,
14 | ) => State;
15 | export type LazyState_NotAHook = LazyStateRedux_NotAHook;
16 | export type SelectorFn = (State) => SelectedState;
17 | export type EqualityFn = (
18 | SelectedState,
19 | SelectedState,
20 | ) => boolean;
21 | export type ReducerFn = (State, Action) => State;
22 | export type TimeVariationFn = (Listener) => Listener;
23 | export type Dispatch = (Action) => void;
24 | export type InitialState = State | LazyState_NotAHook;
25 | export type InitialStateRedux =
26 | | State
27 | | LazyStateRedux_NotAHook;
28 | export type Hook = () => T;
29 | export type SharedHook = () => T;
30 | export type CommonHook = Function;
31 |
--------------------------------------------------------------------------------
/modules/react-global-hooks/src/use-watched.js:
--------------------------------------------------------------------------------
1 | /** Copyright (c) Uber Technologies, Inc.
2 | *
3 | * This source code is licensed under the MIT license found in the
4 | * LICENSE file in the root directory of this source tree.
5 | *
6 | */
7 |
8 | // @flow
9 | import {useRef} from 'react';
10 |
11 | const useWatched = (fn: Function, args: Array = []): void => {
12 | const {current} = useRef({args: [], cleanup: null});
13 | if (
14 | args.length !== current.args.length ||
15 | args.some((val, i) => val !== current.args[i])
16 | ) {
17 | if (typeof current.cleanup === 'function') {
18 | current.cleanup(args);
19 | }
20 | current.cleanup = fn(current.args);
21 | current.args = args;
22 | }
23 | };
24 | export default useWatched;
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-global-hooks",
3 | "version": "1.0.0",
4 | "description": "A library allowing to share state across react components ",
5 | "private": true,
6 | "main": "index.js",
7 | "scripts": {
8 | "lint": "eslint modules/",
9 | "flow": "flow check",
10 | "test": "jest",
11 | "test-cover": "jest --coverage",
12 | "build": "lerna run build --npm-client=npm",
13 | "lerna-build": "lerna run build --parallel",
14 | "lerna-build:cjs": "lerna run build:cjs --parallel",
15 | "lerna-build:esm": "lerna run build:esm --parallel",
16 | "lerna-build:watch": "lerna run dev --parallel",
17 | "lerna-publish": "lerna version",
18 | "lerna-updated": "lerna updated",
19 | "publish:all": "lerna run build --npm-client=npm && lerna run publish --npm-client=npm --no-bail",
20 | "link-examples": "yarn build && ./scripts/link-examples.sh"
21 | },
22 | "devDependencies": {
23 | "@babel/cli": "^7.8.4",
24 | "@babel/core": "^7.9.0",
25 | "@babel/plugin-proposal-class-properties": "^7.8.3",
26 | "@babel/plugin-proposal-export-default-from": "^7.8.3",
27 | "@babel/plugin-transform-runtime": "^7.9.0",
28 | "@babel/preset-env": "^7.9.0",
29 | "@babel/preset-flow": "^7.9.0",
30 | "@babel/preset-react": "^7.9.1",
31 | "@testing-library/jest-dom": "^4.2.4",
32 | "@testing-library/react": "^9.4.0",
33 | "@testing-library/react-hooks": "^3.2.1",
34 | "babel-plugin-version-inline": "^1.0.0",
35 | "create-universal-package": "^4.1.0",
36 | "jest": "^26.2.2",
37 | "lerna": "^3.20.2",
38 | "prettier": "^2.1.1",
39 | "react-dom": "^16.12.0"
40 | },
41 | "dependencies": {
42 | "core-js": "^3.6.4"
43 | },
44 | "workspaces": [
45 | "modules/*"
46 | ],
47 | "engines": {
48 | "node": ">=10.0.0",
49 | "npm": ">=6.8.0",
50 | "yarn": ">=1.19.1"
51 | },
52 | "repository": {
53 | "type": "git",
54 | "url": "git+ssh://git@github.com/uber/react-global-hooks.git"
55 | },
56 | "keywords": [
57 | "react",
58 | "hooks"
59 | ],
60 | "license": "MIT",
61 | "bugs": {
62 | "url": "https://github.com/uber/react-global-hooks/issues"
63 | },
64 | "homepage": "https://github.com/uber/react-global-hooks#readme"
65 | }
66 |
--------------------------------------------------------------------------------
/scripts/link-examples.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -x #echo on
3 | for m in modules/*/ ; do
4 | for d in examples/*/ ; do
5 | destination=${d}node_modules/@uber
6 | mkdir -p $destination
7 | rm -rf $destination/${m#modules/}
8 | cp -R ${m} $destination/${m#modules/}
9 | done
10 | done
--------------------------------------------------------------------------------