Reputation: 35
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.
Upvotes: 0
Views: 455
Reputation: 35
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:
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>
);
};
Upvotes: 0
Reputation: 13698
That is an expected behavior as Solid allows you to control execution order using component's structure + getter methods. In other words, it is up to you to execute children first or parent first, although it is a bit tricky to grasp the execution logic because the compiled code creates a closure and calls rendering methods that calls render effects which will be executed by the scheduler.
Once I opened an issue on this very same topic, thinking it was a bug not to have a deterministic rendering order. You can read more about it: https://github.com/solidjs/solid/issues/1997
You can see this answer for how to solve it: https://stackoverflow.com/a/76995880/7134134
Context provider is just a regular component. Once you get how components are rendered, you can control its execution order. You can read more about Context API: https://stackoverflow.com/a/74492899/7134134
PS: OP marked his answer as the accepted answer in the provided answer. You need to read the linked answer. It is exactly the same issue. Context provider is executed first, that is why you get undefined
value. Returning context provider and having it as a child component produce different results.
I see your aim is setting the context value inside the For
component but that forces you to return the provider component, which produces a different result.
Correct way to provide a context value is keeping the receiver as its children:
<UserContext.Provider value={1}><Child /></UserContext.Provider>
The problem with your code is that the execution order is reversed multiple times while the component tree is being executed, producing a result that is hard to reason about.
Here is what your code produces:
import { template as _$template } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";
var _tmpl$ = /*#__PURE__*/_$template(`<div>I am child `);
import { For, createContext, useContext } from 'solid-js';
import { render } from 'solid-js/web';
export const UserContext = createContext(0);
const ParentComponent = props => {
console.log('Parent');
return _$createComponent(For, {
get each() {
return props.children;
},
children: (child, idx) => {
console.log('child in for:' + idx());
return _$createComponent(UserContext.Provider, {
get value() {
return idx();
},
children: child
});
}
});
};
const Child = () => {
const idx = useContext(UserContext);
console.log('child', idx);
return (() => {
var _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
_$insert(_el$, idx, null);
return _el$;
})();
};
function App() {
console.log('App');
return _$createComponent(UserContext.Provider, {
value: 0,
get children() {
return _$createComponent(ParentComponent, {
get children() {
return [_$createComponent(Child, {}), _$createComponent(Child, {}), _$createComponent(Child, {})];
}
});
}
});
}
render(() => _$createComponent(App, {}), document.body);
Live Demo: https://playground.solidjs.com/anonymous/79dd68ec-79bc-45f7-9076-464fe74d45ae
Here is how you can do it:
import { type Component, For, type JSXElement, createContext, useContext } from 'solid-js';
import {render } from 'solid-js/web';
export const UserContext = createContext(0);
const ParentComponent: Component<{ children: Array<JSXElement> }> = props => {
console.log('Parent');
return (
<For each={props.children}>
{(child, idx) => {
console.log('child in for:' + idx());
return child
}}
</For>
);
};
const Child: Component<any> = () => {
const idx = useContext(UserContext);
console.log('child', idx);
return <div>I am child {idx}</div>;
};
function App() {
console.log('App');
return (
<ParentComponent>
<UserContext.Provider value={1}><Child /></UserContext.Provider>
<UserContext.Provider value={2}><Child /></UserContext.Provider>
<UserContext.Provider value={3}><Child /></UserContext.Provider>
</ParentComponent>
);
}
render(() => <UserContext.Provider value={0}><App /></UserContext.Provider>, document.body);
Live Demo: https://playground.solidjs.com/anonymous/2197fa8f-fbe0-40f5-a18c-deb54280bb7e
The compiled code for the above example is:
import { template as _$template } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";
var _tmpl$ = /*#__PURE__*/_$template(`<div>I am child `);
import { For, createContext, useContext } from 'solid-js';
import { render } from 'solid-js/web';
export const UserContext = createContext(0);
const ParentComponent = props => {
console.log('Parent');
return _$createComponent(For, {
get each() {
return props.children;
},
children: (child, idx) => {
console.log('child in for:' + idx());
return child;
}
});
};
const Child = () => {
const idx = useContext(UserContext);
console.log('child', idx);
return (() => {
var _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
_$insert(_el$, idx, null);
return _el$;
})();
};
function App() {
console.log('App');
return _$createComponent(ParentComponent, {
get children() {
return [_$createComponent(UserContext.Provider, {
value: 1,
get children() {
return _$createComponent(Child, {});
}
}), _$createComponent(UserContext.Provider, {
value: 2,
get children() {
return _$createComponent(Child, {});
}
}), _$createComponent(UserContext.Provider, {
value: 3,
get children() {
return _$createComponent(Child, {});
}
})];
}
});
}
render(() => _$createComponent(UserContext.Provider, {
value: 0,
get children() {
return _$createComponent(App, {});
}
}), document.body);
Upvotes: 0