David Callanan
David Callanan

Reputation: 5800

How to create new component every time signal updates in Solid-JS?

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

Answers (3)

snnsnn
snnsnn

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

David Callanan
David Callanan

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

FoolsWisdom
FoolsWisdom

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

Related Questions