Search code examples
javascripttypescriptsolid-js

How to listen to only a certain value of an object in solid-js?


const App: Component = () => {
  const [obj, setObj] = createSignal({
    name: "John",
    age: 30,
  })

  createEffect(
    on(
      () => obj().name,
      (value) => {
        console.log("name", value)
      }
    )
  )
return ()=>(<button onClick={()=> setObj(obj=> ({ ...obj, age: obj.age + 1}))}>+</button>)
}

When I change age, createEffect will also be triggered, I just want to listen on name, similar to watch in Vue3.

function setup(){
  const obj = reactive({
    name: "John",
    age: 30,
  })
  watch(()=> obj.name,(value)=>{ console.log("name", value) })
}

Any good ideas?


Solution

  • Signals are atomic values providing unidirectional data flow, meaning you update a signal by setting a new value. Even if you pass the same properties, you will be setting a new object, because objects are compared by reference in JavaScript, hence triggering an update.

    That being said, there are multiple ways or workarounds to keep track of an individual property of an object stored in signal.

    1. Using a store. createStore uses a proxy internally and provides ways to track individual properties. You can use produce, an Immer inspried API for localized mutations hence update certain properties only.

    2. You can create a memoized signal that tracks an individual property on an object. Here only changing the age re-runs the effect however updating the name has no effect:

    import { render } from "solid-js/web";
    import { createSignal, createMemo, createEffect } from "solid-js";
    
    function App() {
      const [person, setPerson] = createSignal({ name: "John Doe", age: 20 });
      const handleClick = () => setPerson({ name: "Jenny Doe", age: 20 });
    
      const age = createMemo(() => person().age);
    
      createEffect(() => {
        console.log("Age is updated", age());
      });
    
      return (
        <button type="button" onClick={handleClick}>
          {JSON.stringify(person())}
        </button>
      );
    }
    
    render(App, document.getElementById("app")!);
    
    1. Use on utility from solid-js:
    import { render } from "solid-js/web";
    import { createSignal, createEffect, on } from "solid-js";
    import { createStore } from "solid-js/store";
    
    function App() {
      const [person, setPerson] = createStore({ name: 'John Doe', age: 30 });
      const increment = () => setPerson(p => ({ ...p, age: p.age + 1 }));
    
      createEffect(on(() => person.name, () => {
        console.log('Tracking name');
      }));
    
      createEffect(on(() => person.age, () => {
        console.log('Tracking age');
      }));
    
      return (
        <button type="button" onClick={increment}>
          {person.name} {person.age}
        </button>
      );
    }
    
    render(App, document.getElementById("app")!);
    

    This method also works for a signal:

    import { render } from "solid-js/web";
    import { createSignal, createEffect, on } from "solid-js";
    
    function App() {
      const [person, setPerson] = createSignal({ name: 'John Doe', age: 30 });
      const increment = () => setPerson(p => ({ ...p, age: p.age + 1 }));
    
      createEffect(on(() => person.name, () => {
        console.log('Tracking name');
      }));
    
      createEffect(on(() => person().age, () => {
        console.log('Tracking age');
      }));
    
      return (
        <button type="button" onClick={increment}>
          {person().name} {person().age}
        </button>
      );
    }
    
    render(App, document.getElementById("app")!);
    
    1. You can use equals argument in createSignal:
    import { render } from "solid-js/web";
    import { createSignal, createEffect, on } from "solid-js";
    
    function App() {
      const [person, setPerson] = createSignal(
        { name: "John Doe", age: 20 },
        { equals: (prev, next) => prev.age === next.age }
      );
    
      const handleClick = () => setPerson({ name: "Jenny Doe", age: 20 });
    
      createEffect(() => {
        console.log("Age is updated", person());
      });
    
      return (
        <button type="button" onClick={handleClick}>
          {JSON.stringify(person())}
        </button>
      );
    }
    
    render(App, document.getElementById("app")!);
    

    These are out of the box solution but since Solid is pretty flexible and quite permissive you can implement your own logic if you like.