Search code examples
solid-js

How to update list items inside child components using Solid JS?


I am new to Solid.js, and am not finding existing docs or Q/A to be very helpful.

I have a situation, which I'm sure is a common one, where I want to draw a list of objects as Components that will allow them to be edited. For example, lets say I have:

let list_of_people = [
    {name:'Alice', age:20, address:{...} },
    {name:'Bob', age:22, address:{...} },
    ...
]

I want to draw these with a <Person /> component that will allow name, age to be edited, and which will also include an <Address /> component that will let whatever is going on in the .address property be edited too.

It's my understanding that the way to do this is:

const [people,setPeople] = createStore(list_of_people); 
    // NB: Store instead of Signal because these are 
    // high-level objects with children and grandchildren.

function People(props){
    return <div>
        <For each={people}>{ (person,idx)=>{
            return <Person person={person} setPerson={???}/>
        } }</For>
    </div>
}

function Person(props){
    const person = props.person
    const setPerson = props.setPerson
    return < /*
        ... a bunch of HTML that can display the person's attributes  
        using {person} and set them using {setPerson}, and that also includes
        an <Address/> component that [address, setAddress] objects can be
        passed to.
    */ >
}

But, obviously, setPerson doesn't exist. How can I create it?

setPeople would allow me to get what I want by providing a path from the top-level 'parent' list. But I don't want to have to think about that from within the Person component - the whole point of using a component was to encapsulate that kind of concern away.

I can cheat by doing:

<For each={people}>{ (_person,idx)=>{
    const [person, setPerson] = createStore( _person ); 
    // or createStore(people[idx]) or whatever
    return <Person person={person} setPerson={setPerson}/>
} }</For>

This seems hacky and I have no idea what it does regarding references and memory allocations and the like - ie, whether it would be "safe" if I were to have an object with hundreds of nested subobjects, and I wanted to do this trick for the <Components/> of each of them at every level.

I would like to know how the Solid.js devs intended us to solve these kinds of problems.


Solution

  • If state lives inside the parent component, the only way to update the state from child components is passing the setter function down to the child components as a prop. There are alternatives though, which I will discuss at the end.

    You can pass the whole function or create another function that uses the setter internally.

    If I understand you correctly you are not happy with passing around setPeople as a whole because Person has to set name and age only, proving some form of encapsulation. So, you need to take the second option.

    Here, updatePerson function takes name and age as parameter and update the list using setPeople:

    const updatePerson = (index: number, name: string, age: string) => {
        setPeople(data => {
          const updated = data.map((item, i) => {
            if (i === index) {
              return {...item, name, age};
            }
            return item;
          });
          return [...updated];
        });
      }
    

    Now you can pass the updatePerson function to the Person component:

    <Person name={item.name} age={item.age} updatePerson={updatePerson} />
    

    If you need only the update functionality, that will work, but if you need to add and remove people inside children, you have to provide additional setters.

    Component are there to orginize code, and encapsulation if it keeps an internal state but you need to be more careful about establishing boundaries. The article Thinking in React may help you with that:

    https://react.dev/learn/thinking-in-react

    The article uses React but same rules apply for Solid too.

    This seems hacky and I have no idea what it does regarding references and memory allocations.

    Since you are passing the same function, updatePerson, down to each item, there is no extra memory allocation.

    Using functions for isolating and controlling inputs are not hacky, it is how you do it any programming language.

    A Solid component compiles to function calls that uses native DOM elements. If you are not creating cyclic references there is no problem with nesting components. Plus you can take advantage on onCleanup hook to clean after components when they are unloaded.

    If you are not using very large data set, performance will not be a problem. If you are going to use large data set, you will have the same problem even if you use vanilla JavaScript. In that case you have to use some trickery like caching and virtualization.

    Now, about the alternative methods in case you don't want to pass props down to child components.

    1. You can use JavaScript scopes to access the state directly from the child component. You need to make sure the child component have access to the scope the data lives in.

    You have to options:

    • You can extract the data store into outer scope where child component can access it.

    • Keep both the state and the child component inside the parent component.

    Unlike React components, Solid components can access to their outer scopes.

    1. Provide data through Context Provider and access the data inside the Child component through useContext API.

    https://www.solidjs.com/docs/latest#createcontext

    PS: I see you are still confused. Let me explain briefly.

    First and foremost, using a store or a signal makes no difference since stores use a signal internally. Stores offers some convinces. I will use a signal for simplicity.

    Since data flows from parent to the children, there is no need to create additional signal inside Person component. When we update the data that is kept on the list, it will be updated everywhere. If you use additional signal to keep an item's property, you need to propagate changes to the original list, list that is kept inside the parent component. So, it will introduce an unnecessary overhead.

    I used updateName to update the name field only, in order to simplify the logic. You can use multiple fields, but since you can not update two input fields at the same time because you can edit one input field at a time, so having multiple fields serves no purpose here.

    Can you see the unidirectional data flow. Calling updateName sets a new value for the people signal and the data trickles down to the child components.

    Lastly, I used Index, to keep the input field focused while editing. If you use For, you will lose focus while updating the element. That is because For renders a new DOM element when object reference for an item changes. Buy running updateName, we are creating a new element. Index uses the previously rendered element, but updates the innerText values.

    https://playground.solidjs.com/anonymous/b315a4e5-ff2e-45bb-93db-dc7dbfab0ce3

    import { Component, createSignal, For, Index, JSX } from "solid-js"
    
    const Person: Component<{
      id: number;
      name: string,
      age: number,
      updateName: (index: number, name: string) => void
    }> = (props) => {
      const handleChange: JSX.EventHandlerUnion<HTMLInputElement, Event> = (event) => {
        props.updateName(props.id, event.currentTarget.value);
      };
      return (
        <div>
          <div>Name: {props.name} Age: {props.age}</div>
          <div>Update Name: <input onInput={handleChange} value={props.name}></input></div>
        </div>
      )
    }
    
    interface Person {
      name: string;
      age: number;
      address: { city: string },
    };
    
    export const App = () => {
      const initial: Array<Person> = [
        { name: 'John Doe', age: 20, address: { city: 'LA' } },
        { name: 'Jenny Doe', age: 40, address: { city: 'NY' } },
      ];
    
      const [people, setPeople] = createSignal<Array<Person>>(initial);
    
      const updateName = (index: number, name: string) => {
        setPeople(prev => {
          const updated = prev.map((el, i) => {
            if (i === index) {
              return { ...el, name };
            }
            return el;
          });
          return [...updated];
        });
      }
    
      return (
         <Index each={people()}>
          {(item, index) => (
            <Person
              id={index}
              name={item().name}
              age={item().age}
              updateName={updateName}
            />
          )}
        </Index>
      )
    };