Async Context and Store using route param

I'm sure the answer to this will be deceptively simple but I can't figure it out. In my SolidStart project, a user navigates to a "project" by route, e.g. /project/foo. The file is /project[key].tsx and using const params = useParams() can retrieve params.key perfectly. The next step was:
const projectQuery = createAsync(() =>
api.projects.project.query({ key }) // tRPC, works fine
);
const projectQuery = createAsync(() =>
api.projects.project.query({ key }) // tRPC, works fine
);
This works great and I can then use <Suspend> and friends and project data renders when available. BUT as you can imagine, the loaded project data and operations on that data are needed deeply throughout the page. To avoid prop drilling I want to make a Context that provides a Store to provide both the data and the operations. At the top-level this reads fine:
export default function ProjectDetail() {
const params = useParams(); // Keep params for projectKey to Provider
return (
<ProjectProvider key={params.key}>
<PageContentsGoesHere />
</ProjectProvider>
);
}
export default function ProjectDetail() {
const params = useParams(); // Keep params for projectKey to Provider
return (
<ProjectProvider key={params.key}>
<PageContentsGoesHere />
</ProjectProvider>
);
}
That provider is drafted as below... I know it's not right but I don't know how to get the data returned by createAsync to updated or set into the context:
export const ProjectProvider: ParentComponent<{ key: string }> = (props) => {
// make an empty context
const ctx = makeProjectContext()
const projectQuery = createAsync(() =>
// This works but the data needs to get into the store somehow!
api.projects.project.query({ key: props.key })
)
return (
<ProjectContext.Provider value={ctx}>
{props.children}
</ProjectContext.Provider>
)
}
export const ProjectProvider: ParentComponent<{ key: string }> = (props) => {
// make an empty context
const ctx = makeProjectContext()
const projectQuery = createAsync(() =>
// This works but the data needs to get into the store somehow!
api.projects.project.query({ key: props.key })
)
return (
<ProjectContext.Provider value={ctx}>
{props.children}
</ProjectContext.Provider>
)
}
I know that createAsyncStore exists but I don't understand what it returns and how to use it in a context. Any help most appreciated!
8 Replies
ellers
ellersOP2d ago
You can see the WIP version of makeProjectContext below, and my confusion how to go from an empty placeholder store to the real data from the API:
export const makeProjectContext = () => {

// Using placeholder content in here... don't know how to wait for
// the result of createAsync to put data into the store.
const [project, updateProject] = createStore<ProjectContextData>({
key: 'foo',
name: 'Foo',
scenes: [{
kind: 'image-panel',
label: 'An image',
img: 'foo.jpg'
}],
currentScene: 0,
})

const gotoScene = (i: number) => {
if (i >= 0 && i <= Project.scenes.length) {
updateProject('currentScene', i)
}
}
const sceneCount = () => Project.scenes.length

return {
Project,
updateProject,
gotoScene,
sceneCount,
} as const
}
export const makeProjectContext = () => {

// Using placeholder content in here... don't know how to wait for
// the result of createAsync to put data into the store.
const [project, updateProject] = createStore<ProjectContextData>({
key: 'foo',
name: 'Foo',
scenes: [{
kind: 'image-panel',
label: 'An image',
img: 'foo.jpg'
}],
currentScene: 0,
})

const gotoScene = (i: number) => {
if (i >= 0 && i <= Project.scenes.length) {
updateProject('currentScene', i)
}
}
const sceneCount = () => Project.scenes.length

return {
Project,
updateProject,
gotoScene,
sceneCount,
} as const
}
Madaxen86
Madaxen862d ago
Just declare your data fetching function in a separate file wrap the function in query and export it. Now you can call this in any component with createAsync or createAsyncStore and solid will dedupe the requests thanks to query. But doesn‘t trpc do that by default? It used Tanstack query under the hood. Which dedupes requests afaik.
Madaxen86
Madaxen862d ago
query - Solid Docs
Documentation for SolidJS, the signals-powered UI framework
Madaxen86
Madaxen862d ago
SolidStart also has a built in way for typesafe data fetching combining "use server" with query and optional preloading https://docs.solidjs.com/solid-start/building-your-application/data-loading#data-loading-always-on-the-server
Data loading - Solid Docs
Documentation for SolidJS, the signals-powered UI framework
ellers
ellersOP2d ago
Thanks for the repy @Madaxen86 ! Re tRPC, it's a good point that I may not need that. I'd used that on a previous project and carried it over. But ... putting that to the side, if I say that the async query function is just a setTimout followed by returning hard-coded data, I can't figure out how to get the result of that into the context. A key point is that the data is held in a store and then manipulated by functions provided in the context, so I'm not concerned with deduping queries here.
Madaxen86
Madaxen862d ago
Example from the docs
import { createStore } from 'solid-js/store';
import { CounterContext, INITIAL_COUNT } from "./counter.ts";

export function CounterProvider(props) {
const [value, setValue] = createStore({ count: props.initialCount || INITIAL_COUNT })

const counter = [
value,
{
increment() {
setValue("count", currentCount => currentCount + 1)
},
decrement() {
setValue("count", currentCount => currentCount - 1)
},
},
]

return (
<CounterContext.Provider value={counter}>
{props.children}
</CounterContext.Provider>
)
}
import { createStore } from 'solid-js/store';
import { CounterContext, INITIAL_COUNT } from "./counter.ts";

export function CounterProvider(props) {
const [value, setValue] = createStore({ count: props.initialCount || INITIAL_COUNT })

const counter = [
value,
{
increment() {
setValue("count", currentCount => currentCount + 1)
},
decrement() {
setValue("count", currentCount => currentCount - 1)
},
},
]

return (
<CounterContext.Provider value={counter}>
{props.children}
</CounterContext.Provider>
)
}
Now you wrap this component in another to fetch the data and wrap you context component in Suspense and pass the initial value as a prop. That’s should work.
import { createAsync } from "@solidjs/router";
import {
ParentProps,
Suspense,
createContext,
useContext
} from "solid-js";
import { SetStoreFunction, createStore } from "solid-js/store";

const Test = () => {
const data = createAsync(() => getData());
return (
<Suspense>
<ProjectContextProvider data={data()!}>
<SomeChild />
</ProjectContextProvider>
</Suspense>
);
};
export default Test;

interface IProject {
scenes: TData;
currentScene: TData[number] | null;
}
interface IPrpjectContext extends IProject {
sceneCount: () => number;
gotoScene: (i: number) => void;
updateProject: SetStoreFunction<IProject>;
}

const ProjectContext = createContext<IPrpjectContext>();
const useProject = () => {
const ctx = useContext(ProjectContext);
if (!ctx) throw new Error("useProject must be used inside ProjectContext");
return ctx;
};
const ProjectContextProvider = (props: ParentProps<{ data: TData }>) => {
const [project, updateProject] = createStore<IProject>({
scenes: props.data,
currentScene: null,
});
const gotoScene = (i: number) => {
if (i >= 0 && i <= project.scenes.length) {
updateProject("currentScene", { ...project.scenes[i] });
}
};
const sceneCount = () => project.scenes.length;
return (
<ProjectContext.Provider
value={{
get currentScene() {
return project.currentScene;
},
get scenes() {
return project.scenes;
},
sceneCount,
gotoScene,
updateProject,
}}
>
{props.children}
</ProjectContext.Provider>
);
};

const SomeChild = () => {
const ctx = useProject();
return (
<>
<button onClick={() => ctx.gotoScene(1)}>Goto scene 1</button>
<button onClick={() => ctx.gotoScene(2)}>Goto scene 2</button>
<button onClick={() => ctx.gotoScene(3)}>Goto scene 3</button>
<pre>{JSON.stringify(ctx.currentScene, null, 2)}</pre>
</>
);
};
import { createAsync } from "@solidjs/router";
import {
ParentProps,
Suspense,
createContext,
useContext
} from "solid-js";
import { SetStoreFunction, createStore } from "solid-js/store";

const Test = () => {
const data = createAsync(() => getData());
return (
<Suspense>
<ProjectContextProvider data={data()!}>
<SomeChild />
</ProjectContextProvider>
</Suspense>
);
};
export default Test;

interface IProject {
scenes: TData;
currentScene: TData[number] | null;
}
interface IPrpjectContext extends IProject {
sceneCount: () => number;
gotoScene: (i: number) => void;
updateProject: SetStoreFunction<IProject>;
}

const ProjectContext = createContext<IPrpjectContext>();
const useProject = () => {
const ctx = useContext(ProjectContext);
if (!ctx) throw new Error("useProject must be used inside ProjectContext");
return ctx;
};
const ProjectContextProvider = (props: ParentProps<{ data: TData }>) => {
const [project, updateProject] = createStore<IProject>({
scenes: props.data,
currentScene: null,
});
const gotoScene = (i: number) => {
if (i >= 0 && i <= project.scenes.length) {
updateProject("currentScene", { ...project.scenes[i] });
}
};
const sceneCount = () => project.scenes.length;
return (
<ProjectContext.Provider
value={{
get currentScene() {
return project.currentScene;
},
get scenes() {
return project.scenes;
},
sceneCount,
gotoScene,
updateProject,
}}
>
{props.children}
</ProjectContext.Provider>
);
};

const SomeChild = () => {
const ctx = useProject();
return (
<>
<button onClick={() => ctx.gotoScene(1)}>Goto scene 1</button>
<button onClick={() => ctx.gotoScene(2)}>Goto scene 2</button>
<button onClick={() => ctx.gotoScene(3)}>Goto scene 3</button>
<pre>{JSON.stringify(ctx.currentScene, null, 2)}</pre>
</>
);
};
ellers
ellersOP2d ago
Thanks for the detailed answer! Here's where I've got to, which works but I'm really not sure about the use of memo:
export const ProjectProvider: ParentComponent<{ key: string }> = (props) => {
const projectQuery = createAsync(() =>
api.projects.project.query({ key: props.key })
)

const contextValue = createMemo(() => {
const data = projectQuery()
if (data) {
return makeProjectContext(data)
}
return undefined
})

return (
<Suspense fallback={<p>Loading project data...</p>}>
<Show when={contextValue()} keyed>
{(ctx) => (
<ProjectContext.Provider value={ctx}>
{props.children}
</ProjectContext.Provider>
)}
</Show>
</Suspense>
)
}
export const ProjectProvider: ParentComponent<{ key: string }> = (props) => {
const projectQuery = createAsync(() =>
api.projects.project.query({ key: props.key })
)

const contextValue = createMemo(() => {
const data = projectQuery()
if (data) {
return makeProjectContext(data)
}
return undefined
})

return (
<Suspense fallback={<p>Loading project data...</p>}>
<Show when={contextValue()} keyed>
{(ctx) => (
<ProjectContext.Provider value={ctx}>
{props.children}
</ProjectContext.Provider>
)}
</Show>
</Suspense>
)
}
I've noted that the Suspense doesn't work - if I put an artificial delay in the async call, nothing displays at all. I can live with it, but I don't really understand it. Thanks for your comments so far. Any additional insights, particularly about <Suspense> and the memo would be great.
Madaxen86
Madaxen862d ago
createMemo is "eager" which means it will execute before render. This means that you'll read the async signal outside of the Suspense boundary and your app will Suspend on the next "higher" Boudary. Show uses createMemo under the hood. So you can just remove that from contextValue to make Suspense work as expected.

Did you find this page helpful?