Hooks in ReasonML : an introduction

Learn how to use hooks in your ReasonReact apps !

Benoît Noël du Payrat
Jalgos — A.I. Builders
6 min readAug 7, 2019

--

React 16.8 has finally introduced the concept of Hooks in early 2019, and ReasonReact was quick to adapt it for use in ReasonML apps. Hooks are an exciting feature, that lets us write stateful components and control lifecycles without having to rely on verbose classes.

That’s great, but how does that work in practice ? This article assumes that the reader has some knowledge of ReasonML. To get a basic view of the language and go further, here are some useful resources :

ReasonML documentation
ReasonReact documentation
Setting up a project with BuckleScript
React hooks documentation

  1. Quick presentation of React hooks

1.1 Hooks ? What’s that ?

Hooks are functions that allow us to access and implement the React state logic, without having to create classes. This comes with a few advantages, starting with the most immediately obvious : readability. Declarations are shorter, and smaller hooks are generally more intuitive than classes with large reducer logic.

Aside from that, one of the best uses of hooks is the implementation of custom hooks. Sometimes, we want to share some stateful implementation between components, and that is the exact case where you should use custom hooks. They should try to emulate the syntax of built-in hooks, and start with the prefix `use`, so that React can check that the hooks rules are respected.

Speaking of rules, hooks come with a few very important one : always call hooks at top-level in your component. Do not ever place a hook inside a nested block. This is what guarantees that you will hook correctly into the state (order matters here) ! The good news is that with ReasonReact, we have a compiler and it will catch this kind of issue.

1.2 Hooks in ReasonML ? How ?

Hooks are implemented in ReasonReact much the same as they are in ReactJs, but with a few differences to accomodate for ReasonML. Essentially, these boil down to :

  • Type safety : ReasonML enforces type safety, which is a great advantage in most cases. Here, it guarantees the types of state variables, and forces you to use setter functions to mutate the state — ensuring immutability of the initial state.
  • No polymorphism : this is a consequence of type safety. Symbols have to be unique (per module), and function arguments must be fully declared. For this reason, ReasonML uses multiple declarations for most hooks, with different numbers of arguments.
    Ex : useEffect0, useEffect1… useEffect7.
    This may seem clunky, but it gives stability to the application (no error caused by a wrong number of arguments), and limits the number of arguments you can use so that you are forced to refactor instead of bloating endlessly.
  • Options : ReasonML hates null. Probably with good reason. Options are our solution to deal with this, and they allow us to get the flexibility of nullables without the danger. Do we need a state variable that will be set asynchronously ? Make it an option and set it to None initially. Also, some built-in hooks use options in their declaration. Just make sure that you unwrap your options early to avoid nested switches, and always avoid nested options !
  • Interop and hooks : if you see any of the above as limitations, you can always interop some Js flexibility into your application. But I strongly encourage you to see them as strengths, which they are once you start using them as intended, and avoid introducing some anti-patterns through interop.

2. Hooks detailed

  • useState : access the component state, with a simple getter/setter logic.

This hook forces a pure distinction between setting state and side effects. It should be declared as low as possible in the component hierarchy, and then passed to children as props if necessary. Of course, any change to this variable will trigger a re-render of any component that uses it.

Note : this hook does not provide access to the previous state in setters. If you need to update the state based on previous value, you will need useReducer (see below).

  • useEffect : declare side-effects.

This hook lets you create a clear execution flow, with control over the component life cycle, and clever usage avoids unwanted asynchronization.

You provide useEffect with the state variables that it needs to watch, and the callback that will be executed anytime those variables change. Because ReasonML doesn’t support polymorphism, there are 8 useEffect hooks with various numbers of watched variables : useEffect0-useEffect7. If you need more than 7 variables… you should probably refactor !

Note : be careful of circular dependencies ! If you get to a point where this is a concern, you should consider switching to useReducer (see below).

  • useContext : lets you access a higher level store.

You have to use a Provider to set a value at a high level in your app, and consume it at lower levels.

Context has always been the quick solution, creating a black box that can get bloated pretty fast and isn’t very clear. The good use of this hook is to expose a state variable very widely in your application, or when skipping several levels between the provider and the consumer.

  • useReducer : lets you match actions to state assignments and mutations.

This hook is the closest to the stateful implementations before hooks. It is a natural fit in ReasonML, because of variants and pattern matching. However, a reducer is a large and complex tool, that should be avoided when dealing with simple cases that can be handled by useState and useEffect.

There is still some debate about where the threshold should be between useState+useEffect and useReducer. While this comes down to personal preference to an extent, there are certain situations where useReducer is preferable. These include :
- when state updates depend upon each other (avoid chains of useEffect)
- when state updates depend upon the previous state
- when converting a stateful class to React 16.8

  • useCallback, useMemo : memoization.

Memoization (useMemo) is the concept of keeping in memory the last input and output of a function call. If the next call to that function uses the same input, the hook will return the output without executing the function. This can be very beneficial for optimization.

useCallback is a similar logic, but caches the function instance, instead of the input/output. It is equivalent to a useMemo hook that returns a function instance.

The function provided by useCallback, as well as the values provided by useMemo, can be used as dependencies for useEffect and other hooks, reducing

It is very important that the function is immutable and doesn’t produce side effects ! Side effects belong to useEffect. Any side effect in a memoized function will NOT happen when the hook shortcuts to the output.

  • useRef : create a reference that will retain its value during the component’s lifecycle.

This hook can keep hold of a mutable, or access children methods/properties from the parent. Both of these are, to an extent, anti-pattern in React, and VERY anti-pattern in ReasonReact. If you consider using them, it may well be refactoring time !

There is a legitimate use for useRef though, and it is to access and mutate the DOM properties of a component, together with useLayoutEffect. See below for details on this usage.

  • useImperativeHandle : useRef’s partner in crime.

This is the hook that you would use to access a ref current value in a child from the parent. Don’t.

  • useLayoutEffect : like useEffect, but fires synchronously right after the rendering lifecycle is done, but before useEffect.

This hook is useful for after-effects and reading the final DOM before it is displayed, but for the very large majority of cases, you should use useEffect.

3. Let’s customize our Hooks

Now that we have talked at length about basic hooks, let’s see how we can build our own to share higher-level, stateful logic !

Here we are going to build a localStorage hook, that will let us update localStorage every time a variable is set. Of course, we could do all this inside our component, but then we would have to rewrite the stateful logic whenever we create a component with localStorage access. It’s best to extract stateful logic and make it generic whenever it will be re-used.

Note : remember to always prefix your custom hooks with “use”, so that the ReasonReact compiler will know to check compliance with the hooks rules.

Hooks are very strong tools to develop stateful components. Use them as widely as possible, and help expanding the ReasonML world !

Good reads to go further :

https://blog.bitsrc.io/implementing-react-life-cycles-using-reason-react-hooks-3c1d25de534c
https://www.robinwieruch.de/react-usereducer-vs-usestate/
https://kentcdodds.com/blog/useeffect-vs-uselayouteffect
https://medium.com/free-code-camp/why-you-should-choose-usestate-instead-of-usereducer-ffc80057f815

--

--