Rendering Recursive React Components

Hello! I have some questions about rendering recursive react components.

See this small example:

type rec item<'a> = {
  name: string,
  nested: option<item<'a>>,
}

module Item = {
  @react.component
  let rec make = (~item: item<'a>) => {
    <li className="item">
      <div className="name"> {React.string(item.name)} </div>
      {switch item.nested {
      | Some(nested) => <Item item=nested />
      | None => React.null
      }}
    </li>
  }
}

@react.component
let make = (~items: array<item<'a>>) => {
  <ul className="generic-list">
    {items->Array.map(item => <Item key={item.name} item />)->React.array}
  </ul>
}

Playground link

Results in

[E] Line 12, column 25:
The module or file Item can't be found.
  - If it's a third-party dependency:
    - Did you add it to the "bs-dependencies" or "bs-dev-dependencies" in bsconfig.json?
  - Did you include the file's directory to the "sources" in bsconfig.json?

I tried a few different things like making the recursive make function and using it, making the module recursive but then it asks me for a type signature and when I write it down I’m probably doing it wrong and I get different type errors that I don’t know how to solve.

In the end I managed to solve it by directly using React.createElement. But it’s kind of ugly, so I’m wondering is there a better way to do this?

module Item = {
  @react.component
  let rec make = (~item: item<'a>) => {
    <li className="item">
      <div className="name"> {React.string(item.name)} </div>
      {switch item.nested {
      | Some(nested) => React.createElement(make, {item: nested})
      | None => React.null
      }}
    </li>
  }
}

You can’t reference a module inside of itself, so you need to call the recursive make function instead.

// before
| Some(nested) => <Item item=nested />
// after
| Some(nested) => make({item: nested})

Playground link: ReScript Playground

2 Likes

Brilliant, no need for React.createElement :+1: better!

2 Likes

Just be careful; I don’t think you can have any hooks in a component that works this way.

This example says you can: Recursive components in React: A real-world example - LogRocket Blog

I updated my playground example with a useState and it works: ReScript Playground

I don’t see any mention of this in the React docs, so it might not be best practice, but it does work.

You can certainly use recursive components in react. However:

  1. the blog post relies on actually rendering the components via JSX, which means react is actually cutting new components. By calling make directly, we bypass that.
  2. The playground example is fine, because each call to the component consistently renders at the same depth + width, so the same number of hooks gets called per render. If the number of children actually relies on the state, things get dicey when that state changes.
1 Like

Good news though; we can manually wrap the child renders in createElement to fix that. See this (admittedly hacky) example.

Gotcha. So it turns out the way I solved it with React.createElement is the right way to do it. Thank you all!

3 Likes