Nested state handling in SolidJS components with Immer's "produce"

117 Views Asked by At

I have a SolidJS application, and I'm storing a large JSON object that nests other objects. I'm using Immer to generate patches for undo and redo actions. (well, technically I'm storing a class with nested classes several layers deep, but Immer handles them just fine, so this seems like a trivial detail.)

I'd like to be able to pass a subset of that big object to components, say bigObject.someProp[0], and allow the component to modify and access that instead of having to access bigObject directly. Something like this:

function createImmerSignal<T>(value: T) {
  const [get, set] = createSignal(value);

  function newSet(callback: (draft: Draft<T>) => void) {
    const newVal = produce(get(), callback, (redo, undo) => {
      // Undo and redo logic, would modify some context variable
    });
    set(newVal as Exclude<T, Function>);
  }

  // createNested is what I'm looking to implement
  return [get, newSet, createNested];
}

type Company = {
  name: string;
  developers: {
    name: string;
    someOtherProp: string;
  }[];
};

function CompOne(prop: { origCompany: Company }) {
  const [company, setCompany, createNested] = createImmerSignal<Company>(origCompany);

  // I'm not sure what a good syntax for this would be, or how to even get this functional. This syntax is what I've come up with:
  const dev = createNested(company => company.developers[0]);

  return <>
    <CompTwo dev={dev} />
  </>
}

function CompTwo(props) {
  // createNested can be used indefinitely
  const [dev, setDev, createNested] = props.dev;

  setDev(draft => {
    // This would update company.developers[0].someProp
    draft.name = 'Darles Nemeni';
  });

  return <></>;
}

I'm not entirely sure how to implement this, especially in a way that would respect types. I've tried using functions (company => company.developers[0]) to select the subset of company:

function newNested<U>(sel: (orig: T | Draft<T>) => U) {
  return (fn: (draft: Draft<U>) => void) => set(
    produce(get(), draft => {
      // vvvvvvv This does not work, as you cannot assign like that
      sel(draft) = produce(sel(draft), draft => fn(draft))
    }) as Exclude<T, Function>
  )
}


newNested(company => company.developers[0])

However, I cannot assign a value to sel(draft), unless I access a property on the parent, and I don't believe there's a clean way to do that.

My only idea for fixing that above (and I'm not sure if it's a good idea or would work) would be to somehow use a proxy to detect which properties are being accessed and recursively create a string of accessors to then use on draft, but that hardly seems efficient or clean.

To reiterate my question: What would be the best and cleanest way of designing this?


Semi-related question: I'm struggling to describe this problem, so if anyone has a better title, please let me know.

1

There are 1 best solutions below

6
snnsnn On

Best course of action is getting rid of createImmerSignal and use a store: https://www.solidjs.com/docs/latest#store-utilities. Store provides Immer like API and uses a Proxy to rewire interactions to provide reactivity. It has many utilities to help you interact with the stored value and since store produces a reactive value, you can derive a subset of it directly without breaking a sweat.

If you insist on creating your own store then you can take a look at its implementation to get started.