Frank Condezo

How to render react components with rails

Learn how to work with react components in rails views.

Beginners have a lot of questions about how to work with react and rails correctly, specifically how to render the react components in rails views and how to pass the data on to the components.

In this post I am going to show you some interesting ways to tackle this problem:

I assume you are using webpacker with react in your rails project

Imagine we have this Book component, which is in charge of rendering a collection of books:

# app/javascript/components/Books.js

function Books({ books }) {
  return (
    <ul>
      {books.map((book) => (
        <li key={book.id}>{book.name}</li>
      ))}
    </ul>
  );
}

And you have a rails view:

# app/views/home/index.html.erb

<% content_for :javascript do %>
  <%= javascript_packs_with_chunks_tag "book-home" %>
<% end %>

<%= content_tag(
  :div,
  nil,
  id: "book-list",
  data: { books: @books }
)%>

Interesting code, right? we need to look more closely.

<% content_for :javascript do %>
  <%= javascript_packs_with_chunks_tag "book-home" %>
<% end %>

This piece of code tells rails that we want to import the book-home pack, we don't import the component directly.

If you are wondering why it uses javascript_packs_with_chunks_tag helper and not javascript_pack_tag, the answer is we are using the webpack 4 or a newer version, this webpack creates html tags for pack and all the dependent chunks, you can read more here

<%= content_tag(
  :div,
  nil,
  id: "book-list",
  data: { books: @books }
)%>

In this code we are using data attributes to save server data

Then we need to create a book pack:

app/javascript/packs/book.js

import React from "react";
import ReactDOM from "react-dom";
import Books from "../components/Books";

document.addEventListener("DOMContentLoaded", () => {
  const $bookNode = document.querySelector("#book-list");
  const books = JSON.parse($app.dataset.books)

  if ($bookNode) {
    ReactDOM.render(<Books books={books} />, $bookNode);
  }
});

As you can see, we parse the books collection, then we get the props from data attributes, after that we ensure that div with book-list id exist, and finally we render the component and pass the props to the Book component.

Doing it this way is cool, but in the long term we will repeat this process in all future packs; we can avoid this by creating this nice utility:

# app/javascript/lib/mountComponent.js

import React from "react";
import ReactDOM from "react-dom";

const defaultParser = (dataset) =>
  Object.entries(dataset).reduce((props, [currentKey, currentValue]) => {
    try {
      props[currentKey] = JSON.parse(currentValue);
    } catch (error) {
      props[currentKey] = currentValue;
    }
    return props;
  }, {});

const mountComponent = (
  selector,
  Component,
  mapDataToProps = defaultParser
) => {
  const mount = (element) => {
    const props = mapDataToProps(element.dataset);
    ReactDOM.render(<Component {...props} />, element);
  };

  document.querySelectorAll(selector).forEach(mount);
};

export default mountComponent;

So with this, we can update our current book pack like this:

# app/javascript/packs/book.js

import Books from "../components/Books";
import mountComponent from "../lib/mountComponent";

const $bookNode = document.querySelector("#book-list");
mountComponent($bookNode, Books)

What happens if your props have a different name to data-attributes? you can pass your own customized function to get the props:

import Books from "../components/Books";
import mountComponent from "../lib/mountComponent";

const $bookNode = document.querySelector("#book-list");
mountComponent($bookNode, Books, (data) => ({
  initialBooks: data.books,
}))

Conlusion

I know there are others ways to improve this like using third packages or work with json scripts(future post), but in my opinion this is a really simple approach since you don't have to do extra work and you know how this works in depth.