S
SolidJS3w ago
Leal

Unable to provide context to children

I'm writing a dropdown component, so I decided to use a context to provide tools to the children. I have defined the context as an object with some setters and accessors to a set of signals and no matter what I do, I can't get it from the children components. I'm using SolidJS + SolidStart Here's some code snippets:
type DropdownCtx = {
rootElement: Accessor<HTMLDivElement>;
setValue: Setter<any>;
setSelected: Setter<HTMLElement | undefined>;
};

const DropdownContext = createContext<DropdownCtx>();

export const DropdownComponent = <T extends any>(props: DropdownProps<T>) => {
let rootElement!: HTMLDivElement;
const rootElementAccessor = () => rootElement;

const children = solidChildren(() => props.children); // I import solid's children fn as solidChildren

const [value, setValue] = createSignal(props.value);
const [selected, setSelected] = createSignal<HTMLElement | undefined>(undefined);

// ...

const context: DropdownCtx = {
rootElement: rootElementAccessor,
setValue,
setSelected,
};

// ...

return (
// ...
<dialog
class='dropdown__popup'
id={idPopup()}
open={isOpen()}
>
<DropdownContext.Provider value={context}>
{children()}
</DropdownContext.Provider>
</dialog>
);
};


export const DropdownItemComponent = <T extends any>(props: DropdownItemProps<T>) => {
// ...

const context = useContext(DropdownContext);

if (!context) {
throw new Error("Missing context");
}

// ...
};

// Usage

<DropdownComponent>
<DropdownItemComponent>
<span>help :(</span>
</DropdownItemComponent>
</DropdownComponent>
type DropdownCtx = {
rootElement: Accessor<HTMLDivElement>;
setValue: Setter<any>;
setSelected: Setter<HTMLElement | undefined>;
};

const DropdownContext = createContext<DropdownCtx>();

export const DropdownComponent = <T extends any>(props: DropdownProps<T>) => {
let rootElement!: HTMLDivElement;
const rootElementAccessor = () => rootElement;

const children = solidChildren(() => props.children); // I import solid's children fn as solidChildren

const [value, setValue] = createSignal(props.value);
const [selected, setSelected] = createSignal<HTMLElement | undefined>(undefined);

// ...

const context: DropdownCtx = {
rootElement: rootElementAccessor,
setValue,
setSelected,
};

// ...

return (
// ...
<dialog
class='dropdown__popup'
id={idPopup()}
open={isOpen()}
>
<DropdownContext.Provider value={context}>
{children()}
</DropdownContext.Provider>
</dialog>
);
};


export const DropdownItemComponent = <T extends any>(props: DropdownItemProps<T>) => {
// ...

const context = useContext(DropdownContext);

if (!context) {
throw new Error("Missing context");
}

// ...
};

// Usage

<DropdownComponent>
<DropdownItemComponent>
<span>help :(</span>
</DropdownItemComponent>
</DropdownComponent>
I have tried: - Using props.children instead of children() - Moving the context provider up - Declaring the context as a store using createStore I can't get Solid to provide the context and I don't really know the reason, every try throws the error :(
7 Replies
peerreynders
peerreynders3w ago
Are those snippets in the same module? Otherwise it's not clear where
const context = useContext(DropdownContext);
const context = useContext(DropdownContext);
is getting DropdownContext from.
// https://www.solidjs.com/tutorial/stores_context?solved
import { createSignal, createContext, useContext } from 'solid-js';

const CounterContext = createContext();

export function CounterProvider(props) {
const [count, setCount] = createSignal(props.count || 0),
counter = [
count,
{
increment() {
setCount((c) => c + 1);
},
decrement() {
setCount((c) => c - 1);
},
},
];

return (
<CounterContext.Provider value={counter}>
{props.children}
</CounterContext.Provider>
);
}

export function useCounter() {
return useContext(CounterContext);
}
// https://www.solidjs.com/tutorial/stores_context?solved
import { createSignal, createContext, useContext } from 'solid-js';

const CounterContext = createContext();

export function CounterProvider(props) {
const [count, setCount] = createSignal(props.count || 0),
counter = [
count,
{
increment() {
setCount((c) => c + 1);
},
decrement() {
setCount((c) => c - 1);
},
},
];

return (
<CounterContext.Provider value={counter}>
{props.children}
</CounterContext.Provider>
);
}

export function useCounter() {
return useContext(CounterContext);
}
That's why the general practice is to define both the provider (CounterProvider) and the context specific hook (useCounter) within the same module so that both can share the context value as a module global value. Note: Context as a module global isn't good enough if it is used during SSR as multiple requests within the same worker will share the same module (and by extension its globals). At that point it becomes necessary on the server side store the Context in locals of RequestEvent to keep the Context of each request isolated. Finally, I haven't used children a lot. But it seems to me that with:
const children = solidChildren(() => props.children);

const [value, setValue] = createSignal(props.value);
const [selected, setSelected] = createSignal<HTMLElement | undefined>(
undefined
);

// ...

const context: DropdownCtx = {
rootElement: rootElementAccessor,
setValue,
setSelected,
};
const children = solidChildren(() => props.children);

const [value, setValue] = createSignal(props.value);
const [selected, setSelected] = createSignal<HTMLElement | undefined>(
undefined
);

// ...

const context: DropdownCtx = {
rootElement: rootElementAccessor,
setValue,
setSelected,
};
You may force props.children to be evaluated before the Context is ready; i.e.
const context = useContext(DropdownContext);
const context = useContext(DropdownContext);
is forced to happen while the DropdownContext still only contains undefined. So I'd start by isolating the provider and hook into their own module so that the provider can be imported at the root (at which time all of the context infrastructure should be initialized) and the hook can be imported separately by the nested component.
foolswisdom
foolswisdom3w ago
The problem here is that the children are created early. Normally, props.children is a lazy access, so the children are created under the context provider boundary. In this case, you're eagerly accessing it in the solidChildren function (it's a wrapper around memo, which is eager). This means you're freaking the children outside the boundary. https://docs.solidjs.com/reference/component-apis/children#children Towards the bottom (though it sadly doesn't call out the implications for context)
An important aspect of the children helper is that it forces the children to be created and resolved, as it accesses props.children immediately.
peerreynders
peerreynders3w ago
Given:
I have tried: Using props.children instead of children()
I suspect there may well be something else be going on in code that we aren't seeing …
foolswisdom
foolswisdom3w ago
It isn't clear where props.children was used. If it's in the same place as where the children are accessed / created in the above code, that would result in the same problem. Or to restate the issue: the children helper does not change where the creation of children occurs, it's merely a helper to memoize so they're only created once. And the problem is that creating the children in the body of the component, above the jsx, is above the provider boundary. In your example @peerreynders, children are created lazily in the jsx template. If you move the (nominally identical) access up above the jsx, it becomes eager and context will no longer work
Leal
LealOP3w ago
Thank you guys for answering and sorry for my delay, had to clean the house followed by a power drop 🥲 Answering your messages, in order: - Are those snippets in the same module? Yes, const DropdownContext = createContext<DropdownCtx>(); and both components are in the same module, only the usage part is outside the module. - Context as a module global isn't good enough if it is used during SSR... I couldn't really find examples on context for SolidStart projets, I'll be adjusting the code according to your description and make some tests, but if it's really necessary to declare context as locals for each request, then I don't really understood it's objective. The way I understood context was like an Angular component providing some value that is accessible in it's children, therefore the declaration of the context would just be some kind of identifier to the resource but I might be really wrong here. - You may force props.children to be evaluated before the Context is ready I have tried using props.children instead of the children fn from solid, it had the same result - So I'd start by isolating the provider and hook into their own module... I'll be doing that - The problem here is that the children are created early... children() is only used in the snippet provided, and as I said, I have tried using props.children directly - I suspect there may well be something else be going on in code that we aren't seeing … I'll take the business logic out of the component and provide you guys the hole file ------ As I was writing this answer, based on your suggestions, I was able to fix the problem. It's really a children fn problem. When I tried using props.children I still had the children fn declaration at the beginning of the component and by removing that the problem was fixed! I don't really know when to properly use the children fn, and I don't really remember where I've read that it was a good practice to use it on every component and please, if this is a wrong statement I would love to understand when it's a good thing to use it Just for the sake of documentation, here's the fixed snippet:
type DropdownCtx = {
rootElement: Accessor<HTMLDivElement>;
setValue: Setter<any>;
setSelected: Setter<HTMLElement | undefined>;
};

const DropdownContext = createContext<DropdownCtx>();

export const DropdownComponent = <T extends any>(props: DropdownProps<T>) => {
let rootElement!: HTMLDivElement;
const rootElementAccessor = () => rootElement;

const [value, setValue] = createSignal(props.value);
const [selected, setSelected] = createSignal<HTMLElement | undefined>(undefined);

// ...

const context: DropdownCtx = {
rootElement: rootElementAccessor,
setValue,
setSelected,
};

// ...

return (
// ...
<dialog
class='dropdown__popup'
id={idPopup()}
open={isOpen()}
>
<DropdownContext.Provider value={context}>
{props.children}
</DropdownContext.Provider>
</dialog>
);
};


export const DropdownItemComponent = <T extends any>(props: DropdownItemProps<T>) => {
// ...

const context = useContext(DropdownContext);

if (!context) {
throw new Error("Missing context");
}

// ...
};

// Usage

<DropdownComponent>
<DropdownItemComponent>
<span>help :(</span>
</DropdownItemComponent>
</DropdownComponent>
type DropdownCtx = {
rootElement: Accessor<HTMLDivElement>;
setValue: Setter<any>;
setSelected: Setter<HTMLElement | undefined>;
};

const DropdownContext = createContext<DropdownCtx>();

export const DropdownComponent = <T extends any>(props: DropdownProps<T>) => {
let rootElement!: HTMLDivElement;
const rootElementAccessor = () => rootElement;

const [value, setValue] = createSignal(props.value);
const [selected, setSelected] = createSignal<HTMLElement | undefined>(undefined);

// ...

const context: DropdownCtx = {
rootElement: rootElementAccessor,
setValue,
setSelected,
};

// ...

return (
// ...
<dialog
class='dropdown__popup'
id={idPopup()}
open={isOpen()}
>
<DropdownContext.Provider value={context}>
{props.children}
</DropdownContext.Provider>
</dialog>
);
};


export const DropdownItemComponent = <T extends any>(props: DropdownItemProps<T>) => {
// ...

const context = useContext(DropdownContext);

if (!context) {
throw new Error("Missing context");
}

// ...
};

// Usage

<DropdownComponent>
<DropdownItemComponent>
<span>help :(</span>
</DropdownItemComponent>
</DropdownComponent>
Thank you guys for your help!
peerreynders
peerreynders3w ago
but if it's really necessary to declare context as locals for each request.
Generally context is only active on the client-side so there is nothing to worry about. It only becomes an issue once session specific information fed through context drives the initial server side render. At that point the context returned from createContext should be stored in a more isolated location than a module global that can be accessed by multiple concurrent requests on the server side. https://github.com/peerreynders/solid-start-sse-chat/blob/d2b9070f956947c940dc2046abbbbf4bbcbd58e2/src/components/history-context/index.tsx#L380-L386 Clearly that wasn't your problem.
GitHub
solid-start-sse-chat/src/components/history-context/index.tsx at d2...
Basic Chat demonstration with server-sent events (SSE) - peerreynders/solid-start-sse-chat
Leal
LealOP3w ago
Oh, I see where this can cause some problems Thank you very much for your help!

Did you find this page helpful?