question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Single atom for updating a large list

See original GitHub issue

Is that the best practice?

I’ve seen example like Todolist on the website, it writes

const todoListState = atom({
  key: 'todoListState',
  default: [],
});

function TodoItem({item}) {
  const [todoList, setTodoList] = useRecoilState(todoListState);
  const index = todoList.findIndex((listItem) => listItem === item);

  const editItemText = ({target: {value}}) => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      text: value,
    });

    setTodoList(newList);
  };

  const toggleItemCompletion = () => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      isComplete: !item.isComplete,
    });

    setTodoList(newList);
  };

  const deleteItem = () => {
    const newList = removeItemAtIndex(todoList, index);

    setTodoList(newList);
  };

  return (
    <div>
      <input type="text" value={item.text} onChange={editItemText} />
      <input
        type="checkbox"
        checked={item.isComplete}
        onChange={toggleItemCompletion}
      />
      <button onClick={deleteItem}>X</button>
    </div>
  );
}

function replaceItemAtIndex(arr, index, newValue) {
  return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
}

function removeItemAtIndex(arr, index) {
  return [...arr.slice(0, index), ...arr.slice(index + 1)];
}

function TodoList() {
  const todoList = useRecoilValue(todoListState);

  return (
    <>
      {/* <TodoListStats /> */}
      {/* <TodoListFilters /> */}
      <TodoItemCreator />

      {todoList.map((todoItem) => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  );
}

This makes sense to me, however how is the performance of it if I actually need to create a whole new list every time one element is changed within the todo list. I am having a similar application where I have to update the cells of a spreadsheet, which does not seem so optimal if I were taking this approach.

Is there a better way for solving this problem or how can we improve upon that?

I have something in my mind that is using atomFamily and I wrote it as

type CellValueQuery = {
  workspaceId: string, 
  tableId: string, 
  recordId: string, 
  fieldId: string
}

export const cellValueStates = atomFamily<CellValue, CellValueQuery>({
  key: 'cell-value-states',
  default: null,
});

function Cell({cellValueQuery}) {
  const [cellValue, setCellValue] = useRecoilState(cellValueStates(cellValueQuery));

  const editItemText = ({target: {value}}) => {
    setCellValue(value);
  };

  return (
    <div>
      <input type="text" value={item.text} onChange={editItemText} />
      <input
        type="checkbox"
        checked={item.isComplete}
        onChange={toggleItemCompletion}
      />
      <button onClick={deleteItem}>X</button>
    </div>
  );
}

Am I doing the right thing here?

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:3
  • Comments:12 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
harjiscommented, Dec 29, 2020

I struggled a bit with this issue too. I ended up splitting the atoms this way:

import { atom, atomFamily, selectorFamily } from "recoil";

import { Todo } from "../types";

export const todoIdsState = atom<number[]>({
  key: "todoIdsState",
  default: [],
});

const tempTodo = atomFamily<Todo | null, number>({
  key: "todo",
  default: null,
});
export const todoState = atomFamily<Todo, number>({
  key: "todoState",
  default: selectorFamily<Todo, number>({
    key: "todoState/default",
    get: (todoId) => ({ get }) => {
      const todo = get(tempTodo(todoId));
      if (todo === null) {
        throw new Error(`Fatal error: Todo with id ${todoId} was not found`);
      }
      return todo;
    },
    set: (todoId) => ({ set }, newTodo) => set(tempTodo(todoId), newTodo),
  }),
});

export const todoListFilterState = atom({
  key: "todoListFilterState",
  default: "Show All",
});

The atoms are used like this:

//useTodos.ts
import React from "react";
import { useRecoilCallback, useRecoilState, useRecoilValue } from "recoil";

import { getId, getTodos } from "../../../api/todos";
import { todoIdsState, todoState } from "../atoms";
import { filteredTodoIdsState } from "../selectors";

export const useTodos = () => {
  const [todoIds, setTodoIds] = useRecoilState(todoIdsState);

  const loadTodos = useRecoilCallback(
    ({ set }) => async () => {
      const fetchedTodos = await getTodos();
      const ids = [];
      for (const todo of fetchedTodos) {
        ids.push(todo.id);
        set(todoState(todo.id), todo);
      }
      set(todoIdsState, ids);
    },
    []
  );
  React.useEffect(() => {
    loadTodos();
  }, [loadTodos]);

  const addTodo = useRecoilCallback(
    ({ set }) => (text: string) => {
      const id = getId();
      setTodoIds((prevIds) => prevIds.concat(id));
      set(todoState(id), {
        id,
        text,
        isComplete: false,
      });
    },
    []
  );

  const removeTodo = (id: number) => {
    setTodoIds(todoIds.filter((todoId) => todoId !== id));
  };

  const filteredTodoIds = useRecoilValue(filteredTodoIdsState);

  return {
    filteredTodoIds,
    addTodo,
    removeTodo,
  };
};
// useTodo.ts
import { ChangeEvent } from "react";
import { useRecoilState } from "recoil";

import { todoState } from "../atoms";

export const useTodo = (todoId: number) => {
  const [todo, setTodo] = useRecoilState(todoState(todoId));

  const editItemText = (event: ChangeEvent<HTMLInputElement>) => {
    const value = event.currentTarget.value;
    setTodo((todo) => ({
      ...todo,
      text: value,
    }));
  };

  const toggleItemCompletion = () => {
    setTodo((todo) => ({ ...todo, isComplete: !todo.isComplete }));
  };

  return {
    todo,
    editItemText,
    toggleItemCompletion,
  };
};

The reason why todoState uses selectorFamily is that I wanted to fail hard in cases where invalid primary key was supplied. I guess todoState could also as follows:

export const todoState = atomFamily<Todo, number>({
  key: "todoState",
  default: {} as Todo
});

Entire project can be found here: https://github.com/harjis/react-recoil

Let me know if there is a better way to achieve similar behavior.

0reactions
postbirdcommented, Jan 22, 2021

Yeah so with memoize-one it would some something like this (quick&dirty):

import { selector } from "recoil";

import { todoIdsState, todoListFilterState, todoState } from "../atoms";
import memoizeOne from "memoize-one";

const getCompletedTodoIds = (todoIds: number[], get: any): number[] =>
  todoIds.filter((todoId) => get(todoState(todoId)).isComplete);
const memoizedGetCompletedTodoIds = memoizeOne(
  getCompletedTodoIds,
  (newArgs, lastArgs) => newArgs[0].length === lastArgs[0].length
);

export const filteredTodoIdsState = selector({
  key: "filteredTodoListState",
  get: ({ get }) => {
    const filter = get(todoListFilterState);
    const todoIds = get(todoIdsState);

    switch (filter) {
      case "Show Completed":
        return memoizedGetCompletedTodoIds(todoIds, get);
      case "Show Uncompleted":
        return todoIds.filter((todoId) => !get(todoState(todoId)).isComplete);
      default:
        return todoIds;
    }
  },
});

I also found this. Not sure if it should help with stuff like this

yeah, the feature is useful. but it is not enabled yet. see: #845

I build a local bundle of recoil, enable the feature with gkx.setPass('recoil_suppress_rerender_in_callback');

then the selector return the immutable value such as:

code: https://codesandbox.io/s/objective-matsumoto-7qilr?file=/src/App.js online demo:https://7qilr.csb.app/

const todosState = atom({
  key: uuidv4(),
  default: []
});

let statsInfo = { all: 0, active: 0, completed: 0 };

const todoStats = selector({
  key: uuidv4(),
  get: ({ get }) => {
    const todos = get(todosState);
    const active = todos.filter((todo) => !todo.completed).length;
    if (active === statsInfo.active) {
      return statsInfo;
    }
    statsInfo = {
      all: todos.length,
      active: active,
      completed: todos.length - active
    };
    return statsInfo;
  }
});

it works.

I am looking forward to this feature, and thanks to @drarmstr

Read more comments on GitHub >

github_iconTop Results From Across the Web

neigh_modify command
The indices of neighboring atoms are stored in “pages”, which are allocated one after another as they fill up. The size of each...
Read more >
Nano-sized islands open possibilities for application of single ...
A new method to anchor single atoms of platinum-group metals on nanometer-sized islands allows for efficiently using these expensive metals ...
Read more >
Updating Parts of Documents
The first is atomic updates. This approach allows changing only one or more fields of a document without having to re-index the entire...
Read more >
Upgrading Your Package
Run Atom in Dev Mode, atom --dev , with your package loaded, and open Deprecation Cop (search for "deprecation" in the command palette)....
Read more >
How to Update Members of a Collection with LINQ
Getting to One Line of Code If you're willing to use the ToList method to convert the collection into a List, you can...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found