solid-js

SolidJs dynamic wrapping context does not load before its children


community! This is my first StackOverflow question since I could find most of the answers so far, but this issue is very hard to find. I could see similar questions, but it does not resolve my intention and focusing on making the specific problem rather than answering the fundamental issue. ( I found a similar issue in the Solid github, but does not look like it is reviewed by many people and only one answer says it is impossible)

I want to fill up in the context once children elements are loaded. I have ParentComponent which will hold ContextProvider, and the context will pass

import './App.css';
import { createContext, useContext, splitProps, createSignal, For, children as Children } from 'solid-js';
export const UserContext = createContext(0);

function App() {
    return (
        <>
            <UserContext.Provider value={3}>
                <ParentComponent>
                    <Child />
                    <Child />
                    <Child />
                </ParentComponent>
            </UserContext.Provider>
        </>
    );
}

export default App;

export const ParentComponent = props => {
    console.log('parent');
    return (
        <For each={Children(() => props.children)()}>
            {(child, idx) => {
                console.log('child in for:' + idx());
                return <UserContext.Provider value={idx}>{child}</UserContext.Provider>;
            }}
        </For>
    );
};

export const Child = () => {
    const idx = useContext(UserContext);
    console.log('child', idx);
    return <div>I am child {idx}</div>;
};

what I was expecting is seeing this.

I am child 0
I am child 1
I am child 2

but what I actually see is this.

I am child 3
I am child 3
I am child 3

I added console.log to see which one is loading first, and what I am seeing in the console is like this:

parent
child 3
child 3
child 3
child in for:0
child in for:1
child in for:2

As you see, after the parent is loaded, the children is loaded before the context provider logic. and it is never re-loaded. which makes passing information to the child impossible.

Is there a way to control loading priority? Or is it a Solidjs's bug?

as I expect this, even though the children is rendered first, once the context value is given, the children has to be rendered once more since the value of it is changed. But seems like it does not render again. I tried to make the idx to signal, but still have no luck.

Thank you!

I am trying to pass an extra values into children components not through props but through context. its value through the context is chosen based on its index. Whatever the value is given from the context to the children, children has to re-render to handle the newly given data from the context.

================

to avoid confusion, I will add more context about what I want to achieve. Below example has a complex dataflow. And I am expecting to pass the width 1,2,3 from the children in App component to be passed. But the result say it does not have existing props (width=3,2,1).

import './App.css';
import { createContext, useContext, createEffect, createSignal, For, children as Children } from 'solid-js';
export const UserContext = createContext(0);

function App() {
    return (
        <>
            <ParentComponent>
                <Child width={3} />
                <Child width={2} />
                <Child width={1} />
            </ParentComponent>
        </>
    );
}

export default App;

export const ParentComponent = props => {
    return (
        <For each={Children(() => props.children)()}>
            {(child, idx) => {
                // I made up below child.props. cannot find any way to pass existing props (width)
                return (
                    <UserContext.Provider value={idx}>
                        <Dynamic component={Child} {...child.props} idx={idx} />
                    </UserContext.Provider>
                );
            }}
        </For>
    );
};

export const Child = props => {
    const idx = useContext(UserContext);
    return (
        <div>
            I am child {props.idx ?? 'no-value'} with width {props.width ?? 'no-value'} and idx from context {idx}
        </div>
    );
};


result:

I am child 0 with width no-value and idx from context 0
I am child 1 with width no-value and idx from context 1
I am child 2 with width no-value and idx from context 2

And if I use child from the children, then props and context value is not received.

export const ParentComponent = props => {
    return (
        <For each={Children(() => props.children)()}>
            {(child, idx) => {
                const c = Children(() => child); // is there a way to pass existing child's props?
                return (
                    <UserContext.Provider value={idx}>
                        <Dynamic component={c} idx={idx} />
                    </UserContext.Provider>
                );
            }}
        </For>
    );
};

result:

I am child no-value with width 3 and idx from context 0
I am child no-value with width 2 and idx from context 0
I am child no-value with width 1 and idx from context 0

What I NEED is both of given props from the children context or props given from the parent. In React, I can do it by using cloneElement, but in Solidjs, I cannot find similar functionality.

Thank you for take a look into this issue.


Solution

  • I asked a question on the Solid github repo(https://github.com/solidjs/solid/discussions/2233), and got an answer about how to achieve this issue.

    Quick answer for the workaround is using this pattern:

    1. Child component just return props object instead of DOM element like null. Instead of trying to get some props from child "element", the child itself will be props or whatever object that is returned by Child.
    2. parent will have full access of children. using this information, we can render completely new children.

    the code will be like this:

    import { createContext, For, children as Children } from 'solid-js';
    export const UserContext = createContext(0);
    
    function App() {
        return (
            <>
                <ParentComponent width={100}>
                    <Child width={5}>child1</Child>
                    <Child width={3}>child2</Child>
                    <Child width={2}>child3</Child>
                </ParentComponent>
            </>
        );
    }
    
    export default App;
    
    export const ParentComponent = props => {
        const c = Children(() => props.children).toArray();
        const totalWidths = c.reduce((acc, child) => acc + child.width, 0);
    
        return (
            <For each={c}>
                {(child, idx) => {
                    const width = (child.width * props.width) / totalWidths; 
                    return (
                        <ChildRender idx={idx} width={width}>
                            {child.children}
                        </ChildRender>
                    );
                }}
            </For>
        );
    };
    
    export const Child = props => {
        return props;
    };
    
    const ChildRender = ({ width, idx, children }) => {
        return (
            <div>
                Child {idx} width {width} - {children}
            </div>
        );
    };
    
    

    and I can see an expected result

    Child 0 width 50 - child1
    Child 1 width 30 - child2
    Child 2 width 20 - child3
    

    using context also works with this pattern:

    import './App.css';
    import { createContext, useContext, For, children as Children } from 'solid-js';
    export const UserContext = createContext({ width: 0, idx: -1 });
    
    function App() {
        return (
            <>
                <ParentComponent width={100}>
                    <Child width={5}>child1</Child>
                    <Child width={3}>child2</Child>
                    <Child width={2}>child3</Child>
                </ParentComponent>
            </>
        );
    }
    
    export default App;
    
    export const ParentComponent = props => {
        const c = Children(() => props.children).toArray();
        const totalWidths = c.reduce((acc, child) => acc + child.width, 0);
    
        return (
            <For each={c}>
                {(child, idx) => {
                    const width = (child.width * props.width) / totalWidths; 
                    return (
                        <UserContext.Provider value={{ width, idx }}>
                            <ChildRender>{child.children}</ChildRender>
                        </UserContext.Provider>
                    );
                }}
            </For>
        );
    };
    
    export const Child = props => {
        return props;
    };
    
    const ChildRender = ({ children }) => {
        const { width, idx } = useContext(UserContext);
        return (
            <div>
                Child {idx} width {width} - {children}
            </div>
        );
    };