Skip to content
This repository was archived by the owner on Mar 3, 2022. It is now read-only.

Commit 7a5a69c

Browse files
Cleaning up how the hooks diff args
1 parent dd40de2 commit 7a5a69c

12 files changed

+284
-188
lines changed

README.md

+75-59
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ That template is Copyright (c) 2018 Othneil Drew licensed under the MIT license.
2222
<h3 align="center">Suspension</h3>
2323

2424
<p align="center">
25-
A "hook in place" approach to easily integrating your existing Promise-based async data fetchers with React <pre><Suspense></pre> components.
25+
A "hook in place" approach to easily integrating your existing Promises and async/await data fetchers with React <pre><Suspense></pre> components.
2626
<br />
2727
<a href="https://github.com/zackdotcomputer/suspension"><strong>Explore the docs »</strong></a>
2828
<br />
@@ -53,7 +53,7 @@ That template is Copyright (c) 2018 Othneil Drew licensed under the MIT license.
5353
<li><a href="#suspension-hooks">The Hooks</a></li>
5454
</ul>
5555
</li>
56-
<li><a href="#usage-and-bits">Usage and bits</a></li>
56+
<li><a href="#usage-and-details">Usage and Details</a></li>
5757
<li><a href="#roadmap">Roadmap</a></li>
5858
<li><a href="#contributing">Contributing</a></li>
5959
<li><a href="#license">License</a></li>
@@ -74,7 +74,9 @@ The core concept is to be as simple as possible. Add a "rig" high up in your app
7474

7575
### Built For
7676

77-
At the moment, the library is built for the React web build and requires version 16.13 or newer. If you'd like a build for react-native, drop by [this Issue](https://github.com/zackdotcomputer/suspension/issues/1) and discuss.
77+
At the moment, the library is built for the React web build and requires version 16.13 or newer.
78+
79+
A react-native build would be realistically possible as well, so if you'd like that please drop by [this Issue](https://github.com/zackdotcomputer/suspension/issues/1) and discuss.
7880

7981
<!-- GETTING STARTED -->
8082

@@ -112,7 +114,7 @@ function MyApp() {
112114

113115
### Suspension Hooks
114116

115-
Now that your app is rigged, your ready to use suspenseful loads! The key feature of `suspension` is that it makes using your existing Promises as easy as a hook. Here is an example in Typescript:
117+
Now that your app is rigged, your ready to use the hooks! The key feature of `suspension` is that it makes using your existing Promises with Suspense as easy as a hook. Here is an example in Typescript:
116118

117119
```tsx
118120
function UserProfile() {
@@ -138,87 +140,100 @@ function UserProfile() {
138140

139141
<!-- USAGE EXAMPLES -->
140142

141-
## Usage and bits
143+
## Usage and Details
142144

143145
There are three main pieces to `suspension`:
144146

145-
### `<SuspensionRig>` - Cache and fallback
147+
### `<SuspensionRig>` - Your cache and fallback
146148

147149
`<SuspensionRig>` is a safety net for your suspension calls to fall back on. Because
148150
suspense uses `throw` to interrupt the render process, anything that was stored in state
149151
from that point up to the last `<Suspense>` component will be destroyed. `<SuspensionRig>`
150-
gives a safe space **above** the last `<Suspense>` for the hooks to store their data. It
151-
serves three main purposes in that way:
152+
gives a safe space **above** the last `<Suspense>` for the hooks to store their data. In addition
153+
to just being a landing space for Promises, the rig has three other roles:
152154

153155
1. **It is a cache**. The rig is responsible for caching all the data about the promises
154-
linked to hooks beneath it. There is no limit, though, on the number of rigs you place in
155-
your tree and any hooks you call will use the nearest ancestor rig. So, if you have a page
156-
that loads in a lot of data using Suspense hooks, consider your router wrap that page in its
157-
own rig so that the data is cleaned up when the page is unmounted.
158-
159-
2. **It is the ulimate `<Suspense>`**. Because the rig needs a `Suspense` barrier between it
160-
and its hooks (otherwise it would be swept away by the loading event as well), it was a common
161-
pattern from day one to make the rig's first child a Suspense. So, that's just built-in now.
162-
If you want the `SuspenseRig` to behave as both a cache and a fallback `Suspense`, you just
163-
need to give it the same `fallback` prop you would give a `Suspense`.
164-
165-
3. **It is your Error Boundary**. Similarly to how you can pass the `fallback` prop to your rig to
166-
have it act as a Suspense, you can also pass an `errorBoundary` prop to have it act as your Error
167-
Boundary. Under the hood, this feature uses [react-error-boundary](https://github.com/bvaughn/react-error-boundary)
168-
to implement the error boundary. You can pass anything in the `errorBoundary` object that you
169-
would pass as a prop to that library. If the object is present at all, the rig will insert
170-
a boundary below itself and above the Suspense (if included) and children.
156+
linked to hooks beneath it. There is no limit on the number of rigs you place in
157+
your tree. Suspension hooks will use their nearest ancestor rig. So, if you have a single
158+
component that loads in a lot of data, consider your router wrap that page in its own rig
159+
so that the data is cleaned up when the rig is unmounted.
160+
161+
2. **It is your fallback `<Suspense>`**. Because the rig needs a `Suspense` barrier between it
162+
and its hooks (otherwise it would be swept away by the loading throw as well), it was a common
163+
pattern from day one to make the rig's first child a Suspense. So, I built that in.
164+
If you give the `SuspenseRig` the same `fallback` prop as you would give a `Suspense`, the
165+
right will automatically create a `Suspense` wall below it with that fallback.
166+
167+
3. **It is your Error Boundary**. Similarly you can also pass an `errorBoundary` prop to have it
168+
act as your Error Boundary. Under the hood, this feature uses
169+
[react-error-boundary](https://github.com/bvaughn/react-error-boundary).
170+
You can pass anything in the `errorBoundary` object that you can pass as a prop to that library.
171+
If the prop is present, the rig will insert a boundary below itself and above its other children.
171172

172173
### `useSuspension` - The ready-to-go hook
173174

174-
`useSuspension(generator, cacheKey, args, options)` is the primary hook for accessing suspension.
175-
It takes a parameter-free generator function that returns a Promise and a cache key that uniquely
176-
identifies this call's purpose (see below).
175+
`useSuspension` is the primary hook for accessing suspension. It has a slightly different form
176+
depending on whether your data generator function takes arguments or not, but both forms share
177+
the need for a generator, a cacheKey that identifies this call's purpose (see below), and an
178+
optional object with configuration options. If your generator takes args, you can pass those
179+
as an array as well.
180+
181+
```typescript
182+
// With a generator that takes args:
183+
useSuspension(generator, cacheKey, argsArray, options);
184+
185+
// With a generator that doesn't:
186+
useSuspension(generator, cacheKey, options);
187+
```
177188

178-
As soon as you call this hook, it will start your generator going and interrupt the render cycle
179-
at that point. (Note: even if your generator returns a resolved promise, React will always do at
189+
The first time you call this hook it will immediately start your generator and interrupt the
190+
render cycle. (Note: even if your generator returns a pre-resolved promise, React will always do at
180191
least one render update falling back to the `<Suspense>`.) Once the Promise has resolved, the tree
181192
under the nearest Suspense will be reloaded and the hook will **then** either provide you with the
182-
result of the `Promise` or throw the `Error` for your nearest ErrorBoundary if the Promise was
193+
result of your generator or throw the `Error` for your nearest ErrorBoundary if the Promise was
183194
rejected.
184195

185-
This construction means that your render function will never proceed beyond this call unless it can
186-
return to you the successfully resolved value from your `Promise`. No more needing to deal with
187-
`undefined` loading values.
196+
This means that your render function will never proceed beyond this call unless it can return
197+
the resolved value from your `Promise`. No more needing to deal with `undefined` loading values.
188198

189-
### `useLazySuspension` - For those asynchronous asynchronous cases
199+
### `useLazySuspension` - For a bit more finesse
190200

191-
`useLazySuspension(generator, cacheKey, options)` is your friend for calls where the arguments to your
192-
generator might change between render cycles, or where you might need to delay your generator. It takes
193-
a generator function which can now take any number of parameters but still must return a Promise. It
194-
also still takes that same cache key that uniquely identifies this call's purpose (still see below).
201+
`useLazySuspension(generator, cacheKey, options)` is for calls where you need more control over
202+
when or whether your generator is called. This hook takes the same parameters as `useSuspension`
203+
except that you never pass the args to the hook.
195204

196-
When you call this hook you will get back an array. The first element is your result and it will start
197-
as `undefined`. The second element is your trigger function. It will have the same parameter expectations
198-
as your generators. Whenever you want to start a new call, call the trigger function with the args.
205+
This hook returns an array with two elements. The first element is your lazy reader function. It takes
206+
the args for your generator and will return the resolved value if it has one for those args, undefined
207+
if it doesn't, or will throw an error if the Promise was rejected. If the promise is still in progress,
208+
this reader will throw the pending promise to trigger Suspense.
199209

200-
The trigger function also uses a bit of trickery to trigger a re-render of your component, so as soon
201-
as you call it the host component will redraw and disappear from screen, falling back to the nearest
202-
`<Suspense>` until the load is complete. Once the Promise has resolved, the tree under that Suspense
203-
will be rerendered. At that point this hook will do one of two things. If the Promise resolved, the hook
204-
will return the same array but now the first element will be the resolved value from the Promise. If
205-
the hook was rejected, the hook will throw a `SuspensionResolutionFailedError`, which can be caught
206-
by an ErrorBoundary. This error will have the underlying failure as well as a `retryFunction` that can
207-
be used to retry the generator with the same args.
210+
The second element is your suspenseful loading function. It also should be called with the args for your
211+
generator. This function will always return the resolved value for those args, or it will throw a Promise
212+
if it does not yet have them. **Note** this function will never throw an Error on failure. If you call
213+
it and the most recent Promise was rejected, this function will start a new call.
214+
215+
### Failures and `SuspensionResolutionFailedError`
216+
217+
If the `useSuspension` hook's promise is rejected or if the lazy reader is used to access a value that
218+
most recently was rejected, those calls will throw a `SuspensionResolutionFailedError`.
219+
This error can be caught by an ErrorBoundary and will contain the underlying error from the Promise
220+
as well as a `retryFunction` that can be used to retry the generator with the same args.
208221

209222
### Caching
210223

211-
When you call the `useSuspension` hook or the `useLazySuspension` trigger function the Rig will check
212-
if results are already cached. It will do this based on your supplied cache key and the args array you
213-
provide. If the args array is the same (by default determined by walking the args array with a `===`
214-
check) and the last Promise is loading or resolved, then it will not start a new call. If the last call
215-
was rejected or the args array appears to be new, then the previous results will be discarded and a new
216-
call started from your generator.
224+
When you call the `useSuspension` hook or call the functions returned by `useLazySuspension` the
225+
Rig will check if results are already cached.
226+
227+
It will do this based on your supplied cache key and the args list you provide. By default the args
228+
lists are compared using the `===` comparison. You can override this using the options object's
229+
`shouldRefreshData` property, which should be a function that takes two arrays of arguments and returns
230+
whether they are different enough to merit a refresh.
217231

218232
### Cache Keys
219233

220-
Because the hooks work by hanging their Promises and generators in the rig while they resolve, you need
221-
to provide the hooks with a cache-key that describes their _purpose._
234+
Because the hooks work by hanging their Promises and results in the rig, you need to provide a
235+
cache-key that describes each hook's _purpose._ This is to avoid collisions between two accesses
236+
of the same data source with different parameters.
222237

223238
For example, let's imagine a simple social app. Consider the following four suspensions involved in rendering a profile page:
224239

@@ -227,7 +242,7 @@ For example, let's imagine a simple social app. Consider the following four susp
227242
3. Fetching the target profile to be displayed.
228243
4. Fetching the posts owned by the target profile to be displayed.
229244

230-
In this sample case, we should use 3 unique cache keys: a shared one for 1 and 2, and then unique ones for 3 and 4. This is because suspensions 1 and 2 will always make the same data request with **the same parameters**, so they will always return the same data. Suspensions 2 and 3, though, might use different parameters if you're viewing someone else's profile page, so they should use distinct cacheKeys so they don't overwrite each others' values. Finally, even though suspensions 3 and 4 will probably have the same parameters (`{target: targetUserId}`), they are fetching different kinds of data. So, they should have different cacheKeys from each other as well.
245+
In this sample case, we should use 3 unique cache keys: a shared one for 1 and 2, and then unique ones for 3 and 4. This is because suspensions 1 and 2 will always make the same data request with **the same parameters**, so they will always return the same data. Suspensions 2 and 3, though, might use different parameters if you're viewing someone else's profile page, so they should use distinct cacheKeys to avoid overwriting each others' values. Finally, even though suspensions 3 and 4 will probably have the same parameters (`{target: targetUserId}`), they are fetching different kinds of data. So, they should have different cacheKeys from each other as well.
231246

232247
<!-- _For more examples, please refer to the [Documentation](https://example.com)_ -->
233248

@@ -281,6 +296,7 @@ Project Link: [https://github.com/zackdotcomputer/suspension](https://github.com
281296
- [Best Readme Template](https://github.com/othneildrew/Best-README-Template)
282297
- [Img Shields](https://shields.io)
283298
- [Prettycons](https://thenounproject.com/andrei.manolache7/)
299+
- Though not a fork, Suspension is nonetheless building on the conceptual foundation laid by the [use-async-resource](https://github.com/andreiduca/use-async-resource) package and I'd be remiss not to acknolwedge that project.
284300

285301
<!-- MARKDOWN LINKS & IMAGES -->
286302
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->

example/index.tsx

+12-3
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ const PokeInfo = ({ pokemonNumber }: { pokemonNumber: number }) => {
3333
);
3434
};
3535

36-
const PokeForm = ({ onDoLoad }: { onDoLoad: (no: number) => void }) => {
37-
const [pokeNumber, setPokeNumber] = React.useState<number>(25);
36+
const PokeForm = ({
37+
initialPokeNumber,
38+
onDoLoad
39+
}: {
40+
initialPokeNumber?: number;
41+
onDoLoad: (no: number) => void;
42+
}) => {
43+
const [pokeNumber, setPokeNumber] = React.useState<number>(initialPokeNumber ?? 25);
3844

3945
return (
4046
<div>
@@ -98,7 +104,10 @@ const App = () => {
98104
>
99105
<PokeInfo pokemonNumber={renderedPokeNumber} />
100106
<br />
101-
<PokeForm onDoLoad={(n) => setRenderedPokeNumber(n)} />
107+
<PokeForm
108+
onDoLoad={(n) => setRenderedPokeNumber(n)}
109+
initialPokeNumber={renderedPokeNumber}
110+
/>
102111
</SuspensionRig>
103112
</main>
104113
);

src/@types/LazySuspendableResult.ts

-22
This file was deleted.

src/@types/LazySuspensionReaders.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import FunctionWithArgs from "./FunctionWithArgs";
2+
3+
/**
4+
* The result of a lazy suspension call is an array with two elements.
5+
* - The first one is your lazy reader function. It will return the value if it has
6+
* been cached successfully with the provided args, undefined if it hasn't been,
7+
* or it will throw a promise or error if the attempt is in a loading or error
8+
* state.
9+
* - The second element is the active reader function. It is the same as the lazy
10+
* reader except that the unstarted and failed cases (undefined and thrown Error)
11+
* will instead start a new call to your generator and throw the resulting promise.
12+
*/
13+
export type LazySuspensionReaders<Result, Args extends any[] = []> = [
14+
FunctionWithArgs<Args, Result | undefined>,
15+
FunctionWithArgs<Args, Result>
16+
];

src/@types/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ export type {
77
UnstartedCallState
88
} from "./CallState";
99
export type { default as FunctionWithArgs } from "./FunctionWithArgs";
10-
export type { LazySuspendableResult } from "./LazySuspendableResult";
10+
export type { LazySuspensionReaders } from "./LazySuspensionReaders";
1111
export type { Suspendable, SuspendableWithArgs } from "./Suspendable";

src/SuspensionRig/SuspensionRig.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,9 @@ export default function SuspensionRig({ children, fallback, errorBoundary }: Pro
5656
children
5757
);
5858

59-
console.debug("Error", errorBoundary);
6059
const errorWrapping =
6160
errorBoundary !== undefined ? (
6261
<>
63-
<div>errrrrr</div>
6462
<ErrorBoundary {...errorBoundary}>{suspenseWrapping}</ErrorBoundary>
6563
</>
6664
) : (

0 commit comments

Comments
 (0)