Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useStreamJSON hook #26

Merged
merged 21 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
de089af
Add `useJSON` hook, to allow streaming through JSON files.
sxn Mar 13, 2024
108927b
Reimplement on top of `useCachedPromise`, add support for `file://` U…
sxn Mar 14, 2024
52a1dcb
Switch to accepting `RequestInfo` instead of just a string.
sxn Mar 15, 2024
0280b82
Add docs.
sxn Mar 15, 2024
e20234b
pass abort signal around to make sure fetch & pipeline calls get clos…
sxn Mar 18, 2024
5180422
add support for `transform` function
sxn Mar 18, 2024
703552b
rename `useJSON` to `useStreamJSON`
sxn Mar 18, 2024
60b1967
add `dataPath` option to allow working with arrays wrapped in objects.
sxn Mar 18, 2024
e9dd4e8
allow `file:///` urls that don't end in `json`
sxn Mar 18, 2024
3518759
allow `file:///` urls that don't end in `json`
sxn Mar 18, 2024
6a070ab
directly create stream out of ResourceInfo, instead of caching it to …
sxn Mar 19, 2024
acef748
Revert "directly create stream out of ResourceInfo, instead of cachin…
sxn Mar 19, 2024
015c9c8
allow `RegExp` `dataPath`s
sxn Mar 19, 2024
899e71b
don't swallow errors that occur during streaming
sxn Mar 19, 2024
c8d6198
mention how to use RegExp dataPaths
sxn Mar 20, 2024
652c8b4
force a cache update when the URL changes but the cache destination d…
sxn Mar 21, 2024
0b5b23a
allow flattening deeply-nested data using the `transform` option
sxn Mar 26, 2024
1590690
add `flatten` example
sxn Mar 27, 2024
cd7ced7
remove `folder` and `fileName` options
sxn Mar 27, 2024
0054247
fix test
sxn Mar 27, 2024
9d30cac
bump version
sxn Apr 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/utils-reference/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ npm install --save @raycast/utils

## Changelog

### v1.14.0

- Add `useStreamJSON` hook.

### v1.13.6

- Updated `useFetch`'s `mapResult` type to allow returning `cursor` in addition to `data` and `hasMore`.
Expand Down
4 changes: 2 additions & 2 deletions docs/utils-reference/react-hooks/useCachedPromise.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ function useCachedPromise<T, U>(
execute?: boolean;
onError?: (error: Error) => void;
onData?: (data: Result<T>) => void;
onWillExecute?: (args: Parameters<T>) -> void;
}
onWillExecute?: (args: Parameters<T>) => void;
},
): AsyncState<Result<T>> & {
revalidate: () => void;
mutate: MutatePromise<Result<T> | U>;
Expand Down
6 changes: 3 additions & 3 deletions docs/utils-reference/react-hooks/useFetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ export function useFetch<V, U, T = V>(
url: RequestInfo,
options?: RequestInit & {
parseResponse?: (response: Response) => Promise<V>;
mapResult?: (result: V) => { data: T }
mapResult?: (result: V) => { data: T };
initialData?: U;
keepPreviousData?: boolean;
execute?: boolean;
onError?: (error: Error) => void;
onData?: (data: T) => void;
onWillExecute?: (args: [string, RequestInit]) -> void;
}
onWillExecute?: (args: [string, RequestInit]) => void;
},
): AsyncState<T> & {
revalidate: () => void;
mutate: MutatePromise<T | U | undefined>;
Expand Down
4 changes: 2 additions & 2 deletions docs/utils-reference/react-hooks/usePromise.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ function usePromise<T>(
execute?: boolean;
onError?: (error: Error) => void;
onData?: (data: Result<T>) => void;
onWillExecute?: (args: Parameters<T>) -> void;
}
onWillExecute?: (args: Parameters<T>) => void;
},
): AsyncState<Result<T>> & {
revalidate: () => void;
mutate: MutatePromise<Result<T> | undefined>;
Expand Down
209 changes: 209 additions & 0 deletions docs/utils-reference/react-hooks/useStreamJSON.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# `useStreamJSON`

Hook which takes a `http://`, `https://` or `file:///` URL pointing to a JSON resource, caches it to the command's support folder, and streams through its content. Useful when dealing with large JSON arrays which would be too big to fit in the command's memory.

## Signature

```ts
export function useStreamJSON<T, U>(
url: RequestInfo,
options: RequestInit & {
filter?: (item: T) => boolean;
transform?: (item: any) => T;
pageSize?: number;
initialData?: U;
keepPreviousData?: boolean;
execute?: boolean;
onError?: (error: Error) => void;
onData?: (data: T) => void;
onWillExecute?: (args: [string, RequestInit]) => void;
},
): AsyncState<Result<T>> & {
revalidate: () => void;
};
```

### Arguments

- `url` - The [`RequestInfo`](https://github.com/nodejs/undici/blob/v5.7.0/types/fetch.d.ts#L12) describing the resource that needs to be fetched. Strings starting with `http://`, `https://` and `Request` objects will use `fetch`, while strings starting with `file:///` will be copied to the cache folder.

With a few options:

- `options` extends [`RequestInit`](https://github.com/nodejs/undici/blob/v5.7.0/types/fetch.d.ts#L103-L117) allowing you to specify a body, headers, etc. to apply to the request.
- `options.pageSize` the amount of items to fetch at a time. By default, 20 will be used
- `options.dataPath` is a string or regular expression informing the hook that the array (or arrays) of data you want to stream through is wrapped inside one or multiple objects, and it indicates the path it needs to take to get to it.
- `options.transform` is a function called with each top-level object encountered while streaming. If the function returns an array, the hook will end up streaming through its children, and each array item will be passed to `options.filter`. If the function returns something other than an array, _it_ will be passed to `options.filter`. Note that the hook will revalidate every time the filter function changes, so you need to use [useCallback](https://react.dev/reference/react/useCallback) to make sure it only changes when it needs to.
- `options.filter` is a function called with each object encountered while streaming. If it returns `true`, the object will be kept, otherwise it will be discarded. Note that the hook will revalidate every time the filter function changes, so you need to use [useCallback](https://react.dev/reference/react/useCallback) to make sure it only changes when it needs to.

Including the [useCachedPromise](./useCachedPromise.md)'s options:

- `options.keepPreviousData` is a boolean to tell the hook to keep the previous results instead of returning the initial value if there aren't any in the cache for the new arguments. This is particularly useful when used for data for a List to avoid flickering.

Including the [useCachedState](./useCachedState.md)'s options:

- `options.initialData` is the initial value of the state if there aren't any in the Cache yet.

Including the [usePromise](./usePromise.md)'s options:

- `options.execute` is a boolean to indicate whether to actually execute the function or not. This is useful for cases where one of the function's arguments depends on something that might not be available right away (for example, depends on some user inputs). Because React requires every hook to be defined on the render, this flag enables you to define the hook right away but wait until you have all the arguments ready to execute the function.
- `options.onError` is a function called when an execution fails. By default, it will log the error and show a generic failure toast with an action to retry.
- `options.onData` is a function called when an execution succeeds.
- `options.onWillExecute` is a function called when an execution will start..

### Return

Returns an object with the [AsyncState](#asyncstate) corresponding to the execution of the fetch as well as a couple of methods to manipulate it.

- `data`, `error`, `isLoading` - see [AsyncState](#asyncstate).
- `pagination` - the pagination object that Raycast [`List`s](https://developers.raycast.com/api-reference/user-interface/list#props) and [`Grid`s](https://developers.raycast.com/api-reference/user-interface/grid#props) expect.
- `revalidate` is a method to manually call the function with the same arguments again.
- `mutate` is a method to wrap an asynchronous update and gives some control over how the hook's data should be updated while the update is going through. By default, the data will be revalidated (eg. the function will be called again) after the update is done. See [Mutation and Optimistic Updates](#mutation-and-optimistic-updates) for more information.

## Example

```ts
import { Action, ActionPanel, List, environment } from "@raycast/api";
import { useStreamJSON } from "@raycast/utils";
import { join } from "path";
import { useCallback, useState } from "react";

type Formula = { name: string; desc?: string };

export default function Main(): JSX.Element {
const [searchText, setSearchText] = useState("");

const formulaFilter = useCallback(
(item: Formula) => {
if (!searchText) return true;
return item.name.toLocaleLowerCase().includes(searchText);
},
[searchText],
);

const formulaTransform = useCallback((item: any): Formula => {
return { name: item.name, desc: item.desc };
}, []);

const { data, isLoading, pagination } = useStreamJSON("https://formulae.brew.sh/api/formula.json", {
initialData: [] as Formula[],
pageSize: 20,
filter: formulaFilter,
transform: formulaTransform
});

return (
<List isLoading={isLoading} pagination={pagination} onSearchTextChange={setSearchText}>
<List.Section title="Formulae">
{data.map((d) => (
<List.Item key={d.name} title={d.name} subtitle={d.desc} />
))}
</List.Section>
</List>
);
}
```

## Mutation and Optimistic Updates

In an optimistic update, the UI behaves as though a change was successfully completed before receiving confirmation from the server that it was - it is being optimistic that it will eventually get the confirmation rather than an error. This allows for a more responsive user experience.

You can specify an `optimisticUpdate` function to mutate the data in order to reflect the change introduced by the asynchronous update.

When doing so, you can specify a `rollbackOnError` function to mutate back the data if the asynchronous update fails. If not specified, the data will be automatically rolled back to its previous value (before the optimistic update).

```tsx
import { Action, ActionPanel, List, environment } from "@raycast/api";
import { useStreamJSON } from "@raycast/utils";
import { join } from "path";
import { useCallback, useState } from "react";
import { setTimeout } from "timers/promises";

type Formula = { name: string; desc?: string };

export default function Main(): JSX.Element {
const [searchText, setSearchText] = useState("");

const formulaFilter = useCallback(
(item: Formula) => {
if (!searchText) return true;
return item.name.toLocaleLowerCase().includes(searchText);
},
[searchText],
);

const formulaTransform = useCallback((item: any): Formula => {
return { name: item.name, desc: item.desc };
}, []);

const { data, isLoading, mutate, pagination } = useStreamJSON("https://formulae.brew.sh/api/formula.json", {
initialData: [] as Formula[],
pageSize: 20,
filter: formulaFilter,
transform: formulaTransform,
});

return (
<List isLoading={isLoading} pagination={pagination} onSearchTextChange={setSearchText}>
<List.Section title="Formulae">
{data.map((d) => (
<List.Item
key={d.name}
title={d.name}
subtitle={d.desc}
actions={
<ActionPanel>
<Action
title="Delete All Items But This One"
onAction={async () => {
mutate(setTimeout(1000), {
optimisticUpdate: () => {
return [d];
},
});
}}
/>
</ActionPanel>
}
/>
))}
</List.Section>
</List>
);
}
```

## Types

### AsyncState

An object corresponding to the execution state of the function.

```ts
// Initial State
{
isLoading: true, // or `false` if `options.execute` is `false`
data: undefined,
error: undefined
}

// Success State
{
isLoading: false,
data: T,
error: undefined
}

// Error State
{
isLoading: false,
data: undefined,
error: Error
}

// Reloading State
{
isLoading: true,
data: T | undefined,
error: Error | undefined
}
```
74 changes: 71 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading