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

Writeable Atom with derived default value #352

Closed
leifg opened this issue Mar 13, 2021 · 13 comments · Fixed by #379
Closed

Writeable Atom with derived default value #352

leifg opened this issue Mar 13, 2021 · 13 comments · Fixed by #379
Assignees

Comments

@leifg
Copy link

leifg commented Mar 13, 2021

I just recently went through a refactoring from recoil. Everything was pretty straightforward but one use case I couldn't build in jotai.

I have a list of countries I can fetch from the backend:

const availableCountriesApi = atom<Array<Country>>(async () => fetchEntities("/core/countries"))

I built a defaultCountry atom based on this fetch

const defaultCountry = atom<Country>(
  (get) => get(availableCountries).find(country => country.id === "US") || {id: "US", name: "United States"},
)

And now I have a selectedCountry atom which I want to initialize with the default country. But everything I tried resulted either in a type error or a readable only atom:

const selectedCountryState = atom<Country>((get) => get(defaultCountry))
 
const [selectedCountry, setSelectedCountry] = useAtom(selectedCountryState)

setSelectedCountry({id: "DE", name: "Germany"}) // This expression is not callable, Type 'never' has no call signatures.

I think the async fetch is not super relevant, because when I change my defaultCoutry atom to this, I'll run in the same problem:

const defaultCountry = atom<Country>({id: "US", name: "United States"})

So is there a way to build a writeable atom that I can initialize with a derived atom value?

@dai-shi
Copy link
Member

dai-shi commented Mar 13, 2021

Yeah, this use case is not well covered in the current api/docs. We did something similar in jotai/query.

So, what we could do is to use three atoms.

const defaultValueAtom = atom(async (get) => "some default")
const overwrittenValueAtom = atom(null)
const valueAtom = atom(
  (get) => get(overwrittenValueAtom) || get(defaultValueAtom),
  (_get, set, action) => set(overwrittenValueAtom, action)
)

@leifg
Copy link
Author

leifg commented Mar 13, 2021

OK but how do I use that in my code then?

[value, setValue] = atom(valueAtom)

Does that really update the value or do I need 2 variables? One for reading, one for writing?

@dai-shi
Copy link
Member

dai-shi commented Mar 13, 2021

[value, setValue] = atom(valueAtom)

This looks fine.

@leifg
Copy link
Author

leifg commented Mar 21, 2021

OK that works. Thank you!

The bigger question is:

Os this considered a bug/unwanted behavior?

I feel like that use case is quite common.

@dai-shi
Copy link
Member

dai-shi commented Mar 21, 2021

The current behavior is correct, I'd say.
What's missing is the way to create a new primitive-like atom with a default value with other atom values.

I don't think we can support this in core, because we want to keep it minimal, and there's no nice way to add a feature in the current minimal api.

So, the three atom solution is a valid approach currently.
It would be nice to have a uitil as it's quite common.
How about naming it atomWithDefault? We should use symbol EMPTY instead of null. Also allow function update.

@dai-shi
Copy link
Member

dai-shi commented Mar 22, 2021

So, the three atom solution is a valid approach currently.
It would be nice to have a uitil as it's quite common.

Actually, it might be possible to implement it with just one atom, using some undocumented properties.
We already did atomWithReducer similarly, so let me work on it.

@dai-shi dai-shi self-assigned this Mar 22, 2021
@leifg
Copy link
Author

leifg commented Mar 23, 2021

OK it seems like I don't get this to work with atomFamily. My use case is I have a list of languages in a filter dialogue and I want to preselect a certain number of default languages (only default languages are selected):

const familyStoreDefaultFalse = atomFamily<string, boolean>(() => false)

export const selectedLanguages = atomFamily<string, boolean, boolean>(
  (languageId) => (get) => get(familyStoreDefaultFalse(`language-${languageId}`)) || get(defaultLanguageIDs).includes(languageId),
  (languageId) => (_get, set, value) => set(familyStoreDefaultFalse(`language-${languageId}`), value),
)

const defaultLanguageIDs = atom<Array<string>>(["en"])

The selection works for all languages except for English (the default language). The setter of the selectedLanguages is called but afterwards it doesn't call the getter, so the display is never updated.

@dai-shi
Copy link
Member

dai-shi commented Mar 23, 2021

I'm not sure if I fully understand the weird behavior described. Does it only happen with the default thing?

const familyStoreDefaultFalse = atomFamily<string, boolean>(() => false)

export const selectedLanguages = atomFamily<string, boolean, boolean>(
  (languageId) => (get) => get(familyStoreDefaultFalse(`language-${languageId}`)) || languageId === 'en',
  (languageId) => (_get, set, value) => set(familyStoreDefaultFalse(`language-${languageId}`), value),
)

I mean, does this reproduce the issue, or not?

@leifg
Copy link
Author

leifg commented Mar 23, 2021

Sorry, let me clarify the wrong behavior.

This is how the selection looks like:

Screen Shot 2021-03-22 at 17 40 30

No problem, English as the default. But I cannot deselect English, it will always be selected. All other language selections work perfectly fine.

I can also reproduce the issue with the code that you wrote.

@dai-shi
Copy link
Member

dai-shi commented Mar 23, 2021

Okay, I guess this should fix it.

const familyStoreDefaultFalse = atomFamily<string, boolean | null>(() => null)

export const selectedLanguages = atomFamily<string, boolean, boolean>(
  (languageId) => (get) => get(familyStoreDefaultFalse(`language-${languageId}`)) ?? languageId === 'en',
  (languageId) => (_get, set, value) => set(familyStoreDefaultFalse(`language-${languageId}`), value),
)

@leifg
Copy link
Author

leifg commented Mar 23, 2021

Thank you. Now I can select and deselect English but it is NOT enabled by default.

@dai-shi
Copy link
Member

dai-shi commented Mar 23, 2021

Did you change the default to null?

@leifg
Copy link
Author

leifg commented Mar 23, 2021

Oh sorry, didn't see. Now it works. Thank you so much!

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.

2 participants