Persistence in ReasonML React Native Apps

Let’s learn how to persist state in a ReasonML RN app by flipping a switch!

Whatt is ReasonML

ReasonML (or Reason as a non-SEO-friendly abbreviation) is an open source project from Facebook that makes OCaml easy to use for JS developers. It does so by offering a friendlier syntax for the language and providing tools that make interop with the JS ecosystem as smooth as butter.

OCaml has a wonderfully powerful type system that supercedes anything Flow or TypeScript can offer, and ReasonML is bundled with tools that can replace almost all of the tools you use to make JS bearable to write on a daily basis (ESlint → ReasonML/OCaml type system, Flow/TS → ReasonML syntax, Prettier → refmt, JS/ES module bundlers → ReasonML/OCaml modules, Babel → BuckleScript, etc).

The great thing about ReasonML being backed by Facebook is that it has first-class support for React projects via ReasonReact, so if you’re tired of fighting Flow errors and are still confused as to how TypeScript fits in the React ecosystem, you should give ReasonML a try!

This guide aims to help you integrate state persistence in your ReasonML React Native apps, and promote creating good offline experiences for those developing ReasonReact apps without the aid of JS-based state management solutions (e.g.: Redux+Redux-Persist).

Getting Started

To get started writing ReasonReact Native apps, I suggest you going the create-react-native-app route:

Assuming you already have the latest version of Node.js installed, run these commands in your terminal:

npm install -g create-react-native-app
create-react-native-app Switcheroo --scripts-version reason-react-native-scripts
cd Switcheroo
npm start
# preferably, I would prefix the prior command with `code . &&`
# to open it in VS Code before running the start script
# to prevent needing to do extra effort to open in your editor

Then, as detailed in the CRNA documentation (as of 1/1/2018):

Install the Expo app on your iOS or Android phone, and use the QR code in the terminal to open your app. Find the QR scanner on the Projects tab of the app.”

Scaffolding the UI

Now that we’ve got our development environment up and running, it’s time to get to the fun part and craft some code!

For starters, you’ll need to create a new file named Switcheroo.re which will live in the src folder next to App.re. This is where we will create the logic for our toggle.

In this file, we set up the basis for a simple RN Switch component, handling state with the reducerComponent building block provided in ReasonReact.

/* Switcheroo.re */
open BsReactNative;
type state = {toggled: bool};
type action =
| SetSwitchValue(bool);
let component = ReasonReact.reducerComponent("Switcheroo");
let make = (_children) => {
...component,
initialState: () => {toggled: false},
reducer: (action, _state) =>
switch action {
| SetSwitchValue(v) => ReasonReact.Update({toggled: v})
},
render: (self) =>
<Switch
value=self.state.toggled
onTintColor="#DD4C39"
onValueChange=((value) => self.reduce(() => SetSwitchValue(value), ()))
/>
};

In App.re, you can replace the insides of the View with your newly written Switcheroo component (code below).

/* App.re */
open BsReactNative;
let app = () =>
<View style=Style.(style([flex(1.), justifyContent(Center), alignItems(Center)]))>
<Switcheroo />
</View>;

Okay sweet, we’ve got a cool little switch we can toggle back and forth, but why doesn’t it stay on when I reopen the app?? Oh yeah, we forgot to persist the state locally; Lets get to it!

Persistencet

To persist state in React Native, you must use the AsyncStorage module. This lets you set serialized data to a long-term data store and retrieve it later, enabling apps to hodl onto their data even if the app was closed and relaunched by the user.

The key word in the section above is “serialized”, which means that your data must be converted into a string format to be saved and revived from a string and parsed back into a live data structure to use it in your app. Let’s call this “encoding” and “decoding” our state.

To do this in ReasonML, we need to call on the powers of bs-json which provides helpers for working with JSON structures.

  • Firstly, you’ll need to run npm i -S bs-json to install the package.
  • Next, add it to your bsconfig.json’s bs-dependencies array.
  • While you’re at it, change your bsconfig.json’s name prop to “Switcheroo”.

When you’re done, bsconfig.json (the config file for the BuckleScript toolchain that powers your ReasonML development) should look like this:

{
"name": "Switcheroo",
"reason": {
"react-jsx": 2
},
"bsc-flags": ["-bs-super-errors"],
"bs-dependencies": ["bs-react-native", "reason-react", "bs-json"],
"sources": [
{
"dir": "src"
}
],
"refmt": 3
}

Now, let’s scaffold our persist function, and set it to run when the component updates state.

open BsReactNative;
type state = {toggled: bool};
type action =
| SetSwitchValue(bool);
let persist = state => {
/* convert state to JSON */
/* set it in RN's AsyncStorage */
()
};
let component = ReasonReact.reducerComponent("Switcheroo");
let make = (_children) => {
...component,
initialState: () => {toggled: false},
reducer: (action, _state) =>
switch action {
| SetSwitchValue(v) => ReasonReact.Update({toggled: v})
},
didUpdate: ({newSelf}) => persist(newSelf.state),
render: (self) =>
<Switch
value=self.state.toggled
onTintColor="#DD4C39"
onValueChange=((value) => self.reduce(() => SetSwitchValue(value), ()))
/>
};

Inside this function, we need to use bs-json to encode our state to JSON and set it to our AsyncStorage location, namely “Switcheroo.state”.

/* Switcheroo.re (partial) */
let persist = (state) => {
/* convert state to JSON */
let stateAsJson =
Json.Encode.(object_([("toggled", Js.Json.boolean(Js.Boolean.to_js_boolean(state.toggled)))]))
|> Js.Json.stringify;
/* set it in RN's AsyncStorage */
AsyncStorage.setItem(
"Switcheroo.state",
stateAsJson,
~callback=
(e) =>
switch e {
| None => ()
| Some(err) => Js.log(err)
},
()
)
|> ignore
};

So now, if you check out your app now and flip the switch a few times, you’ll find out that nothing breaks, (which is great!) but when you refresh the app, you’ll notice that your app still doesn’t pick up where it left off. It’s time for your app to get re-hydrated!

Re-hytdration

To re-hydrate our state, we need to:

  • create a re-hydrate action to update the state of our reducerComponent
  • create a rehydrate function that retrieves the JSON from AsyncStorage and decodes it back into a ReasonML record
  • set the Switcheroo component to call our rehydrate function when the component becomes alive

The code that fulfills the steps listed above exists here:

/* Switcheroo.re */
open BsReactNative;
let storageKey = "Switcheroo.state";
type state = {toggled: bool};
type action =
| SetSwitchValue(bool)
| Rehydrate(state);
let persist = (state) => {
/* convert state to JSON */
let stateAsJson =
Json.Encode.(object_([("toggled", Js.Json.boolean(Js.Boolean.to_js_boolean(state.toggled)))]))
|> Js.Json.stringify;
/* set it in RN's AsyncStorage */
AsyncStorage.setItem(
storageKey,
stateAsJson,
~callback=
(e) =>
switch e {
| None => ()
| Some(err) => Js.log(err)
},
()
)
|> ignore
};
let rehydrate = (self) => {
Js.Promise.(
/* begin call to AsyncStorage */
AsyncStorage.getItem(storageKey, ())
|> then_(
(json) =>
(
switch json {
| None => ()
| Some(s) =>
/* parse JSON, decode it into a ReasonML Record, and reset the state */
let parsedJson = Js.Json.parseExn(s);
let state = Json.Decode.{toggled: parsedJson |> field("toggled", bool)};
self.ReasonReact.reduce(() => Rehydrate(state), ());
()
}
)
|> resolve
)
|> ignore
);
ReasonReact.NoUpdate
};
let component = ReasonReact.reducerComponent("Switcheroo");
let make = (_children) => {
...component,
initialState: () => {toggled: false},
reducer: (action, _state) =>
switch action {
| SetSwitchValue(v) => ReasonReact.Update({toggled: v})
| Rehydrate(s) => ReasonReact.Update(s)
},
didUpdate: ({newSelf}) => persist(newSelf.state),
didMount: (self) => rehydrate(self),
render: (self) =>
<Switch
value=self.state.toggled
onTintColor="#DD4C39"
onValueChange=((value) => self.reduce(() => SetSwitchValue(value), ()))
/>
};

Now, you should have a cool little app that lets you toggle a ReasonML-themed switch and persists its state. Sweet!

For a more complex example of persisting and re-hydrating JSON state in ReasonML, check out the code I wrote for my personal fitness tracker here.

I hope you learned a lot from this post and enjoyed yourself while you read it! For more content, you can follow me on Twitter! That’s the easiest place to contact me and hear about new things I’m doing, new ideas I’m exploring, or new techniques I’m learning!

Have a good one!

— Juwan

Like what you read? Give Juwan Wheatley a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.