how to use preact signals with nested state (is it compatible with immer?)

381 Views Asked by At

Ideally I'd like to have a state management setup which allows me to:

  • have an arbitrarily nested state. For example, this could be a valid entry in the state users[0].vehicle.engine.throttle
  • have an arbitrarily nested component tree. For example, there could be a slider which corresponds to the above throttle value deep down in some menu.
  • components must only rerender when their part of the state changes

I've read through the release blog post and docs for preact signals. The concept seems very exciting and I appreciate the clean code.

But it's not clear to me how to represent nested state with preact signals. As far as I understand, when your component looks at signal.value it will rerender whenever the value changes. Let's say I have this user

{
    name: "John",
    age: 34
}

I'd like to consume the name and age in two different components, let's call them NameDisplay and AgeDisplay. How can I prevent AgeDisplay from re-rendering when I change the name from John to James?

As far as I understand, preventing this re-render is only possible by keeping name and age in different signals. I'd have to flatten the state and extract every entry into an individual signal.

But what if I'm dealing with a list of users?

{
    users:[
    {
        name: "John",
        age: 34
    },
    ...
    ]
}

I don't see how this could be flattened. Now I'd have a users signal, and whenever any of the users is changed, all user components re-render.

The wishlist defined at the beginning of the question is definitely doable though. Here's a small hacky prototype with zustand + immer. This code will display two users and allow you to update them independently from each other. Editing the name of one of them doesn't cause the other to rerender (I've checked it with the react devtools and highlighting component re-renders).

store.js (contains nested state with a list of users, and uses immer to update one field deep in the state)

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

export const useUsers = create()(
  immer((set) => ({
    users: [
        {
            name: "John",
            age: 42,
            id: "1"
        },
        {
            name: "Jane",
            age: 21,
            id: "2"
        },
    ],
    renameUser: (id, name) =>
      set((state) => {
        state.users.find((user) => user.id === id).name = name;
      }),
  }))
);

User.js (subscribes to one specific user, listens only to updates to this user)

import React from "react";
import { useUsers } from "./store";
import { useShallow } from "zustand/react/shallow";

export default function User(props) {
  const { id } = props;

  const user = useUsers(
    useShallow((state) => state.users.find((user) => user.id === id))
  );
  const rename = useUsers((state) => state.renameUser);
  return (
    <div>
      <h1>{user.name}</h1>
      <h2>{user.age}</h2>
      <input
        type="text"
        value={user.name}
        onChange={(e) => rename(id, e.target.value)}
      />
    </div>
  );
}

App.js (small wrapper to display a list of users)

import React from 'react';
import { useUsers } from './store'
import { useShallow } from "zustand/react/shallow";
import User from './User'

function App() {
  const users_ids = useUsers(useShallow((state) => state.users.map((user) => user.id)))

  return (
    <div className="App">
      {users_ids.map((id) => <User key={id} id={id} />)}
    </div>
  );
}

export default App;

Would it be possible to set up something like this with preact signals?

0

There are 0 best solutions below