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

Is it possible to reset the atom to its original default value? #41

Closed
italodeandra opened this issue Sep 13, 2020 · 18 comments · Fixed by #42
Closed

Is it possible to reset the atom to its original default value? #41

italodeandra opened this issue Sep 13, 2020 · 18 comments · Fixed by #42

Comments

@italodeandra
Copy link
Contributor

italodeandra commented Sep 13, 2020

My main goal here is to discuss about it if there's no core functionality or util that does it.

@dai-shi
Copy link
Member

dai-shi commented Sep 13, 2020

Thanks for opening up this discussion. An instant question in mind is what would be the use case.

@italodeandra
Copy link
Contributor Author

italodeandra commented Sep 13, 2020

Thank you for the amazing library.

In my case I have an atom that keeps the status of a component. It has a default value and the only "one" that knows the default value is the atom himself. Currently I have the workaround of keeping a defaultValue const together with the atom like this:

export const answerDefaultValue = 42;
export const answerAtom = atom(answerDefaultValue);

When I need to reset the value I do as follows:

let [answer, setAnswer] = useAtom(answerAtom);
useEffect(() => {
  // [...] imaginary code here
  return () => {
    setAnswer(answerDefaultValue);
  }
}, []);

But I think it would be great to be able to reset it. Something like:

export const answerAtom = atom(42);

let [answer, setAnswer, resetAnswer] = useAtom(answerAtom);
useEffect(() => {
  // [...] imaginary code here
  return () => {
    resetAnswer();
  }
}, []);

// or

let [answer, setAnswer] = useAtom(answerAtom);
let resetAnswer = useResetAtom(answerAtom);
useEffect(() => {
  // [...] imaginary code here
  return () => {
    resetAnswer();
  }
}, []);

Doc of the Recoil version of this use case: https://recoiljs.org/docs/api-reference/core/useResetRecoilState

@dai-shi
Copy link
Member

dai-shi commented Sep 13, 2020

Thanks for the example. There are various ways to implement it.
What I'd like to understand it is how often this feature is required.
If it's say 80% of use cases, it should probably be implemented in core. Otherwise, I'd put it in utils.

As for the code, in addition to yours, I can propose something like this:

import { RESET, resetAtom } from 'jotai/utils'

const resettableAtom = resetAtom(42);

const [value, setValue] = useAtom(resettableAtom)
setValue(RESET)

The thing to consider is it's only for primitive atoms. derived atoms don't have the notion of reset/initialValue. Unlike Recoil, Jotai doesn't distinguish derived atoms from primitive atoms. Recoil doesn't distinguish either. What happens if we try useResetRecoilState without default?

@dai-shi
Copy link
Member

dai-shi commented Sep 13, 2020

Maybe it should be more explicit:

import { useResetAtom, atomWithReset } from 'jotai/utils'

const resettableAtom = atomWithReset(42)

const resetAtom = useResetAtom(resettableAtom)

@italodeandra
Copy link
Contributor Author

I really liked the second one. Feels more natural.
And I don't think this should be core. This kind of use case is personal. I could simply use a variable to store the value (as I'm doing right now) or just set the value to 42 again without using any default value. But I like to treat my code as an art, so I want it just to be as nice as possible.
But I would totally go for an util. I'm for sure not the only one to use this.

@dai-shi dai-shi mentioned this issue Sep 13, 2020
3 tasks
@likern
Copy link
Contributor

likern commented Nov 4, 2020

@dai-shi How to use atomWithReset with atom?

I have this atom which represents tags attached to task (todo-list):

const taskTagsAtom = atom(
  (get) => {
    const tags = get(tagsAtom);
    return tags;
  },
  (get, set, { id, isSelected }: { id: string; isSelected: boolean }) => {
    const tags = get(taskTagsAtom);
    const updatedTags = produce(Array.from(tags), (draft) => {
      const tag = draft.find((it) => it.id === id);
      if (tag !== undefined) {
        tag.isSelected = isSelected;
      }
    });

    // @ts-expect-error
    set(taskTagsAtom, updatedTags);
  }
);

Shown tasks can be filtered by tags. When new task is created it inherits filterTags - current tags which are used for filtering.
If I close task creation component (without actual creation) I want to reset task tags to original filterTags.
How to do this? It looks like atomWithReset() only works with primitive values.

I would expect something like this to work:

const [taskTags, updateTag] = useAtom(taskTagsAtom);
const resetTaskTags = useResetAtom(taskTagsAtom);

@dai-shi
Copy link
Member

dai-shi commented Nov 4, 2020

It looks like atomWithReset() only works with primitive values.

It's true because it only has the notion of default value.

Now, as you use a writable derived atom, it's your responsibility to write a reset handler.

const taskTagsAtom = atom(
  (get) => {
    const tags = get(tagsAtom);
    return tags;
  },
  (get, set, action: { id: string; isSelected: boolean } | 'reset') => {
    const tags = get(taskTagsAtom);
    if (action === 'reset') {
      set(taskTagsAtom, tags); // is this what you want by reset?
      return;
    }
    const { id, isSelected } = action;
    const tags = get(taskTagsAtom);
    const updatedTags = produce(Array.from(tags), (draft) => {
      const tag = draft.find((it) => it.id === id);
      if (tag !== undefined) {
        tag.isSelected = isSelected;
      }
    });

    // @ts-expect-error
    set(taskTagsAtom, updatedTags);
  }
);

@dai-shi dai-shi reopened this Nov 4, 2020
@likern
Copy link
Contributor

likern commented Nov 5, 2020

Finally I wanted this code to work. But it doesn't work.

export const tagsAtom = atom(
  (_) => {
    const tagRepository = getTagRepository();
    return tagRepository.find();
  },
  (
    get,
    set,
    { id, ...newTag }: QueryDeepPartialEntity<Tag> & { id: string }
  ) => {
    const oldTags = get(tagsAtom);
    const newTags = oldTags.map((oldTag) => {
      if (oldTag.id === id) {
        return {
          ...oldTag,
          ...newTag
        };
      } else {
        return oldTag;
      }
    });

    console.log(`dbTagsAtom: oldTags are ${JSON.stringify(oldTags, null, 2)}`);
    console.log(`dbTagsAtom: newTags are ${JSON.stringify(newTags, null, 2)}`);
    // @ts-expect-error
    set(tagsAtom, newTags);

    const tagRepository = getTagRepository();
    tagRepository.update({ id }, newTag);
  }
);

tagsAtom is a persistense atom which loads tags from database.

const taskTagsAtom: WritableAtom<db.Tag[], ActionTaskTags> = atom<
  db.Tag[],
  ActionTaskTags
>(
  (get) => {
    const tags = get(tagsAtom);
    return tags;
  },
  (get, set, action) => {
    if (action.kind === TaskTagsKind.SELECT) {
      console.log(`taskTagsAtom: SELECT WAS CALLED`);
      const taskTags = get(taskTagsAtom);
      const updatedTags = produce(Array.from(taskTags), (draft) => {
        const tag = draft.find((it) => it.id === action.id);
        if (tag !== undefined) {
          tag.isSelected = action.isSelected;
        }
      });
      console.log(
        `taskTagsAtom: updatedTags are ${JSON.stringify(updatedTags, null, 2)}`
      );

      // @ts-expect-error
      set(taskTagsAtom, updatedTags);
    } else if (action.kind === TaskTagsKind.RESET) {
      console.log(`taskTagsAtom: RESET WAS CALLED`);
      // Return original tags
      const tags = get(tagsAtom);
      console.log(`taskTagsAtom: tags are ${JSON.stringify(tags, null, 2)}`);

      // @ts-expect-error
      set(taskTagsAtom, tags);
    }
  }
);

taskTagsAtom represents tags for task. Initially it sets tags from tagsAtom. If taskTagsAtom modifies tags - it should only modify it's local copy, without touching tagsAtom. For these purposes I'm using immer and produce().

On reset taskTagsAtom resets to original (expecting it wasn't modified) tagsAtom.

Unfortunately this code does't work. On reset I see this code returns modified tags.

const tags = get(tagsAtom);

But I don't understand why, I haven't modified tagsAtom, only used get(tagsAtom). Why it gets modified?

@dai-shi
Copy link
Member

dai-shi commented Nov 5, 2020

@likern
set(tagsAtom, newTags); is modifying tagsAtom value. Its read function doesn't have a dependency, so it will only invoked once at initialization.

Basically, you would like to avoid setting self in write. Which means no @ts-expect-error. I'd suggest to make two atoms (one primitive and one derived), but if one really wants to make it one atom atomWithReducer is a good pattern.

I can help completing your code, but you would want to understand it first. And, I guess this is our fault with the lack of good documentation.

@dai-shi
Copy link
Member

dai-shi commented Nov 5, 2020

Basically, what you struggle with is the persistence pattern. docs/persistence.md might help. It only has some snippets though. Suggestions to improve the docs are very welcomed.

@likern
Copy link
Contributor

likern commented Nov 5, 2020

@likern
set(tagsAtom, newTags); is modifying tagsAtom value. Its read function doesn't have a dependency, so it will only invoked once at initialization.

Basically, you would like to avoid setting self in write. Which means no @ts-expect-error. I'd suggest to make two atoms (one primitive and one derived), but if one really wants to make it one atom atomWithReducer is a good pattern.

I can help completing your code, but you would want to understand it first. And, I guess this is our fault with the lack of good documentation.

As I understand, set(taskTagsAtom, newTags); should not update another atom tagsAtom, only it's own value. And I nowhere modify tagsAtom directly - so I presumed it should have it's original value.

I'm still investigating the issue, but it looks like it's a bug of immer itself immerjs/immer#626. And produce have no effect - it just modifies original value.

But that issue leads to another concern. Since atoms are not immutable it's very easy to modify accidentally data directly by reference (without using set(atom, value)) and next call of atom will be change.

To understand / trace where it happened is literally impossible. It can be in any line of code.

I'm lucky that I got that bug early. For example in my case this code

const​ ​updatedTags​ ​=​ ​produce(Array.from(taskTags),​ ​(draft)​ ​=>​ ​{​
        ​const​ ​tag​ ​=​ ​draft.find((it)​ ​=>​ ​it.id​ ​===​ ​action.id);​
        ​if​ ​(tag​ !== ​undefined)​ ​{​
          ​tag.isSelected​ ​=​ ​action.isSelected;​
        ​}​
      ​});​
      ​console.log(​
        ​`taskTagsAtom: updatedTags are ​${JSON.stringify(updatedTags,​ ​null,​ ​2)}​`​
      ​);

not only modifies taskTagsAtom but inherently and accidentally modifies tagsAtom since they share the same object by reference.

Calling

const tags = get(tagsAtom)

right after above code returns modified data.

State becomes as one big global object, and any changes in local state might inherently modify other components all over the place.

@dai-shi
Copy link
Member

dai-shi commented Nov 5, 2020

As I understand, set(taskTagsAtom, newTags); should not update another atom tagsAtom, only it's own value. And I nowhere modify tagsAtom directly - so I presumed it should have it's original value.

I misunderstood something in your code. So, the tagsAtom can simply be like this?

export const tagsAtom = atom(
  () => {
    const tagRepository = getTagRepository();
    return tagRepository.find();
  },
)

Then,

  const tags = get(tagsAtom)

should always return a same value, yes.

@dai-shi
Copy link
Member

dai-shi commented Nov 5, 2020

But that issue leads to another concern. Since atoms are not immutable it's very easy to modify accidentally data directly by reference (without using set(atom, value)) and next call of atom will be change.

An atom config object created by atom() should not be mutated. If you want to make sure if you can Object.freeze(anAtom). You can never change atom values without calling set(anAtom, value).

@likern
Copy link
Contributor

likern commented Nov 5, 2020

@dai-shi Yes, I provided definition of tagsAtom to give better picture. The problem with taskTagsAtom.
taskTagsAtom should initially get the same value as tagsAtom, then it can be modified (tagsAtom should be untouched). And if reset requested taskTagsAtom should get it's initial value which is equal to tagsAtom.

But the problem is that

tag.isSelected​ ​=​ ​action.isSelected

inherently modifies tagsAtom. So if I call const tags = get(tagsAtom) right after that line I get modified value.

@likern
Copy link
Contributor

likern commented Nov 5, 2020

@dai-shi Even though logically I worked with taskTagsAtom's data by calling

const taskTags = get(taskTagsAtom);

@dai-shi
Copy link
Member

dai-shi commented Nov 6, 2020

Oh, now I get it. My apologies for some comments based on my misunderstanding.

So, I guess it's the issue of produce usage or behavior?

tag​.​isSelected​ ​=​ ​action​.​isSelected

Yes, this is mutating an atom value, which is not desired.

To be fair, we have the same issue with useState.

@dai-shi
Copy link
Member

dai-shi commented Nov 6, 2020

Even though logically I worked with taskTagsAtom's data by calling

const taskTags = get(taskTagsAtom);

Not sure what you mean. We agree the issue is mutating an atom value (object) accidentally, no?

Let's open a new issue for this discussion? (this issue is about "reset")

@dai-shi
Copy link
Member

dai-shi commented Nov 6, 2020

@likern #166 created. I can change the title and description if you have a suggestion.

@dai-shi dai-shi closed this as completed Nov 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants