11import { UseQueryResult } from "@tanstack/solid-query" ;
22import {
33 Accessor ,
4+ createEffect ,
45 createMemo ,
56 ErrorBoundary ,
67 JSXElement ,
@@ -26,8 +27,7 @@ type Collection<T> = Accessor<T> & {
2627 isError : boolean ;
2728} ;
2829
29- type QueryMapping = Record < string , unknown > | unknown ;
30- type AsyncMap < T extends QueryMapping > = {
30+ type AsyncMap < T extends Record < string , unknown > > = {
3131 [ K in keyof T ] : AsyncEntry < T [ K ] > ;
3232} ;
3333
@@ -38,69 +38,59 @@ type BaseProps = {
3838 errorClass ?: string ;
3939} ;
4040
41- type QueryProps < T extends QueryMapping > = {
41+ type QueryProps < T extends Record < string , unknown > > = {
4242 queries : { [ K in keyof T ] : UseQueryResult < T [ K ] > } ;
4343} ;
4444
45- type SingleQueryProps < T > = {
46- query : UseQueryResult < T > ;
47- } ;
48-
49- type CollectionProps < T extends QueryMapping > = {
45+ type CollectionProps < T extends Record < string , unknown > > = {
5046 collections : { [ K in keyof T ] : Collection < T [ K ] > } ;
5147} ;
5248
53- type SingleCollectionProps < T > = {
54- collection : Collection < T > ;
55- } ;
49+ type AccessorMap < T > = { [ K in keyof T ] : Accessor < T [ K ] > } ;
50+ type DataKeys < T > = { [ K in keyof T as `${K & string } Data`] : T [ K ] } ;
5651
57- type DeferredChildren < T extends QueryMapping > = {
52+ type Source < T extends Record < string , unknown > > =
53+ | QueryProps < T >
54+ | CollectionProps < T > ;
55+
56+ type DeferredChildren < T extends Record < string , unknown > > = {
5857 alwaysShowContent ?: false ;
59- children : ( data : { [ K in keyof T ] : T [ K ] } ) => JSXElement ;
58+ children : (
59+ data : AccessorMap < DataKeys < { [ K in keyof T ] : T [ K ] } > > ,
60+ ) => JSXElement ;
6061} ;
6162
62- type EagerChildren < T extends QueryMapping > = {
63+ type EagerChildren < T extends Record < string , unknown > > = {
6364 alwaysShowContent : true ;
6465 showLoader ?: true ;
65- children : ( data : { [ K in keyof T ] : T [ K ] | undefined } ) => JSXElement ;
66+ children : (
67+ data : AccessorMap < DataKeys < { [ K in keyof T ] : T [ K ] | undefined } > > ,
68+ ) => JSXElement ;
6669} ;
6770
68- export type Props < T extends QueryMapping > = BaseProps &
69- (
70- | QueryProps < T >
71- | SingleQueryProps < T >
72- | CollectionProps < T >
73- | SingleCollectionProps < T >
74- ) &
75- ( DeferredChildren < T > | EagerChildren < T > ) ;
71+ type Children < T extends Record < string , unknown > > =
72+ | DeferredChildren < T >
73+ | EagerChildren < T > ;
74+
75+ export type Props < T extends Record < string , unknown > > = BaseProps &
76+ Source < T > &
77+ Children < T > ;
7678
77- export default function AsyncContent < T extends QueryMapping > (
79+ function AsyncContent < T extends Record < string , unknown > > (
7880 props : Props < T > ,
7981) : JSXElement {
80- //@ts -expect-error this is fine
8182 const source = createMemo < AsyncMap < T > > ( ( ) => {
82- if ( "query" in props ) {
83- return fromQueries ( { defaultQuery : props . query } ) ;
84- } else if ( "queries" in props ) {
83+ if ( "queries" in props ) {
8584 return fromQueries ( props . queries ) ;
86- } else if ( "collection" in props ) {
87- return fromCollections ( { defaultQuery : props . collection } ) ;
88- } else if ( "collections" in props ) {
85+ } else {
8986 return fromCollections ( props . collections ) ;
9087 }
9188 } ) ;
9289
93- const value = ( ) : T => {
94- if ( "defaultQuery" in source ( ) ) {
95- //@ts -expect-error we know the property is present
96- // oxlint-disable-next-line typescript/no-unsafe-call typescript/no-unsafe-member-access
97- return source ( ) . defaultQuery . value ( ) as T ;
98- } else {
99- return Object . fromEntries (
100- typedKeys ( source ( ) ) . map ( ( key ) => [ key , source ( ) [ key ] . value ( ) ] ) ,
101- ) as T ; // For multiple queries
102- }
103- } ;
90+ const value = ( ) : T =>
91+ Object . fromEntries (
92+ typedKeys ( source ( ) ) . map ( ( key ) => [ key , source ( ) [ key ] . value ( ) ] ) ,
93+ ) as T ;
10494
10595 const handleError = ( err : unknown ) : string => {
10696 const message = createErrorMessage (
@@ -119,12 +109,9 @@ export default function AsyncContent<T extends QueryMapping>(
119109 const allResolved = (
120110 data : ReturnType < typeof value > ,
121111 ) : data is { [ K in keyof T ] : T [ K ] } => {
122- //single query
123112 if ( data === undefined || data === null ) {
124113 return false ;
125114 }
126- if ( "defaultQuery" in source ( ) ) return true ;
127-
128115 return Object . values ( data ) . every ( ( v ) => v !== undefined && v !== null ) ;
129116 } ;
130117
@@ -136,6 +123,52 @@ export default function AsyncContent<T extends QueryMapping>(
136123 . find ( ( s ) => s . isError ( ) )
137124 ?. error ?.( ) ;
138125
126+ // Keep the last resolved value so deferred children stay mounted during
127+ // transient loading states (e.g. navigating away and back).
128+ const lastResolvedValue = createMemo < T | undefined > ( ( prev ) => {
129+ const current = value ( ) ;
130+ return allResolved ( current ) ? current : prev ;
131+ } ) ;
132+
133+ const hasResolved = createMemo < boolean > (
134+ ( prev ) => prev || lastResolvedValue ( ) !== undefined ,
135+ false ,
136+ ) ;
137+
138+ // Keys are stable for the component lifetime; per-key closures track
139+ // reactivity internally via value()/lastResolvedValue().
140+ // oxlint-disable-next-line solid/reactivity -- intentional snapshot of initial keys
141+ const keys = typedKeys ( source ( ) ) ;
142+ if ( import . meta. env . DEV ) {
143+ createEffect ( ( ) => {
144+ const currentKeys = typedKeys ( source ( ) ) ;
145+ if (
146+ currentKeys . length !== keys . length ||
147+ currentKeys . some ( ( k , i ) => k !== keys [ i ] )
148+ ) {
149+ console . warn (
150+ "AsyncContent: query keys changed between renders. This is not supported." ,
151+ ) ;
152+ }
153+ } ) ;
154+ }
155+
156+ // oxlint-disable solid/reactivity
157+ const eagerAccessorMap = Object . fromEntries (
158+ typedKeys ( source ( ) ) . map ( ( key ) => [
159+ `${ String ( key ) } Data` ,
160+ ( ) => value ( ) ?. [ key ] ,
161+ ] ) ,
162+ ) as unknown as AccessorMap < DataKeys < { [ K in keyof T ] : T [ K ] | undefined } > > ;
163+
164+ const deferredAccessorMap = Object . fromEntries (
165+ typedKeys ( source ( ) ) . map ( ( key ) => [
166+ `${ String ( key ) } Data` ,
167+ ( ) => lastResolvedValue ( ) ?. [ key ] ,
168+ ] ) ,
169+ ) as unknown as AccessorMap < DataKeys < { [ K in keyof T ] : T [ K ] } > > ;
170+ // oxlint-enable solid/reactivity
171+
139172 const loader = ( ) : JSXElement =>
140173 props . loader ?? < LoadingCircle class = "p-4 text-center text-2xl" /> ;
141174
@@ -144,24 +177,31 @@ export default function AsyncContent<T extends QueryMapping>(
144177 < div class = { props . errorClass } > { handleError ( err ) } </ div >
145178 ) ;
146179
180+ // Show loader on initial load or when the query key changed (no cached data)
181+ const showLoader = ( ) : boolean =>
182+ isLoading ( ) && ! props . alwaysShowContent && ! allResolved ( value ( ) ) ;
183+
147184 return (
148185 < ErrorBoundary fallback = { props . ignoreError ? undefined : errorText } >
149186 < Switch
150187 fallback = {
151188 < >
152- < Show when = { isLoading ( ) && ! props . alwaysShowContent } >
153- { loader ( ) }
154- </ Show >
155-
189+ < Show when = { showLoader ( ) } > { loader ( ) } </ Show >
156190 < Show
157191 when = { props . alwaysShowContent === true }
158192 fallback = {
159- < Show when = { allResolved ( value ( ) ) } >
160- { props . children ( value ( ) ) }
193+ < Show when = { hasResolved ( ) } >
194+ { ( _ ) =>
195+ // oxlint-disable-next-line typescript/no-explicit-any
196+ ( props . children as ( data : any ) => JSXElement ) (
197+ deferredAccessorMap ,
198+ )
199+ }
161200 </ Show >
162201 }
163202 >
164- { props . children ( value ( ) ) }
203+ { /* oxlint-disable-next-line typescript/no-explicit-any */ }
204+ { ( props . children as ( data : any ) => JSXElement ) ( eagerAccessorMap ) }
165205 </ Show >
166206 </ >
167207 }
@@ -170,7 +210,7 @@ export default function AsyncContent<T extends QueryMapping>(
170210 { errorText ( firstError ( ) ) }
171211 </ Match >
172212
173- < Match when = { isLoading ( ) && ! props . alwaysShowContent } > { loader ( ) } </ Match >
213+ < Match when = { showLoader ( ) } > { loader ( ) } </ Match >
174214 </ Switch >
175215 </ ErrorBoundary >
176216 ) ;
@@ -204,3 +244,5 @@ function fromCollections<T extends Record<string, unknown>>(collections: {
204244 return acc ;
205245 } , { } as AsyncMap < T > ) ;
206246}
247+
248+ export default AsyncContent ;
0 commit comments