DEV Community

Gabriel Nordeborn
Gabriel Nordeborn

Posted on

ReScript, React and spread props - it's now possible!

ReScript 10.1 was recently released, bringing a new version of the built in JSX transform in ReScript.

The new JSX transform brings a bunch of new capabilities, and one of those are that you can now do "props spreading".

This post will quickly outline how you can use the new JSX version to bind to APIs that utilize props spreading. We'll assume you know what props spreading is, but if you don't, quickly read through the official React documentation.

More information about the new release, and JSX in ReScript:

Example of using props spreading in bindings - binding useFocus from react-aria

We'll show how to utilize props spreading in ReScript and React by creating bindings to an actual API that uses props spreading, and porting the official JavaScript example code to ReScript from the documentation for that API.

We're going to bind to useFocus from react-aria.

You're encouraged to skim the useFocus documentation page before continuing.

Setting up the bindings

First, let's set up our bindings for useFocus from react-aria.

Reading the documentation, we see both what the function returns, and the config it takes: https://react-spectrum.adobe.com/react-aria/useFocus.html#usage

Writing the return and input types in ReScript from that documentation page looks like this:

type useFocusConfig = {
  onFocus?: JsxEvent.Focus.t => unit,
  onBlur?: JsxEvent.Focus.t => unit,
  onFocusChange?: bool => unit,
}

type useFocusReturn = {focusProps: JsxDOM.domProps}
Enter fullscreen mode Exit fullscreen mode

Notice 2 things here:

  • JsxEvent.Focus.t is the equivalent of FocusEvent in TypeScript.
  • The return type holds one field only, focusProps. That's typed as a JsxDOM.domProps because it's a bag of props that you should apply on the JSX DOM element where you want the focus handlers from useFocus. This is important and is what enables the props spread. More about this further down.

Let's tie together the types with a binding to the hook:

@module("react-aria") external useFocus: useFocusConfig => useFocusReturn = "useFocus"
Enter fullscreen mode Exit fullscreen mode

The finished binding looks like this:

// We're saving this in the file ReactAria.res
type useFocusConfig = {
  onFocus?: JsxEvent.Focus.t => unit,
  onBlur?: JsxEvent.Focus.t => unit,
  onFocusChange?: bool => unit,
}

type useFocusReturn = {focusProps: JsxDOM.domProps}

@module("react-aria") external useFocus: useFocusConfig => useFocusReturn = "useFocus"
Enter fullscreen mode Exit fullscreen mode

Porting the example JavaScript code to ReScript

There! We have the types and the binding to the hook. Let's see if we can implement the same example that the react-aria documentation has. Here's the example, in regular JavaScript, as it's shown in the documentation:

import {useFocus} from 'react-aria';

function Example() {
  let [events, setEvents] = React.useState([]);
  let { focusProps } = useFocus({
    onFocus: (e) =>
      setEvents(
        (events) => [...events, 'focus']
      ),
    onBlur: (e) =>
      setEvents(
        (events) => [...events, 'blur']
      ),
    onFocusChange: (isFocused) =>
      setEvents(
        (events) => [
          ...events,
          `focus change: ${isFocused}`
        ]
      )
  });

  return (
    <>
      <label htmlFor="example">Example</label>
      <input
        {...focusProps}
        id="example"
      />
      <ul
        style={{
          maxHeight: '200px',
          overflow: 'auto'
        }}
      >
        {events.map((e, i) => <li key={i}>{e}</li>)}
      </ul>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Let's implement it in ReScript using our new binding:

module Example = {
  @react.component
  let make = () => {
    let (events, setEvents) = React.useState(() => [])
    // We put the `useFocus` binding code in a `ReactAria.res` file.
    let {focusProps} = ReactAria.useFocus({
      onFocus: _ => setEvents(events => events->Array.copy->Array.concat(["focus"])),
      onBlur: _ => setEvents(events => events->Array.copy->Array.concat(["blur"])),
      onFocusChange: isFocused =>
        setEvents(events =>
          events->Array.copy->Array.concat([`focus change: ${isFocused ? "true" : "false"}`])
        ),
    })

    <>
      <label htmlFor="example"> {"Example"->React.string} </label>
      <input {...focusProps} id="example" />
      <ul style={ReactDOM.Style.make(~maxHeight="200px", ~overflow="auto", ())}>
        {events
        ->Array.mapWithIndex((e, i) => <li key={i->Int.toString}> {e->React.string} </li>)
        ->React.array}
      </ul>
    </>
  }
}
Enter fullscreen mode Exit fullscreen mode

A few things to notice about the ReScript code above:

  • We moved the binding code to a separate file ReactAria.res, and access our binding via ReactAria.useFocus. This is common practice, grouping your bindings in a separate file when it makes sense.
  • It uses ReScript Core for the Array functions.
  • Also notice that there's not a single type annotation in here, but the code is 100% sound/type safe thanks to ReScript's great inference.

As you can see, just like in the JavaScript code, we spread {...focusProps} on the <input />. This works because focusProps has the type JsxDOM.domProps, and that's the same type that <input /> expects its props to have.

This is key to why this works - it's essentially the equivalent of spreading props on a record:

let inputProps: JsxDOM.domProps = {...focusProps, id: "example"}
Enter fullscreen mode Exit fullscreen mode

Wrapping up

Summing up this quick post - whenever you're binding to something that wants you to use spread props on JSX DOM elements, type that as JsxDom.domProps so it matches what JSX DOM elements expect for props.

That's it! Here's a few additional links if you're interested in diving deeper:

Top comments (0)