S
SolidJS4mo ago
hannus

SolidStart SSR: Why does createAsync data work in JSX but not in a child component?

当然,这是你这段问题的英文翻译,适合用来在 SolidStart 或相关社区中提问: ⸻ In a SolidStart project using SSR, I encountered an issue with a route component that uses createAsync to fetch an async resource. I’ve set deferStream: true, and here’s the problem: When this route is rendered, some parts of the page correctly display the async data, but some parts (particularly in components) do not render the data properly until I manually refresh the page. Here’s a simplified version of my component:
export const route = {
preload() {
getUserProfileQuery();
},
} satisfies RouteDefinition;

export default function Profile() {
const userProfile = createAsync(() => getUserProfileQuery(), {
deferStream: true,
});

return (
<div class="min-h-screen py-12 px-4 sm:px-6 lg:px-8">
<p>{userProfile()?.data?.nickname}</p>
<p>{userProfile()?.data?.date_of_birth?.toString()}</p>
<p>{userProfile()?.data?.height_cm}</p>
<p>{userProfile()?.data?.gender}</p>
{/* The above JSX renders data correctly */}

<NumberField
step={0.1}
defaultValue={userProfile()?.data?.height_cm}
>
<NumberFieldLabel>Height</NumberFieldLabel>
<NumberFieldGroup>
<NumberFieldInput name="height_cm" />
<NumberFieldIncrementTrigger />
<NumberFieldDecrementTrigger />
</NumberFieldGroup>
</NumberField>
{/* This component doesn't work properly until I refresh the page */}
</div>
);
}
export const route = {
preload() {
getUserProfileQuery();
},
} satisfies RouteDefinition;

export default function Profile() {
const userProfile = createAsync(() => getUserProfileQuery(), {
deferStream: true,
});

return (
<div class="min-h-screen py-12 px-4 sm:px-6 lg:px-8">
<p>{userProfile()?.data?.nickname}</p>
<p>{userProfile()?.data?.date_of_birth?.toString()}</p>
<p>{userProfile()?.data?.height_cm}</p>
<p>{userProfile()?.data?.gender}</p>
{/* The above JSX renders data correctly */}

<NumberField
step={0.1}
defaultValue={userProfile()?.data?.height_cm}
>
<NumberFieldLabel>Height</NumberFieldLabel>
<NumberFieldGroup>
<NumberFieldInput name="height_cm" />
<NumberFieldIncrementTrigger />
<NumberFieldDecrementTrigger />
</NumberFieldGroup>
</NumberField>
{/* This component doesn't work properly until I refresh the page */}
</div>
);
}
The NumberField component comes from solid-ui, which is built on top of Kobalte. It seems like during client-side hydration, this component doesn’t wait for the async resource to resolve, even though the plain JSX above does. Why does the top part render correctly after the async resource resolves, but NumberField fails unless I refresh the page? Any idea how to ensure proper hydration or data availability across all components? thanks
4 Replies
Madaxen86
Madaxen864mo ago
You‘ll probably just need to wrap the JSX in Suspense.
hannus
hannusOP4mo ago
✅ Update: I solved it! Thanks to some digging and experimentation, I figured out the cause and wanted to share the solution here in case anyone else runs into the same issue. 🔍 Root Cause The issue was related to how createAsync(..., { deferStream: true }) works in combination with components that read props like defaultValue only once during initial mount. In my case, NumberField from solid-ui was using defaultValue={userProfile()?.data?.height_cm}. But since the data wasn’t available immediately (due to deferred streaming), the component initialized with undefined and never updated later — unless I refreshed the page (which reloaded with the data already present). ✅ Solution I fixed it by conditionally rendering the component only after the async data was available, like this:
{userProfile()?.data && (
<NumberField defaultValue={userProfile()!.data.height_cm}>
...
</NumberField>
)}
{userProfile()?.data && (
<NumberField defaultValue={userProfile()!.data.height_cm}>
...
</NumberField>
)}
This ensures the component mounts only when the data is ready, so it initializes with the correct value. Alternatively, if the component supports a controlled mode, you can also use:
<NumberField value={userProfile()?.data?.height_cm}>
...
</NumberField>
<NumberField value={userProfile()?.data?.height_cm}>
...
</NumberField>
📌 Summary • JSX bindings like <p>{userProfile()?.data?.nickname}</p> are reactive and work fine. • Some components “freeze” their defaultValue on mount — before the async resource resolves. • Workaround: delay rendering, or use value for reactive behavior. Hope this helps future readers! Let me know if you’ve faced something similar or have better workarounds 🙌
Madaxen86
Madaxen864mo ago
yes, that's a common crux on working with anything that uses primitives with default values and async data, because primitives are not reactive context. e.g.
const getData = async () => "John"
function Parent() {
const data = createAsync(()=>getData());
return (
<Suspense>
<Child data={data()} />
</Suspense>
}

function Child(props) {
console.log(props.data) //will log undefined as async is not resolved when the component is initialized
const [value,setValue] = createSignal(props.data); //this is not reactive so it'll be undefined
return <>
<p>This will show "John" because of Supsense in parent {props.data}</p>
{/* input will be undefined */}
<input name="name" value={value()} onInput={(e) => setValue(e.target.value) } />
</>
}
const getData = async () => "John"
function Parent() {
const data = createAsync(()=>getData());
return (
<Suspense>
<Child data={data()} />
</Suspense>
}

function Child(props) {
console.log(props.data) //will log undefined as async is not resolved when the component is initialized
const [value,setValue] = createSignal(props.data); //this is not reactive so it'll be undefined
return <>
<p>This will show "John" because of Supsense in parent {props.data}</p>
{/* input will be undefined */}
<input name="name" value={value()} onInput={(e) => setValue(e.target.value) } />
</>
}
In solid 2.0 we will be able to pass an accessor to createSignal which will update when the accessor updates which will help in these cases:
//in 2.0 pass a accessor / getter
const [value,setValue] = createSignal(() => props.data); //will update the signal
//in 2.0 pass a accessor / getter
const [value,setValue] = createSignal(() => props.data); //will update the signal
hannus
hannusOP4mo ago
thanks. I am looking forward to experiencing solid2.0

Did you find this page helpful?