Reputation: 5800
In Solid.js, components are not re-rendered when their props change. We must pass in something reactive (like a signal) if the component wants to react to "CRUD-like" operations to its props.
If we have a component Example
and a signal foo
we can pass in the entire signal with <Example foo={foo}/>
. Alternatively, we can make the Example
component accept a static value and we can instead pass the value at the current moment in time with <Example foo={foo()}/>
.
We often desire the latter in order to prevent arbitrary CRUD operations on props. This is useful if we want a stateful component that has clear non-CRUD state transitions, where the only way to change input data is to construct a fresh component instance. This is crucial to prevent reaching illegal states within critical components.
My question then is how can we automatically make a fresh instance of Example
any time its static props change?
I would have expected that because we are using foo={foo()}
that this would create a dependency on foo
and thus the code to construct the Example
component would automatically re-run when necessary, but this appears not to be the case.
This is similar to this Svelte question.
Example A
In the following example, I would like to return a different input
element whenever foo
changes.
export default () => {
const [foo, setFoo] = createSignal();
return <input .../>;
};
Example B
In this example, Foo
intentionally takes in certain props that are not reactive, and we require Foo
to be entirely recreated if we need to pass in a new value for that prop.
In the below example, day
is not reactive, but time
is.
const Foo = ({ day, time }) => {
// day: "mon", "tue", etc.
// time: "day-time", "night-time"
const weather = fetch_weather(day);
return <>
<Show when={time() === "day-time"}>
{weather.dayTimeResults}
</Show>
<Show when={time() === "night-time"}>
{weather.nightTimeResults}
</Show>
</>
};
export const Weather = () => {
const [day, setDay] = createSignal();
const [time, setTime] = createSignal();
// This does not work
return <Foo day={day()} time={time}/>;
};
Additional
I have devised a solution in my answer below, however this solution requires writing each reactive dependency twice, which is tedious but is also highly error-prone.
I'm looking for a better solution.
Upvotes: 1
Views: 1970
Reputation: 13698
The right way to return different elements based on a condition is using a memo. Since the returned elements are wrapped in a memo, they will be tracked by their consumers too. If you check the built-in components you will see that is how they return a different component based on a condition.
import { render } from "solid-js/web";
import { createSignal, createMemo, JSX } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
const comp = createMemo(() => {
if (count() % 2 === 0) {
return <button type="button" onClick={increment}>Inc</button>
}
return (
<button type="button" onClick={increment}>
Count: {count()} {Date.now()}
</button>
);
}) as unknown as JSX.Element
return comp;
}
render(() => <Counter />, document.getElementById("app")!);
Live Demo: https://playground.solidjs.com/anonymous/bdb6e250-899f-4d82-8317-8658fd5eabb2
Please note that online playground has some issues with exported JSX and JSXElement:
Uncaught SyntaxError: The requested module 'solid-js'
does not provide an export named 'JSX'
That is why I changed component type from JSX.Element to JSX.Element for the live demo. The code snippet should work without any issues in an actual development environment.
Condition is not necessary, you can return an element directly but the value will not be reactive unless you track a signal inside the function:
import { render } from "solid-js/web";
import { createSignal, createMemo } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
const comp = createMemo(() => {
return <input value={count() + Date.now()} />
}) as any;
return (
<button type="button" onClick={increment}>
Count: {count()} {comp}
</button>
);
}
render(() => <Counter />, document.getElementById("app")!);
Live Demo: https://playground.solidjs.com/anonymous/0cd05f3b-17dd-406d-8296-a2379c95224c
A new input element is returned whenever the signal's value changes.
This pattern works because Solid invokes a function if it is placed inside a JSX element: <button>{fn}</button>
. In React, this outputs the stringified definition of fn
.
Memo returns a new element whenever the signal's value changes. To return a different value, you need to re-run the memo. For that you need to track the signal inside memo. In other words, you need to access the signal inside the memo function or explicitly track a signal.
If you remove the signal, memo will be stuck with its initial value. So, if you need to return a new element, i.e input, but do not need to use signal's value, then you have to explicitly track the value using on
function: https://www.solidjs.com/docs/latest#on
Upvotes: 1
Reputation: 5800
This solution works but is a little bit sloppy, requiring each key or non-reactive prop to be stated twice.
Solution for Example A
export default () => {
const [foo, setFoo] = createSignal();
return <>{createMemo(() => {
foo(); // include key here
return <input .../>;
})}</>;
};
Solution for Example B
export const Weather = () => {
const [day, setDay] = createSignal();
const [time, setTime] = createSignal();
return <>{createMemo(() => {
day(); // include non-reactive props here (to trigger reconstruction)
return <Foo day={day()} time={time}/>;
})}</>
};
Upvotes: 0
Reputation: 1061
Ideally, you want to avoid this kind of thing (recreating components), and opt to update only what is needed. If you are sure you need to re-render, the simple solution is to use the keyed prop of <Show>
:
export default () => {
const [foo, setFoo] = createSignal();
return (
<Show when={foo()} keyed>
{foo => <input .../>}
</Show>
);
};
Upvotes: 3