pmndrs / valtio

🧙 Valtio makes proxy-state simple for React and Vanilla
https://valtio.dev
MIT License
9.16k stars 257 forks source link

Unwrapping proxies to check for referential equality? #167

Closed davewasmer closed 3 years ago

davewasmer commented 3 years ago

tl;dr: Is it possible to unwrap both read and write proxies in order to perform referential equality comparisons? Or is there a better way to accomplish this?

Problem

Imagine we have a list of todos:

const state = proxy({
  todos: [
    { title: 'Clean room' },
    { title: 'Go shopping' }
  ]
});

Now imagine we want to remove a todo from the list:

function TodoList() {
  // Get the snapshot (our "read" proxy)
  let snap = useSnapshot(state);

  return (
    <>
      ...
      {snap.todos.map(todo => (
        // Remove the todo from the list of todos
        <button onClick={() => remove(state.todos, todo)}>Remove todo</button>
      ))}
      ...
    </>
  );
}

function remove(todoList, todo) {
  // Uh oh! `todoList` here is coming from our "write" proxy (i.e. `state.todos`),
  // but `todo` is coming from our "read" proxy (i.e. useSnapshot). Since they are
  // different proxy objects, indexOf() will always fail (return -1) since they will
  // always fail referential equality checks (i.e. `writeProxy !== readyProxy`)
  todoList.splice(todoList.indexOf(todo), 1);
}

Alternatives

I could remove the todo from the todos list by looking it up via some method other than referential equality (i.e. I could find it by checking if writeProxyTodo.id === readProxyTodo.id). But I was hoping for a better way, since not all my domain objects have easy, natural keys for comparison.

dai-shi commented 3 years ago

Hi, thanks for using the lib and trying the usage.

I would generally recommend checking primitive id value for comparison, which would be less confusing.

That said, what you want to do can probably be done as follows. Please try:

import { snapshot } from 'valtio';
import { getUntracked } from 'proxy-compare';

function remove(stateTodoList, snapTodo) {
  stateTodoList.splice(snapshot(stateTodoList).indexOf(getUntracked(snapTodo)), 1);
}

As you can see, it relies on the internal knowledge and implementation details

davewasmer commented 3 years ago

Thanks for the quick response! I thought that might be the answer. Bummer, but understandable.

Thanks for the great library!