Skip to content

Commit

Permalink
feat!: version 1
Browse files Browse the repository at this point in the history
  • Loading branch information
cprecioso committed Apr 14, 2023
1 parent 7067c65 commit 561d3bc
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 82 deletions.
99 changes: 54 additions & 45 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,66 +5,75 @@ $ npm i -D @cprecioso/react-suspense # if you use npm
$ yarn add --dev @cprecioso/react-suspense # if you use yarn
```

## `createSingleValueSuspense`
## `use` functions

```ts
const createSingleValueSuspense: <T>(
getClientValue: () => Promise<T>,
getServerValue?: () => Promise<T>
) => () => T;
```

Creates a hook to get a single value, suspending the tree. It only works on the
client unless manually specified.

> The `getServerValue` argument has the same restrictions as the second argument
> for
> [the `useSyncExternalStore` hook](https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering),
> especially the requirement of it returning the same value on client and
> server.
This library returns some functions named `use`. This is to keep consistency
with
[the proposed `use` function from React](https://github.com/reactjs/rfcs/pull/229).
Same as that proposal, `use` can be called from inside a component or a hook,
and inside conditionals or loops, but not from other kinds of functions such as
`useEffect` or code outside of a React tree.

### Example
## Suspenses

#### Only client-side
### `createSingleValueSuspense`

```tsx
```jsx
import { createSingleValueSuspense } from "@cprecioso/react-suspense";

const useAppConfig = createSingleValueSuspense(
// A `Promise`-returning function with the value you want to pass to your application
async () => (await fetch("/api/config")).json()
const { use: useAppConfig } = createSingleValueSuspense(async () =>
(await fetch("/api/config")).json()
);

export const MyComponent = () => {
export const Greeting = () => {
const { accentColor } = useAppConfig();

return (
<div style={{ backgroundColor: accentColor }}>
<h1>Hello world!</h1>
</div>
);
return <h1 style={{ color: accentColor }}>Hello world</h1>;
};
```

#### Client- and server-side
Pass it an async function, returns an object with:

```tsx
import { createSingleValueSuspense } from "@cprecioso/react-suspense";
- `use()`: call it to suspend your tree while the async function resolves.

const useAppConfig = createSingleValueSuspense(
async () => (await fetch("/api/config")).json(),
// For example, here we use a dummy value for the inital server-side rendering,
// but we could do anything, like calling another API.
async () => ({ accentColor: "black" })
);
- `cache`: an object that provides a `get`/`set` function to manually manipulate
the cache. Useful to call `cache.set(null)` and force re-fetching.

export const MyComponent = () => {
const { accentColor } = useAppConfig();
### `createKeyedSuspense`

```jsx
import { createKeyedSuspense } from "@cprecioso/react-suspense";

const { use: useUserInfo } = createKeyedSuspense(async (userId) =>
(await fetch(`/api/user/${userId}`)).json()
);

return (
<div style={{ backgroundColor: accentColor }}>
<h1>Hello world!</h1>
</div>
);
export const UserInfo = ({ userId }) => {
const { name } = useUserInfo(userId);
return <p>Name: {name}</p>;
};
```

Pass it an async function, returns an object with:

- `use(key)`: call it to suspend your tree while the async function resolves.

- `cache`: an object that provides a `get`/`set` function to manually manipulate
the cache. Useful to call `cache.set(key, null)` and force re-fetching.

## Caches

### `createSingleValueCache`

### `createKeyedCache`

Same as their `createXSuspense` counterparts, but the async function is not
passed when creating the cache, but when calling `use`: `use(fn)` /
`use(key, fn)`.

### `createSingleValueCacheWithStorage`

### `createKeyedCacheWithStorage`

Same as their `createXCache` counterparts, but you must provide an storage
object for the promise cache to be stored in. It's just an object with `get` and
`set` methods.
45 changes: 37 additions & 8 deletions src/cache/keyed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,43 @@ import { suspendOnPromise } from "../lib/suspend";

export interface KeyedCacheStorage<K, T> {
get(key: K): CacheValue<T> | undefined | null;
set(key: K, value: CacheValue<T>): void;
set(key: K, value: CacheValue<T> | null): void;
}

export const createKeyedCache = <K, T>(
storage: KeyedCacheStorage<K, T> = new Map()
) => {
const use = (key: K, fn: () => Promise<T>) =>
suspendOnPromise(fn, storage.get(key), (value) => storage.set(key, value));
export declare namespace KeyedCacheStorage {
export type Any = KeyedCacheStorage<any, any>;

return { use, storage };
};
export type KeyType<S extends Any> = S extends KeyedCacheStorage<infer K, any>
? K
: never;

export type ValueType<S extends Any> = S extends KeyedCacheStorage<
any,
infer T
>
? T
: never;
}

export type KeyedUseFn<K, T> = (key: K, fn: () => Promise<T>) => T;

export interface KeyedCache<Storage extends KeyedCacheStorage.Any> {
storage: Storage;
use: KeyedUseFn<
KeyedCacheStorage.KeyType<Storage>,
KeyedCacheStorage.ValueType<Storage>
>;
}

export const createKeyedCacheWithStorage = <
Storage extends KeyedCacheStorage.Any
>(
storage: Storage
): KeyedCache<Storage> => ({
storage,
use: (key, fn) =>
suspendOnPromise(fn, storage.get(key), (value) => storage.set(key, value)),
});

export const createKeyedCache = <K, T>() =>
createKeyedCacheWithStorage(new Map<K, CacheValue<T>>());
50 changes: 41 additions & 9 deletions src/cache/single-value.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
import { CacheValue } from "../lib/cache-ref";
import { suspendOnPromise } from "../lib/suspend";

export const createSingleValueCache = <T>() => {
const storage = {
current: null as CacheValue<T> | null,
};
export interface SingleValueCacheStorage<T> {
get(): CacheValue<T> | undefined | null;
set(value: CacheValue<T> | null): void;
}

export declare namespace SingleValueCacheStorage {
export type Any = SingleValueCacheStorage<any>;

export type ValueType<S extends Any> = S extends SingleValueCacheStorage<
infer T
>
? T
: never;
}

export type SingleValueUseFn<T> = (fn: () => Promise<T>) => T;

const use = (fn: () => Promise<T>) =>
suspendOnPromise(fn, storage.current, (value) => {
storage.current = value;
});
export interface SingleValueCache<Storage extends SingleValueCacheStorage.Any> {
storage: Storage;
use: SingleValueUseFn<SingleValueCacheStorage.ValueType<Storage>>;
}

return { use, storage };
export const createSingleValueCacheWithStorage = <
Storage extends SingleValueCacheStorage.Any
>(
storage: Storage
): SingleValueCache<Storage> => ({
storage,
use: (fn) => suspendOnPromise(fn, storage.get(), storage.set),
});

const makeSimpleStorage = <T>(): SingleValueCacheStorage<T> => {
let storage: CacheValue<T> | null;

return {
get: () => storage,
set: (value) => {
storage = value;
},
};
};

export const createSingleValueCache = <T>() =>
createSingleValueCacheWithStorage(makeSimpleStorage<T>());
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { createKeyedCache } from "./cache/keyed";
export { createSingleValueCache } from "./cache/single-value";
export { createKeyedSuspense } from "./suspense/keyed";
export { createSingleValueSuspense } from "./suspense/single-value";
export * from "./cache/keyed";
export * from "./cache/single-value";
export * from "./suspense/keyed";
export * from "./suspense/single-value";
11 changes: 3 additions & 8 deletions src/suspense/keyed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ import { createKeyedCache } from "../cache/keyed";
export const createKeyedSuspense = <K extends string, T>(
fn: (key: K) => Promise<T>
) => {
const cache = createKeyedCache<K, T>();

const useKeyedSuspense = (key: K) => {
const value = cache.use(key, () => fn(key));
return { value, cache };
};

return useKeyedSuspense;
const { storage, use } = createKeyedCache<K, T>();
const useKeyedSuspense = (key: K) => use(key, () => fn(key));
return { use: useKeyedSuspense, storage };
};
11 changes: 3 additions & 8 deletions src/suspense/single-value.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { createSingleValueCache } from "../cache/single-value";

export const createSingleValueSuspense = <T>(fn: () => Promise<T>) => {
const cache = createSingleValueCache<T>();

const useSingleValueSuspense = () => {
const value = cache.use(fn);
return { value, cache };
};

return useSingleValueSuspense;
const { storage, use } = createSingleValueCache<T>();
const useSingleValueSuspense = () => use(fn);
return { use: useSingleValueSuspense, storage };
};

0 comments on commit 561d3bc

Please sign in to comment.