S
SolidJS14mo ago
florian

Pass every child in props.children a context of their index

I want to create a table where every cell knows its column number without passing it via props. I tried multiple things but they all either didn't work at all or returned undefined when getting the index from the context. Here is how I would like it to work:
const ColumnContext = createContext();

function Cell(props) {
const column = useContext(ColumnContext);

onMount(() => console.log(column)); // logs undefined, expected index

return (
...
);
}

function Row(props) {
const resolved = children(() => props.children);
return (
<div>
<For each={resolved()}>
{(child, index) => (
<ColumnContext.Provider value={index()}>
{child}
</ColumnContext.Provider>
)}
</For>
</div>
);
}

function App() {
return (
<Row>
<Cell>...</Cell>
<Cell>...</Cell>
<Cell>...</Cell>
</Row>
);
}
const ColumnContext = createContext();

function Cell(props) {
const column = useContext(ColumnContext);

onMount(() => console.log(column)); // logs undefined, expected index

return (
...
);
}

function Row(props) {
const resolved = children(() => props.children);
return (
<div>
<For each={resolved()}>
{(child, index) => (
<ColumnContext.Provider value={index()}>
{child}
</ColumnContext.Provider>
)}
</For>
</div>
);
}

function App() {
return (
<Row>
<Cell>...</Cell>
<Cell>...</Cell>
<Cell>...</Cell>
</Row>
);
}
Is this even possible and if so how?
14 Replies
Jasmin
Jasmin14mo ago
The issue here is that children(() => props.children) resolves the children before they get put under the context provider. and there isn't really a way to resolve only "one" children to split them under multiple context providers.
florian
florianOP14mo ago
So the way how I wanna do it isn't possible?
REEEEE
REEEEE14mo ago
You could make the cells "register" with the Row and the row increments a counter of the totals cells or saves something like the ref of the cell, and the cell can get it's index based on that info
florian
florianOP14mo ago
Thanks! Already kinda knew I will have to use register/unregister but I wanted to make sure I don't miss anything. This is what I'm using for now:
const RowContext = createContext();

function useRow() {
return useContext(RowContext);
}

function Cell() {
const row = useRow();
const columnIndex = row.register();

onCleanup(() => row.unregister(columnIndex()));

return <div textContent={columnIndex()}/>;
}

function Row(props) {
const indexes = [];

const context = {
register() {
const [index, setIndex] = createSignal(indexes.length);
indexes.push([index, setIndex]);
return index;
},
unregister(columnIndex) {
indexes.splice(columnIndex, 1);
for (let i = columnIndex; i < indexes.length; i++) {
indexes[i][1](prev => prev - 1);
}
}
};

return (
<RowContext.Provider value={context} children={props.children} />
);
}

export default function App() {
const [cells, setCells] = createSignal(3);
return (
<>
<button onClick={() => setCells(prev => prev + 1)}>Add</button>
<button onClick={() => setCells(prev => prev - 1)}>Remove</button>
<Row>
<For each={Array.from({length: cells()})}>
{() => <Cell />}
</For>
</Row>
</>
);
}
const RowContext = createContext();

function useRow() {
return useContext(RowContext);
}

function Cell() {
const row = useRow();
const columnIndex = row.register();

onCleanup(() => row.unregister(columnIndex()));

return <div textContent={columnIndex()}/>;
}

function Row(props) {
const indexes = [];

const context = {
register() {
const [index, setIndex] = createSignal(indexes.length);
indexes.push([index, setIndex]);
return index;
},
unregister(columnIndex) {
indexes.splice(columnIndex, 1);
for (let i = columnIndex; i < indexes.length; i++) {
indexes[i][1](prev => prev - 1);
}
}
};

return (
<RowContext.Provider value={context} children={props.children} />
);
}

export default function App() {
const [cells, setCells] = createSignal(3);
return (
<>
<button onClick={() => setCells(prev => prev + 1)}>Add</button>
<button onClick={() => setCells(prev => prev - 1)}>Remove</button>
<Row>
<For each={Array.from({length: cells()})}>
{() => <Cell />}
</For>
</Row>
</>
);
}
Lmk if anyone finds a better/more performant solution 🙂
bigmistqke
bigmistqke14mo ago
mmm, i am not sure if context will work if you want to consider re-ordering of elements and conditionals with <Show/> and the like
bigmistqke
bigmistqke14mo ago
but if u don't mind typescript yelling at u and going a bit hacky: https://playground.solidjs.com/anonymous/2abb0360-ba22-4e08-9a66-0bc71b53c518
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
Otonashi
Otonashi14mo ago
the implementation is rough (index is an accessor) but the idea is correct, you have to delay execution of the children until you can inject the index either directly or via context in any case if you intend to use that method, https://github.com/solidjs-community/solid-primitives/tree/main/packages/jsx-tokenizer may help here's a slightly different example for reference https://playground.solidjs.com/anonymous/e0d4d0b4-7007-405b-848f-e5efdf80c26f
bigmistqke
bigmistqke14mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke14mo ago
mm interesting. i don't really understand tbh how this works. i had the impression before children would just execute all functions, no matter how nested, until it got a non-function value, but mb that was an incorrect assumption. we were actually thinking of going for a simpler cast with jsx-tokenizer but never got around implementing it https://github.com/solidjs-community/solid-primitives/issues/399 i would prefer it if it got official support bc it's pretty cool u can do this kind of stuff and is a quality that sets solid apart
Otonashi
Otonashi14mo ago
it ignores functions with .length != 0 is there a reason you're calling setIndex instead of simply passing the index accessor? i.e. https://playground.solidjs.com/anonymous/53013ab4-28af-4619-8be6-fe94248bf648
bigmistqke
bigmistqke14mo ago
True 🙏 mm i don't completely catch that, wdym? how is the length of (v) => <>{props.children}</>; != 0?
Otonashi
Otonashi14mo ago
.length of a function is the number of arguments it has
bigmistqke
bigmistqke14mo ago
i see that's good to know! that would solve this issue too
florian
florianOP14mo ago
Thanks for all the help again! Think I'm pretty happy with this solution now:
import {children, createContext, createSignal, For, Show, useContext} from "solid-js";

const ColumnContext = createContext();

function useColumn() {
return useContext(ColumnContext);
}

function Cell(props) {
return {
render() {
const column = useColumn();
return (
<div>
{column()}. {props.textContent}
</div>
);
}
};
}

function Row(props) {
const resolved = children(() => props.children);

return (
<div style="display: flex; column-gap: 20px">
<For each={resolved.toArray().filter(Boolean)}>
{(child, index) => (
<ColumnContext.Provider value={index}>
{child.render ? child.render() : child}
</ColumnContext.Provider>
)}
</For>
</div>
);
}

export default function App() {
const [cells, setCells] = createSignal(3);
const [showCell, setShowCell] = createSignal(false);
return (
<>
<button onClick={() => setCells(prev => prev + 1)}>Add</button>
<button onClick={() => setCells(prev => prev - 1)}>Remove</button>
<button onClick={() => setShowCell(prev => !prev)}>{showCell() ? "Hide" : "Show"}</button>
<Row>
<Show when={showCell()}>
<Cell textContent="show" />
</Show>
<For each={Array.from({length: cells()})}>
{() => <Cell textContent="for" />}
</For>
</Row>
</>
);
}
import {children, createContext, createSignal, For, Show, useContext} from "solid-js";

const ColumnContext = createContext();

function useColumn() {
return useContext(ColumnContext);
}

function Cell(props) {
return {
render() {
const column = useColumn();
return (
<div>
{column()}. {props.textContent}
</div>
);
}
};
}

function Row(props) {
const resolved = children(() => props.children);

return (
<div style="display: flex; column-gap: 20px">
<For each={resolved.toArray().filter(Boolean)}>
{(child, index) => (
<ColumnContext.Provider value={index}>
{child.render ? child.render() : child}
</ColumnContext.Provider>
)}
</For>
</div>
);
}

export default function App() {
const [cells, setCells] = createSignal(3);
const [showCell, setShowCell] = createSignal(false);
return (
<>
<button onClick={() => setCells(prev => prev + 1)}>Add</button>
<button onClick={() => setCells(prev => prev - 1)}>Remove</button>
<button onClick={() => setShowCell(prev => !prev)}>{showCell() ? "Hide" : "Show"}</button>
<Row>
<Show when={showCell()}>
<Cell textContent="show" />
</Show>
<For each={Array.from({length: cells()})}>
{() => <Cell textContent="for" />}
</For>
</Row>
</>
);
}
This way its also pretty easy to just remove the context stuff and make the index available to the cell via putting it in the render() args instead

Did you find this page helpful?