Open illusionalsagacity opened 3 months ago
Hi @illusionalsagacity, thanks for your suggestions. I'll have a look into it. The first one seems pretty straight forward. So I would definitely go with that. But that's not an either/or decision. We can simply add both. Perhaps using a Functor could solve the problem with the private key
type.
I experimented a little with this and came up with two solutions.
'key
typeThe first one is actually almost identical to your first suggestion. I just replaced the string
type with a generic 'key
type. Now it works for any kind of key:
type unsubscribe = unit => unit
type syncStorageConfig<'key, 'value> = {
getItem: ('key, 'value) => 'value,
setItem: ('key, 'value) => unit,
removeItem: 'key => unit,
subscribe: option<('key, 'value => unit, 'value) => unsubscribe>,
}
@module("jotai/utils")
external makeWithStorage: (
'key,
'value,
syncStorageConfig<'key, 'value>,
) => Atom.t<'value, Atom.Actions.set<'value>, [Atom.Tags.r | Atom.Tags.w]> = "atomWithStorage"
Here is a (very simplistic) example:
type key = Add(int) | Mult(int)
let a = Utils.AtomWithStorage.makeWithStorage(
Add(0),
1,
{
getItem: (key, initialVal) => {
switch key {
| Add(i) => i + initialVal
| Mult(i) => i * initialVal
}
},
setItem: (_, _) => (),
removeItem: _ => (),
subscribe: None,
},
)
The storage implementation can easily be shared by just storing the config.
This is a little more complicated, but instead of sharing the config record, you can create a new module with a given storage implementation, and then simply reuse the module.
module type Config = {
type key
type value
let getItem: (key, value) => value
let setItem: (key, value) => unit
let removeItem: key => unit
let subscribe: option<(key, value => unit, value) => unsubscribe>
}
module Make = (Config: Config) => {
let _makeConfig = () => {
getItem: Config.getItem,
setItem: Config.setItem,
removeItem: Config.removeItem,
subscribe: Config.subscribe,
}
@module("jotai/utils")
external _make: (
Config.key,
Config.value,
syncStorageConfig<Config.key, Config.value>,
) => Atom.t<Config.value, Atom.Actions.set<Config.value>, [Atom.Tags.r | Atom.Tags.w]> =
"atomWithStorage"
let make = (key, initialValue) => _make(key, initialValue, _makeConfig())
}
Example:
module FakeStorage = {
type key = Add(int) | Mult(int)
type value = int
let getItem = (key, initialVal) => {
switch key {
| Add(i) => i + initialVal
| Mult(i) => i * initialVal
}
}
let setItem = (_, _) => ()
let removeItem = _ => ()
let subscribe = Some((_, _, _) => () => ())
}
module FS = Utils.AtomWithStorage.Make(FakeStorage)
let a = FS.make(Add(0), 1)
@illusionalsagacity which variant would you prefer? I think, the first one is much simpler, but the second one is more elegant with regards to reusabilty.
@illusionalsagacity which variant would you prefer? I think, the first one is much simpler, but the second one is more elegant with regards to reusabilty.
I prefer the functor since it fits nicely with SafeLocalStorage
/ SafeSessionStorage
modules we're using to catch exceptions from accessing storage in private browsing--but I think it'd be nice to also expose the external make
binding for those with a simpler use-case.
Would the plan for async storage be a MakeAsync
functor? Or would a Sync
/ Async
submodule make sense to you?
https://jotai.org/docs/utilities/storage#atomwithstorage
I see two obvious paths for this:
Record Type
Here's an example binding for it
The thing I dislike about this approach is it removes the ability to use a variant for the key without conversion. Or have the key be more complex like so:
Module Function
2 seems nicer for larger applications where you want to share a Storage implementation across several atoms, but it is less approachable / diverges from the JavaScript API.
This could look like:
I like how this makes the storage implementation separate from "how I configure Jotai" but I can see this being considered overly opinionated.
@Fattafatta let me know which approach you like (or even if you have other ideas) and I'd be happy to contribute it