Creating an "group index provider" context

For context I am developing a group component which places its children in a flex manner. I want any child of the group (as well as nested ones) be able to access its previous and next sibling sizes in the group, but that seems impossible to do without wrapping each direct child of the group with a context provider. Also the whole system must be reactive. That is, the children list may get changed or reordered, and the contexts must change accordingly and propagate the updates into the nested children as well. Explanation Each child can be any JSX or HTML element, but nested in that child may be a context consumer, which wants to access the index of the direct child of the group widget. So basically there is a context access which may be nested deeply in any of the group's children. What I tried The solution I tried was to wrap each child with its own context provider, and pass the index of that child to the provider. But it didn't work because mapping over children requires using the children helper (or whatever else) for resolving the JSX elements, which effectively calls the component functions. If the context is asserted to be non-null in any of the nested children, the system simply breaks down, throwing an error. I tried using runWithOwner, but it didn't help. After a bit of research, I have found that some people use a counter and a singular context provider to increment the index of each child, but I imagine the the order that the children are resolved in is not guaranteed to stay the same.
10 Replies
deminearchiver
deminearchiverOP•4mo ago
Example Simple example for demonstrating the usage of the child index:
import {
children,
createContext,
For,
splitProps,
useContext,
type Accessor,
type Component,
type FlowComponent,
type ParentComponent,
} from "solid-js";

type GroupContext = {
index: Accessor<number>;
// Whatever else for getting information about other children,
// e.g. size of the previous, next or nth child of the group
// Example:
// previousWidth: (index: number) => number;
// nextWidth: (index: number) => number;
};
const GroupContext = createContext<GroupContext>();

const useGroup = () => useContext(GroupContext);

const GroupProvider: FlowComponent<GroupContext> = (props) => {
const [local, others] = splitProps(props, ["children"]);
return <GroupContext.Provider value={others} children={local.children} />;
};

export const Group: ParentComponent = (props) => {
// THIS DOESN'T WORK
const resolved = children(() => props.children);
return (
<div>
<For each={resolved.toArray()}>
{(child, index) => <GroupProvider index={index}>{child}</GroupProvider>}
</For>
</div>
);
};

// Example component to display the index of a direct group child
// This component may be nested deeply inside of a direct group child
const ShowIndex: Component = (props) => {
// As an example, this component requires the context to be provided
const { index } = useGroup()!;

return <span>{index()}</span>;
};

// All of the following <ShowIndex /> instances must display
// the index of the direct child of the group they are nested in,
// or, if they are the direct child, their own index.
const GroupExample: Component = () => {
return (
<Group>
<div>Hello world!</div>
<div>
<ShowIndex />
</div>
<ShowIndex />
</Group>
);
};
import {
children,
createContext,
For,
splitProps,
useContext,
type Accessor,
type Component,
type FlowComponent,
type ParentComponent,
} from "solid-js";

type GroupContext = {
index: Accessor<number>;
// Whatever else for getting information about other children,
// e.g. size of the previous, next or nth child of the group
// Example:
// previousWidth: (index: number) => number;
// nextWidth: (index: number) => number;
};
const GroupContext = createContext<GroupContext>();

const useGroup = () => useContext(GroupContext);

const GroupProvider: FlowComponent<GroupContext> = (props) => {
const [local, others] = splitProps(props, ["children"]);
return <GroupContext.Provider value={others} children={local.children} />;
};

export const Group: ParentComponent = (props) => {
// THIS DOESN'T WORK
const resolved = children(() => props.children);
return (
<div>
<For each={resolved.toArray()}>
{(child, index) => <GroupProvider index={index}>{child}</GroupProvider>}
</For>
</div>
);
};

// Example component to display the index of a direct group child
// This component may be nested deeply inside of a direct group child
const ShowIndex: Component = (props) => {
// As an example, this component requires the context to be provided
const { index } = useGroup()!;

return <span>{index()}</span>;
};

// All of the following <ShowIndex /> instances must display
// the index of the direct child of the group they are nested in,
// or, if they are the direct child, their own index.
const GroupExample: Component = () => {
return (
<Group>
<div>Hello world!</div>
<div>
<ShowIndex />
</div>
<ShowIndex />
</Group>
);
};
bigmistqke
bigmistqke•4mo ago
A completely bugfree way of doing this with context is impossible afaik. You will not be able to express something like this, for example:
const item = <Item/>
return <Group>
{ bool() ? item : undefined }
<OtherItem/>
{ !bool() ? item : undefined }
</Group>
const item = <Item/>
return <Group>
{ bool() ? item : undefined }
<OtherItem/>
{ !bool() ? item : undefined }
</Group>
In general conditional rendering and swapping orders will make it that this approach will have bugs in it. If you don't care about ssr you could do some trickery with MutationObserver observing html element in Group, adding a data-attribute to an element in ShowIndex and querying them whenever the MutationObserver is being called (you will need to handle the edge case of nested Groups).
deminearchiver
deminearchiverOP•4mo ago
Another way I forgot to mention is using jsx-tokenizer, but it just looks really unpleasant having to wrap each group child with a GroupItem token.
bigmistqke
bigmistqke•4mo ago
i am the co-author of jsx-tokenizer 🙂 with the jsx-tokenizer approach, something like :
<Group>
<div>Hello world!</div>
<div>
<ShowIndex />
</div>
<ShowIndex />
</Group>
<Group>
<div>Hello world!</div>
<div>
<ShowIndex />
</div>
<ShowIndex />
</Group>
would not work either. it requires it to be direct children but if you follow that rule jsx-tokenizer (and the generalized version: returning a non-dom element as JSX and then resolving it in the parent) will give you consistent order of children.
deminearchiver
deminearchiverOP•4mo ago
No, I meant to require wrapping each direct child in a token component, and then map the token components to context providers in the Group component
bigmistqke
bigmistqke•4mo ago
i see, yes, that could work too
deminearchiver
deminearchiverOP•4mo ago
Would this work?
<Group>
<GroupItem><ShowIndex /></GroupItem>
<GroupItem>...</GroupItem>
</Group>
<Group>
<GroupItem><ShowIndex /></GroupItem>
<GroupItem>...</GroupItem>
</Group>
bigmistqke
bigmistqke•4mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke•4mo ago
yes, i think so.
bigmistqke
bigmistqke•4mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template

Did you find this page helpful?