forked from pmndrs/jotai
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: waitFor utilities (pmndrs#358)
* wip: waitForAll * feat: waitForAll * wip returning a record from waitForAll * feat: use weak cache to keep waitForAll atom reference * Add possibly for records in waitFor * Cleanup types * fix types, improve tests * fix: do not cache atoms with named * Add error handling test * Fix test and timers * Add docs * Fix header * chore remove unused import * Return promise.all * Update size snapshot * chore: remove unused/uneffective atom cache * Update docs with csb Co-authored-by: daishi <[email protected]> Co-authored-by: Mathis Møller <[email protected]>
- Loading branch information
1 parent
3bd4ab0
commit 7fcabcf
Showing
5 changed files
with
361 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { Atom, atom } from 'jotai' | ||
|
||
export function waitForAll<Values extends Record<string, unknown>>( | ||
atoms: { [K in keyof Values]: Atom<Values[K]> } | ||
): Atom<Values> | ||
|
||
export function waitForAll<Values extends unknown[]>( | ||
atoms: { [K in keyof Values]: Atom<Values[K]> } | ||
): Atom<Values> | ||
|
||
export function waitForAll<Values extends Record<string, unknown> | unknown[]>( | ||
atoms: { [K in keyof Values]: Atom<Values[K]> } | ||
) { | ||
const derivedAtom = atom((get) => { | ||
const promises: Promise<unknown>[] = [] | ||
const values = unwrapAtoms(atoms).map((anAtom, index) => { | ||
try { | ||
return get(anAtom) | ||
} catch (e) { | ||
if (e instanceof Promise) { | ||
promises[index] = e | ||
} else { | ||
throw e | ||
} | ||
} | ||
}) | ||
if (promises.length) { | ||
return Promise.all(promises) | ||
} | ||
return wrapResults(atoms, values) | ||
}) | ||
return derivedAtom | ||
} | ||
|
||
const unwrapAtoms = <Values extends Record<string, unknown> | unknown[]>( | ||
atoms: { [K in keyof Values]: Atom<Values[K]> } | ||
): Atom<unknown>[] => | ||
Array.isArray(atoms) | ||
? atoms | ||
: Object.getOwnPropertyNames(atoms).map( | ||
(key) => (atoms as Record<string, Atom<unknown>>)[key] | ||
) | ||
|
||
const wrapResults = <Values extends Record<string, unknown> | unknown[]>( | ||
atoms: { [K in keyof Values]: Atom<Values[K]> }, | ||
results: unknown[] | ||
): unknown[] | Record<string, unknown> => | ||
Array.isArray(atoms) | ||
? results | ||
: Object.getOwnPropertyNames(atoms).reduce( | ||
(out, key, idx) => ({ ...out, [key]: results[idx] }), | ||
{} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
import React, { Fragment, StrictMode, Suspense } from 'react' | ||
import { render } from '@testing-library/react' | ||
import { Provider as ProviderOrig, atom, useAtom } from '../../src/index' | ||
import { waitForAll } from '../../src/utils' | ||
|
||
const Provider = process.env.PROVIDER_LESS_MODE ? Fragment : ProviderOrig | ||
|
||
const consoleError = console.error | ||
beforeEach(() => { | ||
console.error = jest.fn() | ||
}) | ||
afterEach(() => { | ||
console.error = consoleError | ||
}) | ||
|
||
jest.useFakeTimers() | ||
|
||
class ErrorBoundary extends React.Component< | ||
{ message?: string }, | ||
{ hasError: boolean } | ||
> { | ||
constructor(props: { message?: string }) { | ||
super(props) | ||
this.state = { hasError: false } | ||
} | ||
static getDerivedStateFromError() { | ||
return { hasError: true } | ||
} | ||
render() { | ||
return this.state.hasError ? ( | ||
<div>{this.props.message || 'errored'}</div> | ||
) : ( | ||
this.props.children | ||
) | ||
} | ||
} | ||
|
||
it('waits for two async atoms', async () => { | ||
let isAsyncAtomRunning = false | ||
let isAnotherAsyncAtomRunning = false | ||
const asyncAtom = atom(async () => { | ||
isAsyncAtomRunning = true | ||
await new Promise((resolve) => { | ||
setTimeout(() => { | ||
isAsyncAtomRunning = false | ||
resolve(true) | ||
}, 10) | ||
}) | ||
return 1 | ||
}) | ||
const anotherAsyncAtom = atom(async () => { | ||
isAnotherAsyncAtomRunning = true | ||
await new Promise((resolve) => { | ||
setTimeout(() => { | ||
isAnotherAsyncAtomRunning = false | ||
resolve(true) | ||
}, 10) | ||
}) | ||
return '2' | ||
}) | ||
|
||
const Counter: React.FC = () => { | ||
const [[num1, num2]] = useAtom(waitForAll([asyncAtom, anotherAsyncAtom])) | ||
return ( | ||
<> | ||
<div>num1: {num1}</div> | ||
<div>num2: {num2}</div> | ||
</> | ||
) | ||
} | ||
|
||
const { findByText } = render( | ||
<StrictMode> | ||
<Provider> | ||
<Suspense fallback="loading"> | ||
<Counter /> | ||
</Suspense> | ||
</Provider> | ||
</StrictMode> | ||
) | ||
|
||
await findByText('loading') | ||
expect(isAsyncAtomRunning).toBe(true) | ||
expect(isAnotherAsyncAtomRunning).toBe(true) | ||
|
||
jest.runOnlyPendingTimers() | ||
|
||
await findByText('num1: 1') | ||
await findByText('num2: 2') | ||
expect(isAsyncAtomRunning).toBe(false) | ||
expect(isAnotherAsyncAtomRunning).toBe(false) | ||
}) | ||
|
||
it('can use named atoms in derived atom', async () => { | ||
let isAsyncAtomRunning = false | ||
let isAnotherAsyncAtomRunning = false | ||
const asyncAtom = atom(async () => { | ||
isAsyncAtomRunning = true | ||
await new Promise((resolve) => { | ||
setTimeout(() => { | ||
isAsyncAtomRunning = false | ||
resolve(true) | ||
}, 10) | ||
}) | ||
return 1 | ||
}) | ||
const anotherAsyncAtom = atom(async () => { | ||
isAnotherAsyncAtomRunning = true | ||
await new Promise((resolve) => { | ||
setTimeout(() => { | ||
isAnotherAsyncAtomRunning = false | ||
resolve(true) | ||
}, 10) | ||
}) | ||
return 'a' | ||
}) | ||
|
||
const combinedWaitingAtom = atom((get) => { | ||
const { num, str } = get( | ||
waitForAll({ | ||
num: asyncAtom, | ||
str: anotherAsyncAtom, | ||
}) | ||
) | ||
return { num: num * 2, str: str.toUpperCase() } | ||
}) | ||
|
||
const Counter: React.FC = () => { | ||
const [{ num, str }] = useAtom(combinedWaitingAtom) | ||
return ( | ||
<> | ||
<div>num: {num}</div> | ||
<div>str: {str}</div> | ||
</> | ||
) | ||
} | ||
|
||
const { findByText } = render( | ||
<StrictMode> | ||
<Provider> | ||
<Suspense fallback="loading"> | ||
<Counter /> | ||
</Suspense> | ||
</Provider> | ||
</StrictMode> | ||
) | ||
|
||
await findByText('loading') | ||
expect(isAsyncAtomRunning).toBe(true) | ||
expect(isAnotherAsyncAtomRunning).toBe(true) | ||
|
||
jest.runOnlyPendingTimers() | ||
|
||
await findByText('num: 2') | ||
await findByText('str: A') | ||
expect(isAsyncAtomRunning).toBe(false) | ||
expect(isAnotherAsyncAtomRunning).toBe(false) | ||
}) | ||
|
||
it('can handle errors', async () => { | ||
let isAsyncAtomRunning = false | ||
let isErrorAtomRunning = false | ||
const asyncAtom = atom(async () => { | ||
isAsyncAtomRunning = true | ||
await new Promise((resolve) => { | ||
setTimeout(() => { | ||
isAsyncAtomRunning = false | ||
resolve(true) | ||
}, 10) | ||
}) | ||
return 1 | ||
}) | ||
const errorAtom = atom(async () => { | ||
isErrorAtomRunning = true | ||
await new Promise((_, reject) => { | ||
setTimeout(() => { | ||
isErrorAtomRunning = false | ||
reject('Charlotte') | ||
}, 10) | ||
}) | ||
return 'a' | ||
}) | ||
|
||
const combinedWaitingAtom = atom((get) => { | ||
return get( | ||
waitForAll({ | ||
num: asyncAtom, | ||
error: errorAtom, | ||
}) | ||
) | ||
}) | ||
|
||
const Counter: React.FC = () => { | ||
const [{ num, error }] = useAtom(combinedWaitingAtom) | ||
return ( | ||
<> | ||
<div>num: {num}</div> | ||
<div>str: {error}</div> | ||
</> | ||
) | ||
} | ||
|
||
const { findByText } = render( | ||
<StrictMode> | ||
<Provider> | ||
<ErrorBoundary> | ||
<Suspense fallback="loading"> | ||
<Counter /> | ||
</Suspense> | ||
</ErrorBoundary> | ||
</Provider> | ||
</StrictMode> | ||
) | ||
|
||
await findByText('loading') | ||
expect(isAsyncAtomRunning).toBe(true) | ||
expect(isErrorAtomRunning).toBe(true) | ||
|
||
jest.runOnlyPendingTimers() | ||
|
||
await findByText('errored') | ||
expect(isAsyncAtomRunning).toBe(false) | ||
expect(isErrorAtomRunning).toBe(false) | ||
}) |