From 561d3bcb770dfabf556bae8a27177555931c8655 Mon Sep 17 00:00:00 2001 From: Carlos Precioso Date: Fri, 14 Apr 2023 16:21:05 +0200 Subject: [PATCH] feat!: version 1 --- readme.md | 99 ++++++++++++++++++++---------------- src/cache/keyed.ts | 45 +++++++++++++--- src/cache/single-value.ts | 50 ++++++++++++++---- src/index.ts | 8 +-- src/suspense/keyed.ts | 11 ++-- src/suspense/single-value.ts | 11 ++-- 6 files changed, 142 insertions(+), 82 deletions(-) diff --git a/readme.md b/readme.md index 1d8c506..e017cc9 100644 --- a/readme.md +++ b/readme.md @@ -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: ( - getClientValue: () => Promise, - getServerValue?: () => Promise -) => () => 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 ( -
-

Hello world!

-
- ); + return

Hello world

; }; ``` -#### 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 ( -
-

Hello world!

-
- ); +export const UserInfo = ({ userId }) => { + const { name } = useUserInfo(userId); + return

Name: {name}

; }; ``` + +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. diff --git a/src/cache/keyed.ts b/src/cache/keyed.ts index 092665a..b11c478 100644 --- a/src/cache/keyed.ts +++ b/src/cache/keyed.ts @@ -3,14 +3,43 @@ import { suspendOnPromise } from "../lib/suspend"; export interface KeyedCacheStorage { get(key: K): CacheValue | undefined | null; - set(key: K, value: CacheValue): void; + set(key: K, value: CacheValue | null): void; } -export const createKeyedCache = ( - storage: KeyedCacheStorage = new Map() -) => { - const use = (key: K, fn: () => Promise) => - suspendOnPromise(fn, storage.get(key), (value) => storage.set(key, value)); +export declare namespace KeyedCacheStorage { + export type Any = KeyedCacheStorage; - return { use, storage }; -}; + export type KeyType = S extends KeyedCacheStorage + ? K + : never; + + export type ValueType = S extends KeyedCacheStorage< + any, + infer T + > + ? T + : never; +} + +export type KeyedUseFn = (key: K, fn: () => Promise) => T; + +export interface KeyedCache { + storage: Storage; + use: KeyedUseFn< + KeyedCacheStorage.KeyType, + KeyedCacheStorage.ValueType + >; +} + +export const createKeyedCacheWithStorage = < + Storage extends KeyedCacheStorage.Any +>( + storage: Storage +): KeyedCache => ({ + storage, + use: (key, fn) => + suspendOnPromise(fn, storage.get(key), (value) => storage.set(key, value)), +}); + +export const createKeyedCache = () => + createKeyedCacheWithStorage(new Map>()); diff --git a/src/cache/single-value.ts b/src/cache/single-value.ts index 1951b18..34d115c 100644 --- a/src/cache/single-value.ts +++ b/src/cache/single-value.ts @@ -1,15 +1,47 @@ import { CacheValue } from "../lib/cache-ref"; import { suspendOnPromise } from "../lib/suspend"; -export const createSingleValueCache = () => { - const storage = { - current: null as CacheValue | null, - }; +export interface SingleValueCacheStorage { + get(): CacheValue | undefined | null; + set(value: CacheValue | null): void; +} + +export declare namespace SingleValueCacheStorage { + export type Any = SingleValueCacheStorage; + + export type ValueType = S extends SingleValueCacheStorage< + infer T + > + ? T + : never; +} + +export type SingleValueUseFn = (fn: () => Promise) => T; - const use = (fn: () => Promise) => - suspendOnPromise(fn, storage.current, (value) => { - storage.current = value; - }); +export interface SingleValueCache { + storage: Storage; + use: SingleValueUseFn>; +} - return { use, storage }; +export const createSingleValueCacheWithStorage = < + Storage extends SingleValueCacheStorage.Any +>( + storage: Storage +): SingleValueCache => ({ + storage, + use: (fn) => suspendOnPromise(fn, storage.get(), storage.set), +}); + +const makeSimpleStorage = (): SingleValueCacheStorage => { + let storage: CacheValue | null; + + return { + get: () => storage, + set: (value) => { + storage = value; + }, + }; }; + +export const createSingleValueCache = () => + createSingleValueCacheWithStorage(makeSimpleStorage()); diff --git a/src/index.ts b/src/index.ts index 6f0fbe0..4590f1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; diff --git a/src/suspense/keyed.ts b/src/suspense/keyed.ts index 466e683..1991b84 100644 --- a/src/suspense/keyed.ts +++ b/src/suspense/keyed.ts @@ -3,12 +3,7 @@ import { createKeyedCache } from "../cache/keyed"; export const createKeyedSuspense = ( fn: (key: K) => Promise ) => { - const cache = createKeyedCache(); - - const useKeyedSuspense = (key: K) => { - const value = cache.use(key, () => fn(key)); - return { value, cache }; - }; - - return useKeyedSuspense; + const { storage, use } = createKeyedCache(); + const useKeyedSuspense = (key: K) => use(key, () => fn(key)); + return { use: useKeyedSuspense, storage }; }; diff --git a/src/suspense/single-value.ts b/src/suspense/single-value.ts index e05c204..1f91361 100644 --- a/src/suspense/single-value.ts +++ b/src/suspense/single-value.ts @@ -1,12 +1,7 @@ import { createSingleValueCache } from "../cache/single-value"; export const createSingleValueSuspense = (fn: () => Promise) => { - const cache = createSingleValueCache(); - - const useSingleValueSuspense = () => { - const value = cache.use(fn); - return { value, cache }; - }; - - return useSingleValueSuspense; + const { storage, use } = createSingleValueCache(); + const useSingleValueSuspense = () => use(fn); + return { use: useSingleValueSuspense, storage }; };