├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── flow-typed └── npm │ └── jest_v29.x.x.js ├── index.d.ts ├── package-lock.json ├── package.json ├── src ├── index.js └── index.test.js └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-flow" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-transform-runtime" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /flow-typed 2 | /js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | module.exports = { 3 | root: true, 4 | parser: '@babel/eslint-parser', 5 | env: { 6 | node: true, 7 | jest: true, 8 | es6: true, 9 | }, 10 | extends: ['eslint:recommended'], 11 | plugins: ['flowtype'], 12 | rules: { 13 | 'flowtype/define-flow-type': 1, 14 | 'flowtype/require-valid-file-annotation': ['error', 'always'], 15 | 16 | indent: ['error', 2], 17 | 'linebreak-style': ['error', 'unix'], 18 | quotes: ['error', 'single', 'avoid-escape'], 19 | semi: ['error', 'always'], 20 | 'no-var': ['error'], 21 | 'brace-style': ['error'], 22 | 'array-bracket-spacing': ['error', 'never'], 23 | 'block-spacing': ['error', 'always'], 24 | 'no-spaced-func': ['error'], 25 | 'no-whitespace-before-property': ['error'], 26 | 'space-before-blocks': ['error', 'always'], 27 | 'keyword-spacing': ['error'], 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /js/ 3 | /node_modules/resolve/test 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 20 14 | cache: 'npm' 15 | - run: npm ci 16 | - run: npm test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | /node_modules 4 | npm-debug.log 5 | /js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | /node_modules 4 | npm-debug.log 5 | /flow-typed 6 | /src 7 | __tests__ 8 | *.test.* 9 | /.github 10 | .babelrc 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.2 (2024-04-26) 2 | 3 | - Uses [React 19's support for ref cleanup functions](https://react.dev/blog/2024/04/25/react-19#cleanup-functions-for-refs) when available. This will ensure compatibility with future React versions that don't call the ref callback with null when unmounting an element. 4 | 5 | ## 1.0.1 (2022-09-11) 6 | 7 | - Fixes a minor issue where `MultiRef.ref()` cached return values would never become uncached if a React render is aborted and then `MultiRef.ref()` was not used with the same key in a subsequent render. 8 | 9 | ## 1.0.0 (2018-11-01) 10 | 11 | Initial release. 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Chris Cowan 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-multi-ref 2 | 3 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Macil/react-multi-ref/blob/master/LICENSE.txt) [![npm version](https://img.shields.io/npm/v/react-multi-ref.svg?style=flat)](https://www.npmjs.com/package/react-multi-ref) 4 | 5 | This is a small utility to make it easy for React components to deal with refs 6 | on multiple dynamically created elements. 7 | 8 | ```js 9 | import { useState } from "react"; 10 | import MultiRef from "react-multi-ref"; 11 | 12 | function Foo(props) { 13 | const [itemRefs] = useState(() => new MultiRef()); 14 | 15 | // Make a 5-item array of divs with keys 0,1,2,3,4 16 | const items = new Array(5).fill(null).map((n, i) => ( 17 |
18 | 19 |
20 | )); 21 | 22 | function onClick() { 23 | const parts = []; 24 | itemRefs.map.forEach((input) => { 25 | parts.push(input.value); 26 | }); 27 | alert("all input values: " + parts.join(", ")); 28 | } 29 | 30 | return ( 31 |
32 | 33 | {items} 34 |
35 | ); 36 | } 37 | ``` 38 | 39 | The `multiRef.map` property is a Map object containing entries where the key is 40 | the parameter passed to `multiRef.ref(key)` and the value is the ref element 41 | given by React. You can retrieve a specific element by key from the map by using 42 | `multiRef.map.get(key)`. 43 | 44 | Subsequent calls to `multiRef.ref(key)` in later renders with the same key 45 | will return the same value so that React knows that it doesn't need to 46 | update the ref. 47 | 48 | ## Class Component Example 49 | 50 | MultiRef is usable as long as you can create an instance of it and persist the 51 | instance for the lifetime of a component. In a function component, you can do this with `useState` (_not_ `useMemo`, because React is allowed to reset its memory at any time), and in a class component, you can do this by keeping the instance as a property on the class. 52 | 53 | ```js 54 | import React from "react"; 55 | import MultiRef from "react-multi-ref"; 56 | 57 | class Foo extends React.Component { 58 | #itemRefs = new MultiRef(); 59 | 60 | render() { 61 | // Make a 5-item array of divs with keys 0,1,2,3,4 62 | const items = new Array(5).fill(null).map((n, i) => ( 63 |
64 | 65 |
66 | )); 67 | return ( 68 |
69 | 70 | {items} 71 |
72 | ); 73 | } 74 | 75 | #onClick = () => { 76 | const parts = []; 77 | this.#itemRefs.map.forEach((input) => { 78 | parts.push(input.value); 79 | }); 80 | alert("all input values: " + parts.join(", ")); 81 | }; 82 | } 83 | ``` 84 | 85 | ## Types 86 | 87 | Both [TypeScript](https://www.typescriptlang.org/) and 88 | [Flow](https://flowtype.org/) type definitions for this module are included! 89 | The type definitions won't require any configuration to use. 90 | -------------------------------------------------------------------------------- /flow-typed/npm/jest_v29.x.x.js: -------------------------------------------------------------------------------- 1 | type JestMockFn, TReturn> = { 2 | (...args: TArguments): TReturn, 3 | /** 4 | * An object for introspecting mock calls 5 | */ 6 | mock: { 7 | /** 8 | * An array that represents all calls that have been made into this mock 9 | * function. Each call is represented by an array of arguments that were 10 | * passed during the call. 11 | */ 12 | calls: Array, 13 | /** 14 | * An array containing the call arguments of the last call that was made 15 | * to this mock function. If the function was not called, it will return 16 | * undefined. 17 | */ 18 | lastCall: TArguments, 19 | /** 20 | * An array that contains all the object instances that have been 21 | * instantiated from this mock function. 22 | */ 23 | instances: Array, 24 | /** 25 | * An array that contains all the object results that have been 26 | * returned by this mock function call 27 | */ 28 | results: Array<{ 29 | isThrow: boolean, 30 | value: TReturn, 31 | ... 32 | }>, 33 | ... 34 | }, 35 | /** 36 | * Resets all information stored in the mockFn.mock.calls and 37 | * mockFn.mock.instances arrays. Often this is useful when you want to clean 38 | * up a mock's usage data between two assertions. 39 | */ 40 | mockClear(): void, 41 | /** 42 | * Resets all information stored in the mock. This is useful when you want to 43 | * completely restore a mock back to its initial state. 44 | */ 45 | mockReset(): void, 46 | /** 47 | * Removes the mock and restores the initial implementation. This is useful 48 | * when you want to mock functions in certain test cases and restore the 49 | * original implementation in others. Beware that mockFn.mockRestore only 50 | * works when mock was created with jest.spyOn. Thus you have to take care of 51 | * restoration yourself when manually assigning jest.fn(). 52 | */ 53 | mockRestore(): void, 54 | /** 55 | * Accepts a function that should be used as the implementation of the mock. 56 | * The mock itself will still record all calls that go into and instances 57 | * that come from itself -- the only difference is that the implementation 58 | * will also be executed when the mock is called. 59 | */ 60 | mockImplementation( 61 | fn: (...args: TArguments) => TReturn 62 | ): JestMockFn, 63 | /** 64 | * Accepts a function that will be used as an implementation of the mock for 65 | * one call to the mocked function. Can be chained so that multiple function 66 | * calls produce different results. 67 | */ 68 | mockImplementationOnce( 69 | fn: (...args: TArguments) => TReturn 70 | ): JestMockFn, 71 | /** 72 | * Accepts a string to use in test result output in place of "jest.fn()" to 73 | * indicate which mock function is being referenced. 74 | */ 75 | mockName(name: string): JestMockFn, 76 | /** 77 | * Just a simple sugar function for returning `this` 78 | */ 79 | mockReturnThis(): void, 80 | /** 81 | * Accepts a value that will be returned whenever the mock function is called. 82 | */ 83 | mockReturnValue(value: TReturn): JestMockFn, 84 | /** 85 | * Sugar for only returning a value once inside your mock 86 | */ 87 | mockReturnValueOnce(value: TReturn): JestMockFn, 88 | /** 89 | * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) 90 | */ 91 | mockResolvedValue(value: TReturn): JestMockFn>, 92 | /** 93 | * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value)) 94 | */ 95 | mockResolvedValueOnce( 96 | value: TReturn 97 | ): JestMockFn>, 98 | /** 99 | * Sugar for jest.fn().mockImplementation(() => Promise.reject(value)) 100 | */ 101 | mockRejectedValue(value: TReturn): JestMockFn>, 102 | /** 103 | * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value)) 104 | */ 105 | mockRejectedValueOnce(value: TReturn): JestMockFn>, 106 | ... 107 | }; 108 | 109 | type JestAsymmetricEqualityType = { 110 | /** 111 | * A custom Jasmine equality tester 112 | */ 113 | asymmetricMatch(value: mixed): boolean, 114 | ... 115 | }; 116 | 117 | type JestCallsType = { 118 | allArgs(): mixed, 119 | all(): mixed, 120 | any(): boolean, 121 | count(): number, 122 | first(): mixed, 123 | mostRecent(): mixed, 124 | reset(): void, 125 | ... 126 | }; 127 | 128 | type JestClockType = { 129 | install(): void, 130 | mockDate(date: Date): void, 131 | tick(milliseconds?: number): void, 132 | uninstall(): void, 133 | ... 134 | }; 135 | 136 | type JestMatcherResult = { 137 | message?: string | (() => string), 138 | pass: boolean, 139 | ... 140 | }; 141 | 142 | type JestMatcher = ( 143 | received: any, 144 | ...actual: Array 145 | ) => JestMatcherResult | Promise; 146 | 147 | type JestPromiseType = { 148 | /** 149 | * Use rejects to unwrap the reason of a rejected promise so any other 150 | * matcher can be chained. If the promise is fulfilled the assertion fails. 151 | */ 152 | rejects: JestExpectType, 153 | /** 154 | * Use resolves to unwrap the value of a fulfilled promise so any other 155 | * matcher can be chained. If the promise is rejected the assertion fails. 156 | */ 157 | resolves: JestExpectType, 158 | ... 159 | }; 160 | 161 | /** 162 | * Jest allows functions and classes to be used as test names in test() and 163 | * describe() 164 | */ 165 | type JestTestName = string | Function; 166 | 167 | type FakeableAPI = 168 | | 'Date' 169 | | 'hrtime' 170 | | 'nextTick' 171 | | 'performance' 172 | | 'queueMicrotask' 173 | | 'requestAnimationFrame' 174 | | 'cancelAnimationFrame' 175 | | 'requestIdleCallback' 176 | | 'cancelIdleCallback' 177 | | 'setImmediate' 178 | | 'clearImmediate' 179 | | 'setInterval' 180 | | 'clearInterval' 181 | | 'setTimeout' 182 | | 'clearTimeout'; 183 | 184 | type FakeTimersConfig = { 185 | advanceTimers?: boolean | number, 186 | doNotFake?: Array, 187 | now?: number | Date, 188 | timerLimit?: number, 189 | legacyFakeTimers?: boolean, 190 | ... 191 | }; 192 | 193 | /** 194 | * Plugin: jest-styled-components 195 | */ 196 | 197 | type JestStyledComponentsMatcherValue = 198 | | string 199 | | JestAsymmetricEqualityType 200 | | RegExp 201 | | typeof undefined; 202 | 203 | type JestStyledComponentsMatcherOptions = { 204 | media?: string, 205 | modifier?: string, 206 | supports?: string, 207 | ... 208 | }; 209 | 210 | type JestStyledComponentsMatchersType = { 211 | toHaveStyleRule( 212 | property: string, 213 | value: JestStyledComponentsMatcherValue, 214 | options?: JestStyledComponentsMatcherOptions 215 | ): void, 216 | ... 217 | }; 218 | 219 | /** 220 | * Plugin: jest-enzyme 221 | */ 222 | type EnzymeMatchersType = { 223 | // 5.x 224 | toBeEmpty(): void, 225 | toBePresent(): void, 226 | // 6.x 227 | toBeChecked(): void, 228 | toBeDisabled(): void, 229 | toBeEmptyRender(): void, 230 | toContainMatchingElement(selector: string): void, 231 | toContainMatchingElements(n: number, selector: string): void, 232 | toContainExactlyOneMatchingElement(selector: string): void, 233 | toContainReact(element: React$Element): void, 234 | toExist(): void, 235 | toHaveClassName(className: string): void, 236 | toHaveHTML(html: string): void, 237 | toHaveProp: ((propKey: string, propValue?: any) => void) & 238 | ((props: { ... }) => void), 239 | toHaveRef(refName: string): void, 240 | toHaveState: ((stateKey: string, stateValue?: any) => void) & 241 | ((state: { ... }) => void), 242 | toHaveStyle: ((styleKey: string, styleValue?: any) => void) & 243 | ((style: { ... }) => void), 244 | toHaveTagName(tagName: string): void, 245 | toHaveText(text: string): void, 246 | toHaveValue(value: any): void, 247 | toIncludeText(text: string): void, 248 | toMatchElement( 249 | element: React$Element, 250 | options?: {| ignoreProps?: boolean, verbose?: boolean |} 251 | ): void, 252 | toMatchSelector(selector: string): void, 253 | // 7.x 254 | toHaveDisplayName(name: string): void, 255 | ... 256 | }; 257 | 258 | // DOM testing library extensions (jest-dom) 259 | // https://github.com/testing-library/jest-dom 260 | type DomTestingLibraryType = { 261 | /** 262 | * @deprecated 263 | */ 264 | toBeInTheDOM(container?: HTMLElement): void, 265 | 266 | // 4.x 267 | toBeInTheDocument(): void, 268 | toBeVisible(): void, 269 | toBeEmpty(): void, 270 | toBeDisabled(): void, 271 | toBeEnabled(): void, 272 | toBeInvalid(): void, 273 | toBeRequired(): void, 274 | toBeValid(): void, 275 | toContainElement(element: HTMLElement | null): void, 276 | toContainHTML(htmlText: string): void, 277 | toHaveAttribute(attr: string, value?: any): void, 278 | toHaveClass(...classNames: string[]): void, 279 | toHaveFocus(): void, 280 | toHaveFormValues(expectedValues: { [name: string]: any, ... }): void, 281 | toHaveStyle(css: string | { [name: string]: any, ... }): void, 282 | toHaveTextContent( 283 | text: string | RegExp, 284 | options?: {| normalizeWhitespace: boolean |} 285 | ): void, 286 | toHaveValue(value?: string | string[] | number): void, 287 | 288 | // 5.x 289 | toHaveDisplayValue(value: string | string[]): void, 290 | toBeChecked(): void, 291 | toBeEmptyDOMElement(): void, 292 | toBePartiallyChecked(): void, 293 | toHaveDescription(text: string | RegExp): void, 294 | ... 295 | }; 296 | 297 | // Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers 298 | type JestJQueryMatchersType = { 299 | toExist(): void, 300 | toHaveLength(len: number): void, 301 | toHaveId(id: string): void, 302 | toHaveClass(className: string): void, 303 | toHaveTag(tag: string): void, 304 | toHaveAttr(key: string, val?: any): void, 305 | toHaveProp(key: string, val?: any): void, 306 | toHaveText(text: string | RegExp): void, 307 | toHaveData(key: string, val?: any): void, 308 | toHaveValue(val: any): void, 309 | toHaveCss(css: { [key: string]: any, ... }): void, 310 | toBeChecked(): void, 311 | toBeDisabled(): void, 312 | toBeEmpty(): void, 313 | toBeHidden(): void, 314 | toBeSelected(): void, 315 | toBeVisible(): void, 316 | toBeFocused(): void, 317 | toBeInDom(): void, 318 | toBeMatchedBy(sel: string): void, 319 | toHaveDescendant(sel: string): void, 320 | toHaveDescendantWithText(sel: string, text: string | RegExp): void, 321 | ... 322 | }; 323 | 324 | // Jest Extended Matchers: https://github.com/jest-community/jest-extended 325 | type JestExtendedMatchersType = { 326 | /** 327 | * Note: Currently unimplemented 328 | * Passing assertion 329 | * 330 | * @param {String} message 331 | */ 332 | // pass(message: string): void; 333 | 334 | /** 335 | * Note: Currently unimplemented 336 | * Failing assertion 337 | * 338 | * @param {String} message 339 | */ 340 | // fail(message: string): void; 341 | 342 | /** 343 | * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty. 344 | */ 345 | toBeEmpty(): void, 346 | /** 347 | * Use .toBeOneOf when checking if a value is a member of a given Array. 348 | * @param {Array.<*>} members 349 | */ 350 | toBeOneOf(members: any[]): void, 351 | /** 352 | * Use `.toBeNil` when checking a value is `null` or `undefined`. 353 | */ 354 | toBeNil(): void, 355 | /** 356 | * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`. 357 | * @param {Function} predicate 358 | */ 359 | toSatisfy(predicate: (n: any) => boolean): void, 360 | /** 361 | * Use `.toBeArray` when checking if a value is an `Array`. 362 | */ 363 | toBeArray(): void, 364 | /** 365 | * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x. 366 | * @param {Number} x 367 | */ 368 | toBeArrayOfSize(x: number): void, 369 | /** 370 | * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set. 371 | * @param {Array.<*>} members 372 | */ 373 | toIncludeAllMembers(members: any[]): void, 374 | /** 375 | * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set. 376 | * @param {Array.<*>} members 377 | */ 378 | toIncludeAnyMembers(members: any[]): void, 379 | /** 380 | * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array. 381 | * @param {Function} predicate 382 | */ 383 | toSatisfyAll(predicate: (n: any) => boolean): void, 384 | /** 385 | * Use `.toBeBoolean` when checking if a value is a `Boolean`. 386 | */ 387 | toBeBoolean(): void, 388 | /** 389 | * Use `.toBeTrue` when checking a value is equal (===) to `true`. 390 | */ 391 | toBeTrue(): void, 392 | /** 393 | * Use `.toBeFalse` when checking a value is equal (===) to `false`. 394 | */ 395 | toBeFalse(): void, 396 | /** 397 | * Use .toBeDate when checking if a value is a Date. 398 | */ 399 | toBeDate(): void, 400 | /** 401 | * Use `.toBeFunction` when checking if a value is a `Function`. 402 | */ 403 | toBeFunction(): void, 404 | /** 405 | * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`. 406 | * 407 | * Note: Required Jest version >22 408 | * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same 409 | * 410 | * @param {Mock} mock 411 | */ 412 | toHaveBeenCalledBefore(mock: JestMockFn): void, 413 | /** 414 | * Use `.toBeNumber` when checking if a value is a `Number`. 415 | */ 416 | toBeNumber(): void, 417 | /** 418 | * Use `.toBeNaN` when checking a value is `NaN`. 419 | */ 420 | toBeNaN(): void, 421 | /** 422 | * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`. 423 | */ 424 | toBeFinite(): void, 425 | /** 426 | * Use `.toBePositive` when checking if a value is a positive `Number`. 427 | */ 428 | toBePositive(): void, 429 | /** 430 | * Use `.toBeNegative` when checking if a value is a negative `Number`. 431 | */ 432 | toBeNegative(): void, 433 | /** 434 | * Use `.toBeEven` when checking if a value is an even `Number`. 435 | */ 436 | toBeEven(): void, 437 | /** 438 | * Use `.toBeOdd` when checking if a value is an odd `Number`. 439 | */ 440 | toBeOdd(): void, 441 | /** 442 | * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive). 443 | * 444 | * @param {Number} start 445 | * @param {Number} end 446 | */ 447 | toBeWithin(start: number, end: number): void, 448 | /** 449 | * Use `.toBeObject` when checking if a value is an `Object`. 450 | */ 451 | toBeObject(): void, 452 | /** 453 | * Use `.toContainKey` when checking if an object contains the provided key. 454 | * 455 | * @param {String} key 456 | */ 457 | toContainKey(key: string): void, 458 | /** 459 | * Use `.toContainKeys` when checking if an object has all of the provided keys. 460 | * 461 | * @param {Array.} keys 462 | */ 463 | toContainKeys(keys: string[]): void, 464 | /** 465 | * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. 466 | * 467 | * @param {Array.} keys 468 | */ 469 | toContainAllKeys(keys: string[]): void, 470 | /** 471 | * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys. 472 | * 473 | * @param {Array.} keys 474 | */ 475 | toContainAnyKeys(keys: string[]): void, 476 | /** 477 | * Use `.toContainValue` when checking if an object contains the provided value. 478 | * 479 | * @param {*} value 480 | */ 481 | toContainValue(value: any): void, 482 | /** 483 | * Use `.toContainValues` when checking if an object contains all of the provided values. 484 | * 485 | * @param {Array.<*>} values 486 | */ 487 | toContainValues(values: any[]): void, 488 | /** 489 | * Use `.toContainAllValues` when checking if an object only contains all of the provided values. 490 | * 491 | * @param {Array.<*>} values 492 | */ 493 | toContainAllValues(values: any[]): void, 494 | /** 495 | * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values. 496 | * 497 | * @param {Array.<*>} values 498 | */ 499 | toContainAnyValues(values: any[]): void, 500 | /** 501 | * Use `.toContainEntry` when checking if an object contains the provided entry. 502 | * 503 | * @param {Array.} entry 504 | */ 505 | toContainEntry(entry: [string, string]): void, 506 | /** 507 | * Use `.toContainEntries` when checking if an object contains all of the provided entries. 508 | * 509 | * @param {Array.>} entries 510 | */ 511 | toContainEntries(entries: [string, string][]): void, 512 | /** 513 | * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries. 514 | * 515 | * @param {Array.>} entries 516 | */ 517 | toContainAllEntries(entries: [string, string][]): void, 518 | /** 519 | * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries. 520 | * 521 | * @param {Array.>} entries 522 | */ 523 | toContainAnyEntries(entries: [string, string][]): void, 524 | /** 525 | * Use `.toBeExtensible` when checking if an object is extensible. 526 | */ 527 | toBeExtensible(): void, 528 | /** 529 | * Use `.toBeFrozen` when checking if an object is frozen. 530 | */ 531 | toBeFrozen(): void, 532 | /** 533 | * Use `.toBeSealed` when checking if an object is sealed. 534 | */ 535 | toBeSealed(): void, 536 | /** 537 | * Use `.toBeString` when checking if a value is a `String`. 538 | */ 539 | toBeString(): void, 540 | /** 541 | * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings. 542 | * 543 | * @param {String} string 544 | */ 545 | toEqualCaseInsensitive(string: string): void, 546 | /** 547 | * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix. 548 | * 549 | * @param {String} prefix 550 | */ 551 | toStartWith(prefix: string): void, 552 | /** 553 | * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix. 554 | * 555 | * @param {String} suffix 556 | */ 557 | toEndWith(suffix: string): void, 558 | /** 559 | * Use `.toInclude` when checking if a `String` includes the given `String` substring. 560 | * 561 | * @param {String} substring 562 | */ 563 | toInclude(substring: string): void, 564 | /** 565 | * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times. 566 | * 567 | * @param {String} substring 568 | * @param {Number} times 569 | */ 570 | toIncludeRepeated(substring: string, times: number): void, 571 | /** 572 | * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings. 573 | * 574 | * @param {Array.} substring 575 | */ 576 | toIncludeMultiple(substring: string[]): void, 577 | ... 578 | }; 579 | 580 | // Diffing snapshot utility for Jest (snapshot-diff) 581 | // https://github.com/jest-community/snapshot-diff 582 | type SnapshotDiffType = { 583 | /** 584 | * Compare the difference between the actual in the `expect()` 585 | * vs the object inside `valueB` with some extra options. 586 | */ 587 | toMatchDiffSnapshot( 588 | valueB: any, 589 | options?: {| 590 | expand?: boolean, 591 | colors?: boolean, 592 | contextLines?: number, 593 | stablePatchmarks?: boolean, 594 | aAnnotation?: string, 595 | bAnnotation?: string, 596 | |}, 597 | testName?: string 598 | ): void, 599 | ... 600 | }; 601 | 602 | interface JestExpectType { 603 | not: JestExpectType & 604 | EnzymeMatchersType & 605 | DomTestingLibraryType & 606 | JestJQueryMatchersType & 607 | JestStyledComponentsMatchersType & 608 | JestExtendedMatchersType & 609 | SnapshotDiffType; 610 | /** 611 | * If you have a mock function, you can use .lastCalledWith to test what 612 | * arguments it was last called with. 613 | */ 614 | lastCalledWith(...args: Array): void; 615 | /** 616 | * toBe just checks that a value is what you expect. It uses === to check 617 | * strict equality. 618 | */ 619 | toBe(value: any): void; 620 | /** 621 | * Use .toBeCalledWith to ensure that a mock function was called with 622 | * specific arguments. 623 | */ 624 | toBeCalledWith(...args: Array): void; 625 | /** 626 | * Using exact equality with floating point numbers is a bad idea. Rounding 627 | * means that intuitive things fail. 628 | */ 629 | toBeCloseTo(num: number, delta: any): void; 630 | /** 631 | * Use .toBeDefined to check that a variable is not undefined. 632 | */ 633 | toBeDefined(): void; 634 | /** 635 | * Use .toBeFalsy when you don't care what a value is, you just want to 636 | * ensure a value is false in a boolean context. 637 | */ 638 | toBeFalsy(): void; 639 | /** 640 | * To compare floating point numbers, you can use toBeGreaterThan. 641 | */ 642 | toBeGreaterThan(number: number): void; 643 | /** 644 | * To compare floating point numbers, you can use toBeGreaterThanOrEqual. 645 | */ 646 | toBeGreaterThanOrEqual(number: number): void; 647 | /** 648 | * To compare floating point numbers, you can use toBeLessThan. 649 | */ 650 | toBeLessThan(number: number): void; 651 | /** 652 | * To compare floating point numbers, you can use toBeLessThanOrEqual. 653 | */ 654 | toBeLessThanOrEqual(number: number): void; 655 | /** 656 | * Use .toBeInstanceOf(Class) to check that an object is an instance of a 657 | * class. 658 | */ 659 | toBeInstanceOf(cls: Class): void; 660 | /** 661 | * .toBeNull() is the same as .toBe(null) but the error messages are a bit 662 | * nicer. 663 | */ 664 | toBeNull(): void; 665 | /** 666 | * Use .toBeTruthy when you don't care what a value is, you just want to 667 | * ensure a value is true in a boolean context. 668 | */ 669 | toBeTruthy(): void; 670 | /** 671 | * Use .toBeUndefined to check that a variable is undefined. 672 | */ 673 | toBeUndefined(): void; 674 | /** 675 | * Use .toContain when you want to check that an item is in a list. For 676 | * testing the items in the list, this uses ===, a strict equality check. 677 | */ 678 | toContain(item: any): void; 679 | /** 680 | * Use .toContainEqual when you want to check that an item is in a list. For 681 | * testing the items in the list, this matcher recursively checks the 682 | * equality of all fields, rather than checking for object identity. 683 | */ 684 | toContainEqual(item: any): void; 685 | /** 686 | * Use .toEqual when you want to check that two objects have the same value. 687 | * This matcher recursively checks the equality of all fields, rather than 688 | * checking for object identity. 689 | */ 690 | toEqual(value: any): void; 691 | /** 692 | * Use .toHaveBeenCalled to ensure that a mock function got called. 693 | */ 694 | toHaveBeenCalled(): void; 695 | toBeCalled(): void; 696 | /** 697 | * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact 698 | * number of times. 699 | */ 700 | toHaveBeenCalledTimes(number: number): void; 701 | toBeCalledTimes(number: number): void; 702 | /** 703 | * 704 | */ 705 | toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void; 706 | nthCalledWith(nthCall: number, ...args: Array): void; 707 | /** 708 | * 709 | */ 710 | toHaveReturned(): void; 711 | toReturn(): void; 712 | /** 713 | * 714 | */ 715 | toHaveReturnedTimes(number: number): void; 716 | toReturnTimes(number: number): void; 717 | /** 718 | * 719 | */ 720 | toHaveReturnedWith(value: any): void; 721 | toReturnWith(value: any): void; 722 | /** 723 | * 724 | */ 725 | toHaveLastReturnedWith(value: any): void; 726 | lastReturnedWith(value: any): void; 727 | /** 728 | * 729 | */ 730 | toHaveNthReturnedWith(nthCall: number, value: any): void; 731 | nthReturnedWith(nthCall: number, value: any): void; 732 | /** 733 | * Use .toHaveBeenCalledWith to ensure that a mock function was called with 734 | * specific arguments. 735 | */ 736 | toHaveBeenCalledWith(...args: Array): void; 737 | toBeCalledWith(...args: Array): void; 738 | /** 739 | * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called 740 | * with specific arguments. 741 | */ 742 | toHaveBeenLastCalledWith(...args: Array): void; 743 | lastCalledWith(...args: Array): void; 744 | /** 745 | * Check that an object has a .length property and it is set to a certain 746 | * numeric value. 747 | */ 748 | toHaveLength(number: number): void; 749 | /** 750 | * 751 | */ 752 | toHaveProperty(propPath: string | $ReadOnlyArray, value?: any): void; 753 | /** 754 | * Use .toMatch to check that a string matches a regular expression or string. 755 | */ 756 | toMatch(regexpOrString: RegExp | string): void; 757 | /** 758 | * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. 759 | */ 760 | toMatchObject(object: Object | Array): void; 761 | /** 762 | * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object. 763 | */ 764 | toStrictEqual(value: any): void; 765 | /** 766 | * This ensures that an Object matches the most recent snapshot. 767 | */ 768 | toMatchSnapshot(propertyMatchers?: any, name?: string): void; 769 | /** 770 | * This ensures that an Object matches the most recent snapshot. 771 | */ 772 | toMatchSnapshot(name: string): void; 773 | 774 | toMatchInlineSnapshot(snapshot?: string): void; 775 | toMatchInlineSnapshot(propertyMatchers?: any, snapshot?: string): void; 776 | /** 777 | * Use .toThrow to test that a function throws when it is called. 778 | * If you want to test that a specific error gets thrown, you can provide an 779 | * argument to toThrow. The argument can be a string for the error message, 780 | * a class for the error, or a regex that should match the error. 781 | * 782 | * Alias: .toThrowError 783 | */ 784 | toThrow(message?: string | Error | Class | RegExp): void; 785 | toThrowError(message?: string | Error | Class | RegExp): void; 786 | /** 787 | * Use .toThrowErrorMatchingSnapshot to test that a function throws a error 788 | * matching the most recent snapshot when it is called. 789 | */ 790 | toThrowErrorMatchingSnapshot(): void; 791 | toThrowErrorMatchingInlineSnapshot(snapshot?: string): void; 792 | } 793 | 794 | type JestObjectType = { 795 | /** 796 | * Disables automatic mocking in the module loader. 797 | * 798 | * After this method is called, all `require()`s will return the real 799 | * versions of each module (rather than a mocked version). 800 | */ 801 | disableAutomock(): JestObjectType, 802 | /** 803 | * An un-hoisted version of disableAutomock 804 | */ 805 | autoMockOff(): JestObjectType, 806 | /** 807 | * Enables automatic mocking in the module loader. 808 | */ 809 | enableAutomock(): JestObjectType, 810 | /** 811 | * An un-hoisted version of enableAutomock 812 | */ 813 | autoMockOn(): JestObjectType, 814 | /** 815 | * Clears the mock.calls and mock.instances properties of all mocks. 816 | * Equivalent to calling .mockClear() on every mocked function. 817 | */ 818 | clearAllMocks(): JestObjectType, 819 | /** 820 | * Resets the state of all mocks. Equivalent to calling .mockReset() on every 821 | * mocked function. 822 | */ 823 | resetAllMocks(): JestObjectType, 824 | /** 825 | * Restores all mocks back to their original value. 826 | */ 827 | restoreAllMocks(): JestObjectType, 828 | /** 829 | * Removes any pending timers from the timer system. 830 | */ 831 | clearAllTimers(): void, 832 | /** 833 | * Returns the number of fake timers still left to run. 834 | */ 835 | getTimerCount(): number, 836 | /** 837 | * Set the current system time used by fake timers. 838 | * Simulates a user changing the system clock while your program is running. 839 | * It affects the current time but it does not in itself cause 840 | * e.g. timers to fire; they will fire exactly as they would have done 841 | * without the call to jest.setSystemTime(). 842 | */ 843 | setSystemTime(now?: number | Date): void, 844 | /** 845 | * The same as `mock` but not moved to the top of the expectation by 846 | * babel-jest. 847 | */ 848 | doMock(moduleName: string, moduleFactory?: any): JestObjectType, 849 | /** 850 | * The same as `unmock` but not moved to the top of the expectation by 851 | * babel-jest. 852 | */ 853 | dontMock(moduleName: string): JestObjectType, 854 | /** 855 | * Returns a new, unused mock function. Optionally takes a mock 856 | * implementation. 857 | */ 858 | fn, TReturn>( 859 | implementation?: (...args: TArguments) => TReturn 860 | ): JestMockFn, 861 | /** 862 | * Determines if the given function is a mocked function. 863 | */ 864 | isMockFunction(fn: Function): boolean, 865 | /** 866 | * Alias of `createMockFromModule`. 867 | */ 868 | genMockFromModule(moduleName: string): any, 869 | /** 870 | * Given the name of a module, use the automatic mocking system to generate a 871 | * mocked version of the module for you. 872 | */ 873 | createMockFromModule(moduleName: string): any, 874 | /** 875 | * Mocks a module with an auto-mocked version when it is being required. 876 | * 877 | * The second argument can be used to specify an explicit module factory that 878 | * is being run instead of using Jest's automocking feature. 879 | * 880 | * The third argument can be used to create virtual mocks -- mocks of modules 881 | * that don't exist anywhere in the system. 882 | */ 883 | mock( 884 | moduleName: string, 885 | moduleFactory?: any, 886 | options?: Object 887 | ): JestObjectType, 888 | /** 889 | * Returns the actual module instead of a mock, bypassing all checks on 890 | * whether the module should receive a mock implementation or not. 891 | */ 892 | requireActual(m: $Flow$ModuleRef | string): T, 893 | /** 894 | * Returns a mock module instead of the actual module, bypassing all checks 895 | * on whether the module should be required normally or not. 896 | */ 897 | requireMock(moduleName: string): any, 898 | /** 899 | * Resets the module registry - the cache of all required modules. This is 900 | * useful to isolate modules where local state might conflict between tests. 901 | */ 902 | resetModules(): JestObjectType, 903 | /** 904 | * Creates a sandbox registry for the modules that are loaded inside the 905 | * callback function. This is useful to isolate specific modules for every 906 | * test so that local module state doesn't conflict between tests. 907 | */ 908 | isolateModules(fn: () => void): JestObjectType, 909 | /** 910 | * Exhausts the micro-task queue (usually interfaced in node via 911 | * process.nextTick). 912 | */ 913 | runAllTicks(): void, 914 | /** 915 | * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), 916 | * setInterval(), and setImmediate()). 917 | */ 918 | runAllTimers(): void, 919 | /** 920 | * Exhausts all tasks queued by setImmediate(). 921 | */ 922 | runAllImmediates(): void, 923 | /** 924 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout() 925 | * or setInterval() and setImmediate()). 926 | */ 927 | advanceTimersByTime(msToRun: number): void, 928 | /** 929 | * Executes only the macro-tasks that are currently pending (i.e., only the 930 | * tasks that have been queued by setTimeout() or setInterval() up to this 931 | * point) 932 | */ 933 | runOnlyPendingTimers(): void, 934 | /** 935 | * Explicitly supplies the mock object that the module system should return 936 | * for the specified module. Note: It is recommended to use jest.mock() 937 | * instead. 938 | */ 939 | setMock(moduleName: string, moduleExports: any): JestObjectType, 940 | /** 941 | * Indicates that the module system should never return a mocked version of 942 | * the specified module from require() (e.g. that it should always return the 943 | * real module). 944 | */ 945 | unmock(moduleName: string): JestObjectType, 946 | /** 947 | * Instructs Jest to use fake versions of the standard timer functions 948 | * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, 949 | * setImmediate and clearImmediate). 950 | */ 951 | useFakeTimers(fakeTimersConfig?: FakeTimersConfig): JestObjectType, 952 | /** 953 | * Instructs Jest to use the real versions of the standard timer functions. 954 | */ 955 | useRealTimers(): JestObjectType, 956 | /** 957 | * Creates a mock function similar to jest.fn but also tracks calls to 958 | * object[methodName]. 959 | */ 960 | spyOn( 961 | object: Object, 962 | methodName: string, 963 | accessType?: 'get' | 'set' 964 | ): JestMockFn, 965 | /** 966 | * Set the default timeout interval for tests and before/after hooks in milliseconds. 967 | * Note: The default timeout interval is 5 seconds if this method is not called. 968 | */ 969 | setTimeout(timeout: number): JestObjectType, 970 | ... 971 | }; 972 | 973 | type JestSpyType = { calls: JestCallsType, ... }; 974 | 975 | type JestDoneFn = {| 976 | (error?: Error): void, 977 | fail: (error: Error) => void, 978 | |}; 979 | 980 | /** Runs this function after every test inside this context */ 981 | declare function afterEach( 982 | fn: (done: JestDoneFn) => ?Promise, 983 | timeout?: number 984 | ): void; 985 | /** Runs this function before every test inside this context */ 986 | declare function beforeEach( 987 | fn: (done: JestDoneFn) => ?Promise, 988 | timeout?: number 989 | ): void; 990 | /** Runs this function after all tests have finished inside this context */ 991 | declare function afterAll( 992 | fn: (done: JestDoneFn) => ?Promise, 993 | timeout?: number 994 | ): void; 995 | /** Runs this function before any tests have started inside this context */ 996 | declare function beforeAll( 997 | fn: (done: JestDoneFn) => ?Promise, 998 | timeout?: number 999 | ): void; 1000 | 1001 | /** A context for grouping tests together */ 1002 | declare var describe: { 1003 | /** 1004 | * Creates a block that groups together several related tests in one "test suite" 1005 | */ 1006 | (name: JestTestName, fn: () => void): void, 1007 | /** 1008 | * Only run this describe block 1009 | */ 1010 | only(name: JestTestName, fn: () => void): void, 1011 | /** 1012 | * Skip running this describe block 1013 | */ 1014 | skip(name: JestTestName, fn: () => void): void, 1015 | /** 1016 | * each runs this test against array of argument arrays per each run 1017 | * 1018 | * @param {table} table of Test 1019 | */ 1020 | each( 1021 | ...table: Array | mixed> | [Array, string] 1022 | ): ( 1023 | name: JestTestName, 1024 | fn?: (...args: Array) => ?Promise, 1025 | timeout?: number 1026 | ) => void, 1027 | ... 1028 | }; 1029 | 1030 | /** An individual test unit */ 1031 | declare var it: { 1032 | /** 1033 | * An individual test unit 1034 | * 1035 | * @param {JestTestName} Name of Test 1036 | * @param {Function} Test 1037 | * @param {number} Timeout for the test, in milliseconds. 1038 | */ 1039 | ( 1040 | name: JestTestName, 1041 | fn?: (done: JestDoneFn) => ?Promise, 1042 | timeout?: number 1043 | ): void, 1044 | /** 1045 | * Only run this test 1046 | * 1047 | * @param {JestTestName} Name of Test 1048 | * @param {Function} Test 1049 | * @param {number} Timeout for the test, in milliseconds. 1050 | */ 1051 | only: {| 1052 | ( 1053 | name: JestTestName, 1054 | fn?: (done: JestDoneFn) => ?Promise, 1055 | timeout?: number 1056 | ): void, 1057 | each( 1058 | ...table: Array | mixed> | [Array, string] 1059 | ): ( 1060 | name: JestTestName, 1061 | fn?: (...args: Array) => ?Promise, 1062 | timeout?: number 1063 | ) => void, 1064 | |}, 1065 | /** 1066 | * Skip running this test 1067 | * 1068 | * @param {JestTestName} Name of Test 1069 | * @param {Function} Test 1070 | * @param {number} Timeout for the test, in milliseconds. 1071 | */ 1072 | skip: {| 1073 | ( 1074 | name: JestTestName, 1075 | fn?: (done: JestDoneFn) => ?Promise, 1076 | timeout?: number 1077 | ): void, 1078 | each( 1079 | ...table: Array | mixed> | [Array, string] 1080 | ): ( 1081 | name: JestTestName, 1082 | fn?: (...args: Array) => ?Promise, 1083 | timeout?: number 1084 | ) => void, 1085 | |}, 1086 | /** 1087 | * Highlight planned tests in the summary output 1088 | * 1089 | * @param {String} Name of Test to do 1090 | */ 1091 | todo(name: string): void, 1092 | /** 1093 | * Run the test concurrently 1094 | * 1095 | * @param {JestTestName} Name of Test 1096 | * @param {Function} Test 1097 | * @param {number} Timeout for the test, in milliseconds. 1098 | */ 1099 | concurrent( 1100 | name: JestTestName, 1101 | fn?: (done: JestDoneFn) => ?Promise, 1102 | timeout?: number 1103 | ): void, 1104 | /** 1105 | * each runs this test against array of argument arrays per each run 1106 | * 1107 | * @param {table} table of Test 1108 | */ 1109 | each( 1110 | ...table: Array | mixed> | [Array, string] 1111 | ): ( 1112 | name: JestTestName, 1113 | fn?: (...args: Array) => ?Promise, 1114 | timeout?: number 1115 | ) => void, 1116 | ... 1117 | }; 1118 | 1119 | declare function fit( 1120 | name: JestTestName, 1121 | fn: (done: JestDoneFn) => ?Promise, 1122 | timeout?: number 1123 | ): void; 1124 | /** An individual test unit */ 1125 | declare var test: typeof it; 1126 | /** A disabled group of tests */ 1127 | declare var xdescribe: typeof describe; 1128 | /** A focused group of tests */ 1129 | declare var fdescribe: typeof describe; 1130 | /** A disabled individual test */ 1131 | declare var xit: typeof it; 1132 | /** A disabled individual test */ 1133 | declare var xtest: typeof it; 1134 | 1135 | type JestPrettyFormatColors = { 1136 | comment: { 1137 | close: string, 1138 | open: string, 1139 | ... 1140 | }, 1141 | content: { 1142 | close: string, 1143 | open: string, 1144 | ... 1145 | }, 1146 | prop: { 1147 | close: string, 1148 | open: string, 1149 | ... 1150 | }, 1151 | tag: { 1152 | close: string, 1153 | open: string, 1154 | ... 1155 | }, 1156 | value: { 1157 | close: string, 1158 | open: string, 1159 | ... 1160 | }, 1161 | ... 1162 | }; 1163 | 1164 | type JestPrettyFormatIndent = (string) => string; 1165 | type JestPrettyFormatRefs = Array; 1166 | type JestPrettyFormatPrint = (any) => string; 1167 | type JestPrettyFormatStringOrNull = string | null; 1168 | 1169 | type JestPrettyFormatOptions = {| 1170 | callToJSON: boolean, 1171 | edgeSpacing: string, 1172 | escapeRegex: boolean, 1173 | highlight: boolean, 1174 | indent: number, 1175 | maxDepth: number, 1176 | min: boolean, 1177 | plugins: JestPrettyFormatPlugins, 1178 | printFunctionName: boolean, 1179 | spacing: string, 1180 | theme: {| 1181 | comment: string, 1182 | content: string, 1183 | prop: string, 1184 | tag: string, 1185 | value: string, 1186 | |}, 1187 | |}; 1188 | 1189 | type JestPrettyFormatPlugin = { 1190 | print: ( 1191 | val: any, 1192 | serialize: JestPrettyFormatPrint, 1193 | indent: JestPrettyFormatIndent, 1194 | opts: JestPrettyFormatOptions, 1195 | colors: JestPrettyFormatColors 1196 | ) => string, 1197 | test: (any) => boolean, 1198 | ... 1199 | }; 1200 | 1201 | type JestPrettyFormatPlugins = Array; 1202 | 1203 | /** The expect function is used every time you want to test a value */ 1204 | declare var expect: { 1205 | /** The object that you want to make assertions against */ 1206 | ( 1207 | value: any 1208 | ): JestExpectType & 1209 | JestPromiseType & 1210 | EnzymeMatchersType & 1211 | DomTestingLibraryType & 1212 | JestJQueryMatchersType & 1213 | JestStyledComponentsMatchersType & 1214 | JestExtendedMatchersType & 1215 | SnapshotDiffType, 1216 | /** Add additional Jasmine matchers to Jest's roster */ 1217 | extend(matchers: { [name: string]: JestMatcher, ... }): void, 1218 | /** Add a module that formats application-specific data structures. */ 1219 | addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, 1220 | assertions(expectedAssertions: number): void, 1221 | hasAssertions(): void, 1222 | any(value: mixed): JestAsymmetricEqualityType, 1223 | anything(): any, 1224 | arrayContaining(value: $ReadOnlyArray): Array, 1225 | objectContaining(value: { ... }): Object, 1226 | /** Matches any received string that contains the exact expected string. */ 1227 | stringContaining(value: string): string, 1228 | stringMatching(value: string | RegExp): string, 1229 | not: { 1230 | arrayContaining: (value: $ReadOnlyArray) => Array, 1231 | objectContaining: (value: { ... }) => Object, 1232 | stringContaining: (value: string) => string, 1233 | stringMatching: (value: string | RegExp) => string, 1234 | ... 1235 | }, 1236 | ... 1237 | }; 1238 | 1239 | // TODO handle return type 1240 | // http://jasmine.github.io/2.4/introduction.html#section-Spies 1241 | declare function spyOn(value: mixed, method: string): Object; 1242 | 1243 | /** Holds all functions related to manipulating test runner */ 1244 | declare var jest: JestObjectType; 1245 | 1246 | /** 1247 | * The global Jasmine object, this is generally not exposed as the public API, 1248 | * using features inside here could break in later versions of Jest. 1249 | */ 1250 | declare var jasmine: { 1251 | DEFAULT_TIMEOUT_INTERVAL: number, 1252 | any(value: mixed): JestAsymmetricEqualityType, 1253 | anything(): any, 1254 | arrayContaining(value: Array): Array, 1255 | clock(): JestClockType, 1256 | createSpy(name: string): JestSpyType, 1257 | createSpyObj( 1258 | baseName: string, 1259 | methodNames: Array 1260 | ): { [methodName: string]: JestSpyType, ... }, 1261 | objectContaining(value: Object): Object, 1262 | stringMatching(value: string): string, 1263 | ... 1264 | }; 1265 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export default class MultiRef { 2 | readonly map: ReadonlyMap; 3 | ref(key: K): (value: V|null) => any; 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-multi-ref", 3 | "version": "1.0.2", 4 | "description": "Utility for keeping references to multiple React elements.", 5 | "main": "js/index.js", 6 | "sideEffects": false, 7 | "scripts": { 8 | "prepare": "rimraf js && babel -s true -d js/ src/ --ignore '**/*.test.js' && flow-copy-source -v src js --ignore '**/*.test.*'", 9 | "test": "npm run lint && flow check && jest && tsc", 10 | "lint": "eslint .", 11 | "lint-fix": "eslint . --fix" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Macil/react-multi-ref.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "ref" 20 | ], 21 | "author": "Chris Cowan ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/Macil/react-multi-ref/issues" 25 | }, 26 | "homepage": "https://github.com/Macil/react-multi-ref#readme", 27 | "dependencies": { 28 | "@babel/runtime": "^7.24.4" 29 | }, 30 | "devDependencies": { 31 | "@babel/cli": "^7.0.0", 32 | "@babel/core": "^7.0.0", 33 | "@babel/eslint-parser": "^7.18.9", 34 | "@babel/plugin-transform-runtime": "^7.1.0", 35 | "@babel/preset-env": "^7.0.0", 36 | "@babel/preset-flow": "^7.0.0", 37 | "@babel/register": "^7.0.0", 38 | "babel-jest": "^29.0.3", 39 | "eslint": "^8.23.0", 40 | "eslint-plugin-flowtype": "^8.0.3", 41 | "flow-bin": "^0.235.1", 42 | "flow-copy-source": "^2.0.0", 43 | "jest": "^29.0.3", 44 | "rimraf": "^5.0.5", 45 | "typescript": "^5.4.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | type RefFn = (value: V|null) => mixed; 4 | 5 | export default class MultiRef { 6 | map: Map = new Map(); 7 | 8 | #refFns: Map> = new Map(); 9 | 10 | ref(key: K): RefFn { 11 | const refFn = this.#refFns.get(key); 12 | if (refFn) { 13 | return refFn; 14 | } else { 15 | const refFn: RefFn = value => { 16 | if (value == null) { 17 | // Support for React <=18, which cleans up ref functions by calling them 18 | // with null. 19 | this.#refFns.delete(key); 20 | this.map.delete(key); 21 | } else { 22 | this.#refFns.set(key, refFn); 23 | this.map.set(key, value); 24 | // React 19+ cleanup support 25 | return () => { 26 | this.#refFns.delete(key); 27 | this.map.delete(key); 28 | }; 29 | } 30 | }; 31 | // We don't put `refFn` into `this._refFns` yet, because if the current render 32 | // is aborted, then it's possible than `refFn(null)` won't be called later 33 | // and its cleanup will never happen. We shouldn't cause any side effects that 34 | // need cleaning up later until `refFn` gets called with a non-null value. 35 | return refFn; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import MultiRef from '.'; 4 | 5 | function basicTest(react19Api: boolean) { 6 | const mr = new MultiRef(); 7 | 8 | const originalRef1 = mr.ref(1); 9 | const originalRef2 = mr.ref(2); 10 | 11 | expect(originalRef2).not.toBe(originalRef1); 12 | 13 | // These will be new functions not matching the above, because 14 | // the value isn't cached until it's used. This is important because 15 | // if a React render is aborted (likely when features of concurrent 16 | // rendering are used), then the function returned by `ref` may not 17 | // get called, which means we won't get a chance to un-remember the 18 | // returned function. We only remember a returned function when it's 19 | // called (with a non-null value), because that's the only point we're 20 | // guaranteed the returned function will get a chance to unremember 21 | // itself later. 22 | const ref1 = mr.ref(1); 23 | const ref2 = mr.ref(2); 24 | 25 | expect(ref2).not.toBe(ref1); 26 | 27 | expect(ref1).not.toBe(originalRef1); 28 | expect(ref2).not.toBe(originalRef2); 29 | 30 | ref1('123'); 31 | ref2('456'); 32 | 33 | expect(mr.ref(1)).toBe(ref1); 34 | expect(mr.ref(2)).toBe(ref2); 35 | 36 | mr.ref(1)('abc'); 37 | mr.ref(2)('def'); 38 | 39 | expect(mr.map.get(1)).toBe('abc'); 40 | expect(mr.map.get(2)).toBe('def'); 41 | 42 | expect(mr.ref(1)).toBe(ref1); 43 | expect(mr.ref(2)).toBe(ref2); 44 | 45 | const ref1Cleanup: Function = mr.ref(1)('ABC'); 46 | 47 | expect(mr.map.get(1)).toBe('ABC'); 48 | expect(mr.map.get(2)).toBe('def'); 49 | 50 | expect(mr.ref(1)).toBe(ref1); 51 | expect(mr.ref(2)).toBe(ref2); 52 | 53 | if (react19Api) { 54 | ref1Cleanup(); 55 | } else { 56 | mr.ref(1)(null); 57 | } 58 | 59 | expect(mr.map.has(1)).toBe(false); 60 | expect(mr.map.get(2)).toBe('def'); 61 | 62 | const ref1b = mr.ref(1); 63 | expect(ref1b).not.toBe(ref1); 64 | 65 | ref1b('temporary'); 66 | 67 | expect(mr.ref(1)).toBe(ref1b); 68 | expect(mr.ref(2)).toBe(ref2); 69 | 70 | mr.ref(1)('a_b_c'); 71 | 72 | expect(mr.map.get(1)).toBe('a_b_c'); 73 | expect(mr.map.get(2)).toBe('def'); 74 | 75 | mr.ref(2)('d_e_f'); 76 | 77 | expect(mr.map.get(1)).toBe('a_b_c'); 78 | expect(mr.map.get(2)).toBe('d_e_f'); 79 | 80 | expect(mr.ref(1)).toBe(ref1b); 81 | expect(mr.ref(2)).toBe(ref2); 82 | } 83 | 84 | test('works with React <=18 API', () => { 85 | basicTest(false); 86 | }); 87 | 88 | test('works with React 19 API', () => { 89 | basicTest(true); 90 | }); 91 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true, 5 | "module": "commonjs", 6 | "target": "ES2015", 7 | "moduleResolution": "node", 8 | "moduleDetection": "force" 9 | } 10 | } 11 | --------------------------------------------------------------------------------