Hi, I've read through https://github.com/ryansolid/solid/blob/master/documentation/rendering.md and understand that using map isn't optimized, and we should use <For>, but I don't really understand why. Could you elaborate on what happens when we use one vs the other?
Similarly, with using a ternary operator vs <Show>, or switch vs <Switch> - what's going to happen to my app if I don't use the Control Flow components?
Ok I think it's easiest to demonstrate by imagining a psuedo code version of what is happening.
So as you know. No Virtual DOM. So when you see <div /> it's more or less document.createElement("div")
So picture what happens when we use map:
// simple list of divs with text
<>{
list().map(text=> <div>{text}</div>
}</>
```js
// compiles roughly to:
createEffect(() => {
// reference trackable data so it re-runs on update
const items = list();
const rendered = items.map(item => {
const el = document.createElement("div");
});
// assume we check what's there and move and update and remove nodes
diffAndPatch(parentEl.childNodes, rendered);
})
There is a huge problem with this approach if the list ever updates. Every time list changes even for the same item we have different DOM nodes. Even if we explicitly key them so we know that we can skip them or use the version already rendered you are creating those DOM elements. Ie. If you have 1000 items in a list and you swap 2, you are recreating all 1000 items even though you need to recreate 0 of them.
By the time any diffing can occur you've already paid the cost since you've mapped over every item again. The only thing we can do here is not recreate the DOM nodes we don't need to. We do that by having a special helper function that keeps the previous input value along with the previous mapped value and if it finds the new input in the old input it passes through the old mapped value instead of creating it again. And then when we get to the diffing stage it can recognize we are dealing with the exact same DOM nodes and everything can be optimized. Solid has a `mapArray` method to do this but I offer the `<For />` component for convenience and consistency. It has some other small benefits when used in the view and I've made these control flows Suspense aware by default to reduce complexity for end users.
Conditionals are sort of similar. They have a slightly different problem in that you want to isolate the condition from the effect resolution. If you have a condition like `count() > 5` a reactive library has no choice but to re-evaluate every time count changes. However your condition doesn't care when it changes unless the boolean switches from false to true or vice versa. Picture:
```jsx
const [count, setCount] = createSignal(0);
setInterval(() => setCount(count() + 1, 1000)
<div>{ count() > 5 && <MyComp /> }</div>
Without intervention every time count incremented past 5 and it re-evaluated it would be re-creating MyComp. We do not want this since our component contains real DOM node creation and probably reactive effects. This is costly. Instead we want to hoist out the condition and have it more less look like visible() && <MyComp />. Special helper here also comes to the rescue.
Luckily ternary and boolean logic is part of the language (unlike map which is an arbitrary function call) so I can statically analyze the grammar in bindings to automatically do this hoisting. But keep in mind it is only something I can do in the JSX where I can analyze the grammar. If you move the logic out to a function I can't see it. So you are safe doing Boolean conditionals and Ternaries in your view code. <Show /> and <Switch /> have the advantage of being Suspense aware so I recommend them for Component insertion logic (especially around major mounting point like routing). If you are just toggling some text or whatnot use a ternary or if the condition never updates etc.. They are fine in most places anyway.
But honestly give the helpers a try. Not having to escape into expressions I find makes the code look less noisy.
Note
By Suspense Aware I mean that when Suspense is in a suspended state these decision points will stay showing the previous content rather than immediately flipping. Remembering that Solid is granular and updates happen at any level of the hierarchy independently Suspense mechanism needs to work like this to save massive work of re-rendering everything all the time.
Thank you Ryan, that was exactly the explanation I was looking for!
especially around major mounting point like routing
I trying to change my code about router here https://github.com/aleksanderd/solid-router-sandbox/commit/f8e3636b3a369820d15294e53eb83ac61cde2fb2
I tried For with ternary inside, but it is not reactive (may be cos conditional render itself: route.component vs route.children). Currently this variant work ok(now it not recreating parents on subroutes changes):
<Switch fallback={props.fallback}>
{getRoutes().map((route, idx) => (
<Match when={idx === getRouteIdx()}>
{route.component ? <route.component /> : route.children}
</Match>
))}
</Switch>
but I not sure about native map and conditional {route.component ? <route.component /> : route.children}.
also, may be there is a way to "cache" created sub-switches at lost its active state?
thank you!
Yeah thanks for the reminder to update docs. CallbackFn for control are all sampled (non-tracked). I do this for 3 reasons. It reduces creating reactive primitives unnecessarily which can improve performance. It is useful as a static place to do certain resolutions where you do not want reactivity, it's always easy to add reactivity back. It's consistent with other callbacks being not reactive like event handlers. The thing is a callback function is called by the owner and is up to their discretion how to handle it.
To your example. Unless getRoutes dynamically changes native map is fine. Do you see this list dynamically changing? Caching is possible but keep in mind if it is connected to the reactive system is live so to speak (even if off screen). That can be really cool and beneficial but I cannot see that ever be the default behavior. There are a few ways to achieve this. Once would simply be to create a wrapper (perhaps Component) that just per route stores the rendered result on first time rendered in a hoisted context (something outside of the component itself.. you can picture using like a factory function to create the component that returns the component to be instantiated but references the cache over a closure. Since there is granularity here as long as we are fine sending the original props down (we should be as they too are reactive and wrapped, and in so independently updated) this could work.
But as I said I think it's playing with fire a bit. I think the end consumer could choose to do it themselves if they choose simply by making that the component. Example of what I'm talking about:
function cache(Comp) {
let instance;
return (props) => instance || (instance = <Comp {...props} />);
}
// use by creating component in a hoisted context..
const Comp1 = cache(MyComp),
Comp2 = cache(OtherComp),
// down in the view
<>{ cond() ? <Comp1 someProp={state.something /> : <Comp2 anotherProp={state.other} /> }</>
No matter how many times you switch back and forth you get the same instance of Comp1 and Comp2. But as you see the end user could do that regardless of the routing solution.
thank you!
in my example, getRoutes - is memo with deps on props.children & props.routes, and getRouteIdx is signal with current route index(in getRoutes result).
@aleksanderd Hmm if getRoutes is dynamic you want to be using For or it's underlying method (mapArray) otherwise adding routes will cause re-evaluates of the whole list. I mean Matches aren't real DOM nodes so it isn't completely expensive but I imagine it could cascade into something not good. However looking you already use mapArray in getRoutes I wonder if there is a simpler way to combine these without chaining mapArray into mapArray etc.. I've seen a few approaches now towards routing. Maybe it deserves some high level conversation in the other thread.
@ryansolid thanks!
if getRoutes is dynamic you want to be using For or it's underlying method (mapArray)
I tried the both:
// `route.render` is
// `() => route.component ? <route.component /> : route.children`)
<Switch fallback={props.fallback}>
<For each={getRoutes()}>
{(route, idx) => (
<Match when={idx() === getRouteIdx()}>{route.render()}</Match>
)}
</For>
</Switch>
and:
<Switch fallback={props.fallback}>
{mapArray(getRoutes, (route, idx) => (
<Match when={idx() === getRouteIdx()}>{route.render()}</Match>
))}
</Switch>
but they both not work: always fallback.
I tried to reproduce it outside of router context (click for code)
import {
Component,
For,
Switch,
Match,
createSignal,
mapArray,
} from 'solid-js';
export const RenderArrays: Component = () => {
const arr = ['111', '222', '333'];
const [getIdx, setIdx] = createSignal(0);
setInterval(() => {
let idx = getIdx() + 1;
setIdx(idx >= arr.length ? 0 : idx);
}, 777);
return (
<div>
<h3>native map in Switch:</h3>
{/* work ok */}
<Switch>
{arr.map((item, idx) => (
<Match when={getIdx() === idx}>{item}</Match>
))}
</Switch>
<h3>For in Switch:</h3>
{/* always fallback */}
<Switch fallback="Switch fallback">
<For each={arr}>
{(item, idx) => <Match when={getIdx() === idx()}>{item}</Match>}
</For>
</Switch>
<h3>mapArray in Switch:</h3>
{/* always fallback */}
<Switch fallback="Switch fallback">
{mapArray(
() => arr,
(item, idx) => (
<Match when={getIdx() === idx()}>{item}</Match>
),
)}
</Switch>
<h3>ternary in For:</h3>
{/* always initial(first) */}
<For each={arr} fallback="For fallback">
{(item, idx) => getIdx() === idx() && item}
</For>
</div>
);
};
only native map variant is works ok (
Is this code correct? No miss-understanding?
I missed your original goal was to put it in a Switch. I've never tried using For inside a Switch... I suspect For or mapArray own memoization gets in the way. Switch expects Matches to be immediate children and while map does that anything reactive would not as there would be a function in between. It was simplistic. I never considered dynamic children. Which is arguably a mistake. It's possible Switch in general is too complicated to be a built in. I figured people would just loosen match criteria rather than dynamically generate.
Honestly I suspect this scenario could forgo using this prebuilt stuff here and just write what makes sense working backwards from the desired API. The control flow in Solid are just examples of what can be done, but we are in no means locked into them. The code for Switch/Map is like 50 lines or something we can just write the router literally as is. Looping and mapping is all more expensive.
Not sure this makes sense, but if we do want to use the prebuilt have you considered just using props.children. What if Route returned Match components. And housed the Switch.. Ie:
const RouteSwitch = (props) => {
return <RouteSwitchProvider>
<Switch fallback={props.fallback}>{props.children}</Switch>
</RouteSwitchProvider>
}
const Route= (props) => {
const someContextStuff = useContext();
return <Match when={someContextStuff(props.something)}>{props.children}</Match>;
}
Most helpful comment
Ok I think it's easiest to demonstrate by imagining a psuedo code version of what is happening.
So as you know. No Virtual DOM. So when you see
<div />it's more or less document.createElement("div")So picture what happens when we use
map:```js
// compiles roughly to:
createEffect(() => {
// reference trackable data so it re-runs on update
const items = list();
const rendered = items.map(item => {
const el = document.createElement("div");
});
// assume we check what's there and move and update and remove nodes
diffAndPatch(parentEl.childNodes, rendered);
})
Without intervention every time count incremented past 5 and it re-evaluated it would be re-creating
MyComp. We do not want this since our component contains real DOM node creation and probably reactive effects. This is costly. Instead we want to hoist out the condition and have it more less look likevisible() && <MyComp />. Special helper here also comes to the rescue.Luckily ternary and boolean logic is part of the language (unlike
mapwhich is an arbitrary function call) so I can statically analyze the grammar in bindings to automatically do this hoisting. But keep in mind it is only something I can do in the JSX where I can analyze the grammar. If you move the logic out to a function I can't see it. So you are safe doing Boolean conditionals and Ternaries in your view code.<Show />and<Switch />have the advantage of being Suspense aware so I recommend them for Component insertion logic (especially around major mounting point like routing). If you are just toggling some text or whatnot use a ternary or if the condition never updates etc.. They are fine in most places anyway.But honestly give the helpers a try. Not having to escape into expressions I find makes the code look less noisy.
Note
By Suspense Aware I mean that when Suspense is in a suspended state these decision points will stay showing the previous content rather than immediately flipping. Remembering that Solid is granular and updates happen at any level of the hierarchy independently Suspense mechanism needs to work like this to save massive work of re-rendering everything all the time.