├── .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 --------------------------------------------------------------------------------