DEV Community

Rickard Natt och Dag
Rickard Natt och Dag

Posted on • Originally published at willcodefor.beer on

ReScript: FFI basics in React

A foreign function interface (FFI) is a way for a program written in one language to speak with a program written in another language. In ReScript we are creating FFI bindings to JavaScript. We touched on the concept in the post about connecting to localStorage, but in this post we'll learn some of the most common bindings we encounter while developing a React app in ReScript.

React components

react-hot-toast is a small and simple package that displays beautiful notifications (toasts). Here are bindings to its <Toaster> component and toast function.

module Toaster = {
  // JavaScript equivalent
  // import { Toaster } from 'react-hot-toast'
  @react.component @module("react-hot-toast")
  external make: unit => React.element = "Toaster"

  // import ReactHotToast from 'react-hot-toast'
  @module("react-hot-toast")
  external make: t = "default"

  // ReactHotToast.success("Some string")
  @send external success: (t, string) => unit = "success"
}

// Usage in our app
@react.component
let make = () => {
  <>
    <Toaster />
    <button onClick={_ => Toaster.make->Toaster.success("Success!")} />
  </>
}
Enter fullscreen mode Exit fullscreen mode

We start by adding two decorators, @react.component and @module("react-hot-toast").@react.component is the same as the one we use to annotate any React component. @module("react-hot-toast") creates a binding that imports from an external package, in this case react-hot-toast.

We are happy with the defaults of the <Toaster> so we define that the make function takes a unit, which in this case means no props, and returns a React.element. Lastly, we set "Toaster" as it is a named export.

The default export of react-hot-toast is a function that takes a string, but it also has variants for special cases such as success. Using the @senddecorator we can bind to this success function. Calling this takes two steps as we first need to create the Toaster.t parameter and then pass the text we want to display. The resulting code is in theonClick handler.

With props

Most of the times we want to be able to pass some props to the React components we bind to, so here's another example that binds to react-markdown.

module Markdown = {
  // JavaScript equivalent
  // import ReactMarkdown from 'react-markdown'
  @react.component @module("react-markdown")
  external make: (
    ~children: string,
    ~className: string=?,
  ) => React.element = "default"
}

// Usage in our app
@react.component
let make = () => {
  <Markdown>
    "# I'm an H1"
  </Markdown>
}
Enter fullscreen mode Exit fullscreen mode

The difference compared to the binding without props is that the make function accepts:

  • children: string - The children of the component, i.e. the content, is a string which will be parsed as markdown to HTML
  • className: string=? - The ? denotes that the className is an optional property

Also, note that we are using "default" which imports the default export of the package.

React hooks

Binding to a React hook is like binding to any other function. Here's an example of a binding to use-dark-mode.

module DarkMode = {
  type t = {
    value: bool,
    toggle: unit => unit,
  }

  // JavaScript equivalent
  // import UseDarkMode from 'use-dark-mode'
  @module("use-dark-mode") external useDarkMode: bool => t = "default"
}

@react.component
let make = () => {
  let darkMode = DarkMode.useDarkMode(false)

  <div>
    {React.string(darkMode.value ? "Dark and sweet" : "Light and clean")}
    <button onClick={_ => darkMode.toggle()}>
      {React.string("Flip the switch")}
    </button>
  </div>
}
Enter fullscreen mode Exit fullscreen mode

It's not necessary to create a module for the binding, but I think it encapsulates the binding nicer. The hook takes a bool for the initial state and returns DarkMode.t.DarkMode.t is a ReScript record but these compile to JavaScript objects without any runtime costs and are easier to work with than the alternative method using ReScript objects.

Render prop

Render props aren't very common anymore following the introduction of React hooks, but we still encounter them sometimes. Here's an example of binding to Formik.

module Formik = {
  type renderProps<'values> = {values: 'values}

  // JavaScript equivalent
  // import { Formik } from 'formik'
  @react.component @module("formik")
  external make: (
    ~children: renderProps<'values> => React.element,
    ~initialValues: 'values,
  ) => React.element = "Formik"
}

type form = {name: string}

@react.component
let make = () => {
  <Formik initialValues={{name: "React"}}>
    {({values}) => {
      <div> {React.string(values.name)} </div>
    }}
  </Formik>
}
Enter fullscreen mode Exit fullscreen mode

Now it's getting more complex and it's the first time we are using a type parameter aka generic! We start by defining a React component for <Formik>. It accepts two props:

  • children: renderProps<'values> => React.element - The child should be a function that gets the renderProps record (with the generic'values) and returns a React.element
  • initialValues: 'values - A record with the initial data of the form

We define the type of the values in type form and pass a record of that type to Formik's initialValues prop. After this, the type of values in the render prop will automatically be of the type form since it uses the same type parameter as initialValues in our binding.

Note: Formik has multiple APIs for creating forms and this is not a fully functioning binding. It's just to demonstrate the use of render props.

Global variables

Sometimes we need to reach out and connect to a global variable. This is exactly what we did in the previous post about connecting to localStorage. I'll include the code example here but if you want to learn more about it see the previous post.

@val @scope("localStorage") external getItem: string => Js.Nullable.t<string> = "getItem"
@val @scope("localStorage") external setItem: (string, string) => unit = "setItem"
Enter fullscreen mode Exit fullscreen mode

Top comments (0)