Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tips] Atoms can be created on demand #5

Closed
dai-shi opened this issue Aug 30, 2020 · 16 comments · Fixed by #144
Closed

[Tips] Atoms can be created on demand #5

dai-shi opened this issue Aug 30, 2020 · 16 comments · Fixed by #144
Labels
has snippet This issue includes code snipppets as solutions or workarounds

Comments

@dai-shi
Copy link
Member

dai-shi commented Aug 30, 2020

Atoms can be created at anytime, like event callbacks and useEffect, or in promises.

Here's a simple Todo example.

import * as React from "react";
import { useState, FormEvent } from "react";
import { Provider, atom, useAtom, PrimitiveAtom } from "jotai";

type Todo = {
  title: string;
  completed: boolean;
};

const TodoItem: React.FC<{
  todoAtom: PrimitiveAtom<Todo>;
  remove: () => void;
}> = ({ todoAtom, remove }) => {
  const [item, setItem] = useAtom(todoAtom);
  const toggleCompleted = () => {
    setItem((prev) => ({ ...prev, completed: !prev.completed }));
  };
  return (
    <li>
      <label>
        <input
          type="checkbox"
          checked={item.completed}
          onChange={toggleCompleted}
        />
        <span style={{ textDecoration: item.completed ? "line-through" : "" }}>
          {item.title}
        </span>
        {item.completed && <button onClick={remove}>Remove</button>}
      </label>
    </li>
  );
};

const todosAtom = atom<PrimitiveAtom<Todo>[]>([]);

const TodoList: React.FC = () => {
  const [title, setTitle] = useState("");
  const [todos, setTodos] = useAtom(todosAtom);
  const addTodo = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const todoAtom = atom<Todo>({ title, completed: false });
    setTodos((prev) => [...prev, todoAtom]);
    setTitle("");
  };
  const removeTodo = (todoAtom: PrimitiveAtom<Todo>) => {
    setTodos((prev) => prev.filter((item) => item !== todoAtom));
  };
  return (
    <ul>
      {todos.map((todoAtom) => (
        <TodoItem
          key={todoAtom.key}
          todoAtom={todoAtom}
          remove={() => removeTodo(todoAtom)}
        />
      ))}
      <li>
        <form onSubmit={addTodo}>
          <input
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            placeholder="Enter title..."
          />
        </form>
      </li>
    </ul>
  );
};

const App: React.FC = () => (
  <Provider>
    <TodoList />
  </Provider>
);

export default App;
@gsimone gsimone pinned this issue Sep 6, 2020
@asfktz
Copy link

asfktz commented Sep 9, 2020

Hi @dai-shi, thanks for releasing this library! 🙏
I wonder, when an atom will be garbage collected?

For example, when you remove a todo, the garbage collector will kick in?

@dai-shi
Copy link
Member Author

dai-shi commented Sep 9, 2020

Hi @asfktz !
First off, let's define terminologies:

  • atom config: The return value of atom(). It's an object which never changes.
  • atom value: The return value of useAtom(...)[0]. It's the real value stored in Provider.

The atom value is removed from the Provider when no components (precisely useAtom hook) use it any longer. This takes care of dependencies: If atomA depends on atomB, atomB value is removed only after atomA is removed.

The atom config is just an object, so it's garbage collected by JS when there' no reference to it.

For example, of you remove a todo, the garbage collector will kick in?

So, yes.

@asfktz
Copy link

asfktz commented Sep 9, 2020

Hmm..! I get it now
Thanks @dai-shi

@dai-shi dai-shi added the has snippet This issue includes code snipppets as solutions or workarounds label Sep 11, 2020
@dai-shi
Copy link
Member Author

dai-shi commented Sep 12, 2020

Here's the basic working ToDo example: https://codesandbox.io/s/jotai-demo-ijyxm

@MeloJR
Copy link

MeloJR commented Sep 18, 2020

Hi! It feels a bit strange to me adding an atom instead of a simple object when you add a todo so I was asking myself if we could use an approach similar to the one used in this recoil video. The idea is to use derived atoms (or selectors in recoil) with a memoized function. Something like this:

const todoWithId = momoize(id => atom(
  get => get(todosAtom).find(todo => todo.id == id),
  (get, set, newData) => {
    set(todosAtom, get(todosAtom).map(todo => todo.id == id ? newData : todo)
  }
)
const TodoItem: React.FC<{
  todoAtom: PrimitiveAtom<Todo>;
  remove: () => void;
}> = ({ id,_ remove }) => {
  const [item, setItem] = useAtom(todoWithId(id);
  const toggleCompleted = () => {
    setItem({ ...item, completed: !item.completed });
  };
  return (
    <li>
      ...
    </li>
  );
};

Does this approach make sense in the Jotai paradigm? Thank you.

@dai-shi
Copy link
Member Author

dai-shi commented Sep 18, 2020

It feels a bit strange to me adding an atom instead of a simple object

Yeah, I see what you mean. You might get crazy without TS types.

Your approach also seems valid. So, you have a big todosAtom atom, and each todoAtom is a derived atom out of it. (wait, how do you add a new atom to todosAtom?)

In this case, you'd need to keep ids somehow, so it would be just easier to use atomFamily. atomFamily is discussed and proposed in #45

@MeloJR
Copy link

MeloJR commented Sep 18, 2020

My goal is to use only "simple" array/objects values for atoms instead of objects created by an atom function. With this assumption, todosAtom's value is an array of todos (with, id, title and completed) and the derived atoms selects the element of the array that they want. The code will look like this:

const filterAtom = atom("all");
const todosAtom = atom([]);
const filteredAtom = atom((get) => {
  const filter = get(filterAtom);
  const todos = get(todosAtom);
  if (filter === "all") return todos;
  else if (filter === "completed")
    return todos.filter((todo) => todo.completed);
  else return todos.filter((todo) => !todo.completed);
});
const todoWithId = memoize((id) =>
  atom(
    (get) => get(todosAtom).find((todo) => todo.id === id),
    (get, set, newData) => {
      set(
        todosAtom,
        get(todosAtom).map((todo) => (todo.id === id ? newData : todo))
      );
    }
  )
);

const TodoList = () => {
  const [, setTodos] = useAtom(todosAtom);
  const remove = (todo) =>
    setTodos((prev) => prev.filter((item) => item !== todo));
  const add = (e) => {
    e.preventDefault();
    const title = e.currentTarget.inputTitle.value;
    e.currentTarget.inputTitle.value = "";
    setTodos((prev) => [...prev, { id: nanoid(), title, completed: false }]);
  };
  return (
    <form onSubmit={add}>
      <Filter />
      <input name="inputTitle" placeholder="Type ..." />
      <Filtered remove={remove} />
    </form>
  );
};

I made a fork of your todo demo here. I'm thinking about this idea because it seems more straightforward for me but it does not fit well with this atom paradigm, does it?

@dai-shi
Copy link
Member Author

dai-shi commented Sep 18, 2020

Yes, it totally does. It's just another practice. Create a big atom (well not that big) and derive smaller pieces. I mean proposing such a new practice is always welcomed.

I just wanted to point out there could be another practice that fulfill your goal. (Hm, but now I'm not sure how to pass initial title with atomFamily.)

@MeloJR
Copy link

MeloJR commented Sep 18, 2020

Yeah, I took a look to atomFamily and it looks interesting. The problem that I detected with the solution I proposed is that I need to update todosAtom's value in order to update one todo, so it rerenders all components that use this atom, directly or indirectly 😓. So, it breaks the idea behind recoil (as far as I understood the message of the video I linked above).

@dai-shi
Copy link
Member Author

dai-shi commented Sep 18, 2020

so it rerenders all components that use this atom, directly or indirectly

You can fix it with React.memo. I think your idea is still valid.

atomFamily

Finally made the version with atomFamily (I needed to use areEqual).
https://codesandbox.io/s/jotai-demo-forked-n8isd?file=/src/App.js

@MeloJR
Copy link

MeloJR commented Sep 22, 2020

Thanks for the example. One question: if you want to load the todos from an API or a localStorage, where will you do this load with atomFamily?

@dai-shi
Copy link
Member Author

dai-shi commented Sep 22, 2020

That's actually a good question. Basically, it doesn't change anything with atomFamily, I suppose, but we'd still need to come up with better ideas for save/load (serialize/deserialize) features. #4

@MeloJR
Copy link

MeloJR commented Sep 22, 2020

Well, it needs to take the completed value of param, at least. Isn't it?

@dai-shi
Copy link
Member Author

dai-shi commented Sep 23, 2020

Well, it needs to take the completed value of param, at least. Isn't it?

@MeloJR Oh, I see. You are right. I missed it.

const todoAtomFamily = atomFamily(
  (param) => ({ title: param.title, completed: param.completed ?? false }),
  null,
  (a, b) => a.id === b.id
)

Something like this 👆 . It's a bit annoying that this basic atoms have to take care of (de)serialization. I'd like to separate it somehow.

const todoAtomFamily = atomFamily(
  (param) => ({ title: param.title, completed: false }),
  null,
  (a, b) => a.id === b.id
)

const serializeTodo = ...
const deserializeTodo = ...
const serializableTodoAtomFamily = atomFamily(
  (param) => (get) => serialize(get(todoAtomFamily(param))),
  (params) => (_get, set, arg) => set(todoAtomFamily(param), deserialize(arg)),
  (a, b) => a.id === b.id
)

You know, we are still challenging about (de)serialization. Ideas are welcome. Eventually, a good pattern will turn into a helper function.

@dai-shi
Copy link
Member Author

dai-shi commented Sep 28, 2020

@MeloJR

I made the todos example with atomFamily to support persistence with localStorage.
https://codesandbox.io/s/react-typescript-forked-eilkg?file=/src/App.tsx

Please check it out and let us know if you find any issues.

@MeloJR
Copy link

MeloJR commented Sep 28, 2020

Great! I have checked it quickly and it looks very interesting. I will code some concepts that I wanted to test following your example. If I find any issue I will post it here. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has snippet This issue includes code snipppets as solutions or workarounds
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants