Skip to content

Commit 8c402ef

Browse files
authored
Merge pull request #490 from solidjs/query
rename `cache` to `query`, action `onComplete`
2 parents d6c52cf + 663b3c1 commit 8c402ef

File tree

7 files changed

+97
-49
lines changed

7 files changed

+97
-49
lines changed

.changeset/happy-actors-pretend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/router": patch
3+
---
4+
5+
fix passing result into onComplete

.changeset/perfect-pears-look.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/router": minor
3+
---
4+
5+
rename `cache` to `query`, action `onComplete`

README.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -427,30 +427,30 @@ The return value of the `preload` function is passed to the page component when
427427

428428
Keep in mind these are completely optional. To use but showcase the power of our preload mechanism.
429429

430-
### `cache`
430+
### `query`
431431

432-
To prevent duplicate fetching and to trigger handle refetching we provide a cache api. That takes a function and returns the same function.
432+
To prevent duplicate fetching and to trigger handle refetching we provide a query api. That takes a function and returns the same function.
433433

434434
```jsx
435-
const getUser = cache(async (id) => {
435+
const getUser = query(async (id) => {
436436
return (await fetch(`/api/users${id}`)).json()
437-
}, "users") // used as cache key + serialized arguments
437+
}, "users") // used as the query key + serialized arguments
438438
```
439-
It is expected that the arguments to the cache function are serializable.
439+
It is expected that the arguments to the query function are serializable.
440440

441-
This cache accomplishes the following:
441+
This query accomplishes the following:
442442

443-
1. It does just deduping on the server for the lifetime of the request.
444-
2. It does preload cache in the browser which lasts 5 seconds. When a route is preloaded on hover or when preload is called when entering a route it will make sure to dedupe calls.
443+
1. It does deduping on the server for the lifetime of the request.
444+
2. It fills a preload cache in the browser which lasts 5 seconds. When a route is preloaded on hover or when preload is called when entering a route it will make sure to dedupe calls.
445445
3. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation.
446-
4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses it. Revalidation or new fetch updates the cache.
446+
4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses this cache. Revalidation or new fetch updates the cache.
447447

448448
Using it with preload function might look like:
449449

450450
```js
451451
import { lazy } from "solid-js";
452452
import { Route } from "@solidjs/router";
453-
import { getUser } from ... // the cache function
453+
import { getUser } from ... // the query function
454454

455455
const User = lazy(() => import("./pages/users/[id].js"));
456456

@@ -467,7 +467,7 @@ Inside your page component you:
467467

468468
```jsx
469469
// pages/users/[id].js
470-
import { getUser } from ... // the cache function
470+
import { getUser } from ... // the query function
471471

472472
export default function User(props) {
473473
const user = createAsync(() => getUser(props.params.id));
@@ -483,9 +483,9 @@ getUser.key // returns "users"
483483
getUser.keyFor(id) // returns "users[5]"
484484
```
485485

486-
You can revalidate the cache using the `revalidate` method or you can set `revalidate` keys on your response from your actions. If you pass the whole key it will invalidate all the entries for the cache (ie "users" in the example above). You can also invalidate a single entry by using `keyFor`.
486+
You can revalidate the query using the `revalidate` method or you can set `revalidate` keys on your response from your actions. If you pass the whole key it will invalidate all the entries for the query (ie "users" in the example above). You can also invalidate a single entry by using `keyFor`.
487487

488-
`cache` can be defined anywhere and then used inside your components with:
488+
`query` can be defined anywhere and then used inside your components with:
489489

490490
### `createAsync`
491491

@@ -502,7 +502,7 @@ const user = createAsync((currentValue) => getUser(params.id))
502502
return <h1>{user.latest.name}</h1>;
503503
```
504504

505-
Using `cache` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly.
505+
Using `query` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly.
506506

507507
### `createAsyncStore`
508508

@@ -597,13 +597,13 @@ const submission = useSubmission(action, (input) => filter(input));
597597

598598
### Response Helpers
599599

600-
These are used to communicate router navigations from cache/actions, and can include invalidation hints. Generally these are thrown to not interfere the with the types and make it clear that function ends execution at that point.
600+
These are used to communicate router navigations from query/actions, and can include invalidation hints. Generally these are thrown to not interfere the with the types and make it clear that function ends execution at that point.
601601

602602
#### `redirect(path, options)`
603603

604604
Redirects to the next route
605605
```js
606-
const getUser = cache(() => {
606+
const getUser = query(() => {
607607
const user = await api.getCurrentUser()
608608
if (!user) throw redirect("/login");
609609
return user;
@@ -614,7 +614,7 @@ const getUser = cache(() => {
614614

615615
Reloads the data on the current page
616616
```js
617-
const getTodo = cache(async (id: number) => {
617+
const getTodo = query(async (id: number) => {
618618
const todo = await fetchTodo(id);
619619
return todo;
620620
}, "todo")
@@ -937,7 +937,7 @@ Related without Outlet component it has to be passed in manually. At which point
937937

938938
### `data` functions & `useRouteData`
939939

940-
These have been replaced by a preload mechanism. This allows link hover preloads (as the preload function can be run as much as wanted without worry about reactivity). It support deduping/cache APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks.
940+
These have been replaced by a preload mechanism. This allows link hover preloads (as the preload function can be run as much as wanted without worry about reactivity). It support deduping/query APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks.
941941

942942
That being said you can reproduce the old pattern largely by turning off preloads at the router level and then injecting your own Context:
943943

src/data/action.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { $TRACK, createMemo, createSignal, JSX, onCleanup, getOwner } from "solid-js";
22
import { isServer } from "solid-js/web";
33
import { useRouter } from "../routing.js";
4-
import type { RouterContext, Submission, SubmissionStub, Navigator, NarrowResponse } from "../types.js";
4+
import type {
5+
RouterContext,
6+
Submission,
7+
SubmissionStub,
8+
Navigator,
9+
NarrowResponse
10+
} from "../types.js";
511
import { mockBase } from "../utils.js";
6-
import { cacheKeyOp, hashKey, revalidate, cache } from "./cache.js";
12+
import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
713

814
export type Action<T extends Array<any>, U, V = T> = (T extends [FormData] | []
915
? JSX.SerializableAttributeValue
@@ -47,7 +53,8 @@ export function useSubmission<T extends Array<any>, U, V>(
4753
{},
4854
{
4955
get(_, property) {
50-
if (submissions.length === 0 && property === "clear" || property === "retry") return (() => {});
56+
if ((submissions.length === 0 && property === "clear") || property === "retry")
57+
return () => {};
5158
return submissions[submissions.length - 1]?.[property as keyof Submission<T, U>];
5259
}
5360
}
@@ -62,7 +69,15 @@ export function useAction<T extends Array<any>, U, V>(action: Action<T, U, V>) {
6269
export function action<T extends Array<any>, U = void>(
6370
fn: (...args: T) => Promise<U>,
6471
name?: string
65-
): Action<T, U, T> {
72+
): Action<T, U>;
73+
export function action<T extends Array<any>, U = void>(
74+
fn: (...args: T) => Promise<U>,
75+
options?: { name?: string; onComplete?: (s: Submission<T, U>) => boolean }
76+
): Action<T, U>;
77+
export function action<T extends Array<any>, U = void>(
78+
fn: (...args: T) => Promise<U>,
79+
options: string | { name?: string; onComplete?: (s: Submission<T, U>) => boolean } = {}
80+
): Action<T, U> {
6681
function mutate(this: { r: RouterContext; f?: HTMLFormElement }, ...variables: T) {
6782
const router = this.r;
6883
const form = this.f;
@@ -76,6 +91,17 @@ export function action<T extends Array<any>, U = void>(
7691
function handler(error?: boolean) {
7792
return async (res: any) => {
7893
const result = await handleResponse(res, error, router.navigatorFactory());
94+
let retry = null;
95+
!o.onComplete?.({
96+
...submission,
97+
result: result?.data,
98+
error: result?.error,
99+
pending: false,
100+
retry() {
101+
return retry = submission.retry();
102+
}
103+
});
104+
if (retry) return retry;
79105
if (!result) return submission.clear();
80106
setResult(result);
81107
if (result.error && !form) throw result.error;
@@ -97,7 +123,7 @@ export function action<T extends Array<any>, U = void>(
97123
return !result();
98124
},
99125
clear() {
100-
router.submissions[1](v => v.filter(i => i.input !== variables));
126+
router.submissions[1](v => v.filter(i => i !== submission));
101127
},
102128
retry() {
103129
setResult(undefined);
@@ -108,10 +134,10 @@ export function action<T extends Array<any>, U = void>(
108134
]);
109135
return p.then(handler(), handler(true));
110136
}
111-
137+
const o = typeof options === "string" ? { name: options } : options;
112138
const url: string =
113139
(fn as any).url ||
114-
(name && `https://action/${name}`) ||
140+
(o.name && `https://action/${o.name}`) ||
115141
(!isServer ? `https://action/${hashString(fn.toString())}` : "");
116142
mutate.base = url;
117143
return toAction(mutate, url);
@@ -177,7 +203,7 @@ async function handleResponse(response: unknown, error: boolean | undefined, nav
177203
// invalidate
178204
cacheKeyOp(keys, entry => (entry[0] = 0));
179205
// set cache
180-
flightKeys && flightKeys.forEach(k => cache.set(k, custom[k]));
206+
flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
181207
// trigger revalidation
182208
await revalidate(keys, false);
183209
return data != null ? { data } : undefined;

src/data/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { createAsync, createAsyncStore, type AccessorWithLatest } from "./createAsync.js";
22
export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js";
3-
export { cache, revalidate, type CachedFunction } from "./cache.js";
3+
export { query, revalidate, cache, type CachedFunction } from "./query.js";
44
export { redirect, reload, json } from "./response.js";
55

src/data/cache.ts renamed to src/data/query.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ if (!isServer) {
2121
setInterval(() => {
2222
const now = Date.now();
2323
for (let [k, v] of cacheMap.entries()) {
24-
if (!v[3].count && now - v[0] > CACHE_TIMEOUT) {
24+
if (!v[4].count && now - v[0] > CACHE_TIMEOUT) {
2525
cacheMap.delete(k);
2626
}
2727
}
@@ -40,7 +40,7 @@ export function revalidate(key?: string | string[] | void, force = true) {
4040
const now = Date.now();
4141
cacheKeyOp(key, entry => {
4242
force && (entry[0] = 0); //force cache miss
43-
entry[3][1](now); // retrigger live signals
43+
entry[4][1](now); // retrigger live signals
4444
});
4545
});
4646
}
@@ -67,7 +67,7 @@ export type CachedFunction<T extends (...args: any) => any> = T extends (
6767
}
6868
: never;
6969

70-
export function cache<T extends (...args: any) => any>(fn: T, name: string): CachedFunction<T> {
70+
export function query<T extends (...args: any) => any>(fn: T, name: string): CachedFunction<T> {
7171
// prioritize GET for server functions
7272
if ((fn as any).GET) fn = (fn as any).GET;
7373
const cachedFn = ((...args: Parameters<T>) => {
@@ -96,22 +96,22 @@ export function cache<T extends (...args: any) => any>(fn: T, name: string): Cac
9696
}
9797
if (getListener() && !isServer) {
9898
tracking = true;
99-
onCleanup(() => cached[3].count--);
99+
onCleanup(() => cached[4].count--);
100100
}
101101

102102
if (
103103
cached &&
104104
cached[0] &&
105105
(isServer ||
106106
intent === "native" ||
107-
cached[3].count ||
107+
cached[4].count ||
108108
Date.now() - cached[0] < PRELOAD_TIMEOUT)
109109
) {
110110
if (tracking) {
111-
cached[3].count++;
112-
cached[3][0](); // track
111+
cached[4].count++;
112+
cached[4][0](); // track
113113
}
114-
if (cached[2] === "preload" && intent !== "preload") {
114+
if (cached[3] === "preload" && intent !== "preload") {
115115
cached[0] = now;
116116
}
117117
let res = cached[1];
@@ -120,7 +120,7 @@ export function cache<T extends (...args: any) => any>(fn: T, name: string): Cac
120120
"then" in cached[1]
121121
? cached[1].then(handleResponse(false), handleResponse(true))
122122
: handleResponse(false)(cached[1]);
123-
!isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
123+
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
124124
}
125125
inPreloadFn && "then" in res && res.catch(() => {});
126126
return res;
@@ -133,18 +133,18 @@ export function cache<T extends (...args: any) => any>(fn: T, name: string): Cac
133133
if (cached) {
134134
cached[0] = now;
135135
cached[1] = res;
136-
cached[2] = intent;
137-
!isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
136+
cached[3] = intent;
137+
!isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
138138
} else {
139139
cache.set(
140140
key,
141-
(cached = [now, res, intent, createSignal(now) as Signal<number> & { count: number }])
141+
(cached = [now, res, , intent, createSignal(now) as Signal<number> & { count: number }])
142142
);
143-
cached[3].count = 0;
143+
cached[4].count = 0;
144144
}
145145
if (tracking) {
146-
cached[3].count++;
147-
cached[3][0](); // track
146+
cached[4].count++;
147+
cached[4][0](); // track
148148
}
149149
if (isServer) {
150150
const e = getRequestEvent();
@@ -192,6 +192,7 @@ export function cache<T extends (...args: any) => any>(fn: T, name: string): Cac
192192
if ((v as any).customBody) v = await (v as any).customBody();
193193
}
194194
if (error) throw v;
195+
cached[2] = v;
195196
return v;
196197
};
197198
}
@@ -201,24 +202,35 @@ export function cache<T extends (...args: any) => any>(fn: T, name: string): Cac
201202
return cachedFn;
202203
}
203204

204-
cache.set = (key: string, value: any) => {
205+
query.get = (key: string) => {
206+
const cached = getCache().get(key) as CacheEntry;
207+
return cached[2];
208+
}
209+
210+
query.set = <T>(key: string, value: T extends Promise<any> ? never : T) => {
205211
const cache = getCache();
206212
const now = Date.now();
207213
let cached = cache.get(key);
208214
if (cached) {
209215
cached[0] = now;
210-
cached[1] = value;
211-
cached[2] = "preload";
216+
cached[1] = Promise.resolve(value);
217+
cached[2] = value;
218+
cached[3] = "preload";
212219
} else {
213220
cache.set(
214221
key,
215-
(cached = [now, value, , createSignal(now) as Signal<number> & { count: number }])
222+
(cached = [now, Promise.resolve(value), value, "preload", createSignal(now) as Signal<number> & { count: number }])
216223
);
217-
cached[3].count = 0;
224+
cached[4].count = 0;
218225
}
219226
};
220227

221-
cache.clear = () => getCache().clear();
228+
query.delete = (key: string) => getCache().delete(key);
229+
230+
query.clear = () => getCache().clear();
231+
232+
/** @deprecated use query instead */
233+
export const cache = query;
222234

223235
function matchKey(key: string, keys: string[]) {
224236
for (let k of keys) {

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export interface MaybePreloadableComponent extends Component {
229229
preload?: () => void;
230230
}
231231

232-
export type CacheEntry = [number, any, Intent | undefined, Signal<number> & { count: number }];
232+
export type CacheEntry = [number, Promise<any>, any, Intent | undefined, Signal<number> & { count: number }];
233233

234234
export type NarrowResponse<T> = T extends CustomResponse<infer U> ? U : Exclude<T, Response>;
235235
export type RouterResponseInit = Omit<ResponseInit, "body"> & { revalidate?: string | string[] };

0 commit comments

Comments
 (0)