til / Rescript - connect to localstorage using ffi and functors
While creating my snippets website I needed to store a value for how the user wants to copy the snippet. To store the value I wanted to use localStorage
which is very straight-forward to bind to using ReScript’s foreign function interface (FFI).
Writing these bindings is usually one of the harder parts when getting started with ReScript, but the help is getting better with both the syntax lookup and the docs.
@val @scope("localStorage") external getItem: string => Js.Nullable.t<string> = "getItem"
@val @scope("localStorage") external setItem: (string, string) => unit = "setItem"
This is all we need to do to bind to localStorage
’s getItem
and setItem
functions. Let’s walk through the parts of one of them.
@val
- Bind to a global JavaScript value@scope("localStorage")
- Set the parent scope to “localStorage”external getItem
- An external value and what we want to call it (getItem
) on the ReScript end.string => Js.Nullable.t<string>
- The function takes onestring
, the key inlocalStorage
, and returns astring
ornull
."getItem"
- Tells the compiler what the name of the function is on the JavaScript end. This works together with the scope to bind tolocalStorage.getItem
The return value of getItem
isn’t very easy to work with as it could potentially be any string
or null
. We can improve this by using a functor, like we previously used for React Context, which returns a nice custom hook that uses variants instead.
// Storage.res
module type Config = {
type t
let key: string
let fromString: option<string> => t
let toString: t => string
}
We start by creating a module type
that tells us what the module that is passed in needs to contain.
t
is the variant we are transforming thestring
tokey
is a what the value should be stored as inlocalStorage
fromString
andtoString
handle the conversions of the value from JavaScript land to ReScript and vice versa.
// Storage.res
// module type Config here...
module Make = (Config: Config) => {
let useLocalStorage = () => {
let key = Config.key
let (state, setState) = React.useState(() => getItem(key))
let setValue = value => {
setItem(key, value->Config.toString)
setState(_ => getItem(key))
}
(state->Js.Nullable.toOption->Config.fromString, setValue)
}
}
We then add a Make
module that accepts another module (very meta) of the Config
type we created above. This returns a useLocalStorage
hook that wraps the getting and setting using our configuration module.
// FruitBasket.res
module Fruit = {
type t = Apple | Banana
let key = "fruit"
let fromString = value =>
switch value {
| Some("apple") => Apple
| Some("banana") => Banana
| Some(_)
| None =>
Apple
}
let toString = value =>
switch value {
| Apple => "apple"
| Banana => "banana"
}
}
module FruitStorage = Storage.Make(Fruit)
@react.component
let make = () => {
let (fruit, setFruit) = FruitStorage.useLocalStorage()
let toggleFruit = _ => {
switch fruit {
| Apple => Banana
| Banana => Apple
}->setFruit
}
<div>
{fruit->Fruit.toString->React.string}
<button onClick={toggleFruit}> {React.string("Toggle fruit")} </button>
</div>
}
This is the final part where we are creating a storage setup and a component. We first create a Fruit
module that implements all the parts of our Config
module. If we miss something in our implementation of the module the compiler will complain when we try to create a Storage
in the next step. Note that fromString
takes care of handling any unknown strings and null
values, for those cases we always get Apple
.
To get storage for our fruits we create a FruitStorage
using module FruitStorage = Storage.Make(Fruit)
. This contains our useLocalStorage
hook that we can use in our component to both get the current fruit and update the stored value. Now we have a great way of persisting if we either have an apple or a banana!
If you want to see the implementation I ended up with for my snippets, which is very similar to what we’ve created, you can take a look at these two files in the repo, Storage.res and SnippetCode.res.