S
SolidJS12mo 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
Jasmin12mo 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
florianOP12mo ago
So the way how I wanna do it isn't possible?
REEEEE
REEEEE12mo 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
florianOP12mo 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
bigmistqke12mo 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
bigmistqke12mo 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
Otonashi12mo 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
bigmistqke12mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke12mo 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
Otonashi12mo 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
bigmistqke12mo ago
True 🙏 mm i don't completely catch that, wdym? how is the length of (v) => <>{props.children}</>; != 0?
Otonashi
Otonashi12mo ago
.length of a function is the number of arguments it has
bigmistqke
bigmistqke12mo ago
i see that's good to know! that would solve this issue too
florian
florianOP12mo 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
Want results from more Discord servers?
Add your server