Rails, React, Redux and SEO

Reactjs logo

My take on using React and redux in RubyOnRails and my experience with Google indexing Javascript.

Rails uses Sprockets to transform javascript, css and other assets for the various environments. Sprockets makes it really easy to put everything together and handles transforming, cache busting, combining, minifiying, e.g. all the tasks you need, but without having to use watchers and more or less complicated configuration files like with grunt, gulp, webpack or others. That comes with a price. In the frontend world, especially with Javascript, there is a lot going on, and sometimes you will be hitting a wall when sprockets itself is just not flexible enough for the task. The React ecosystem everybody seems to use is es6, Babel and webpack. While there is even a React gem out there, I am not a big fan of the “one gem for everything” approach.

Update 3/30/2017: These workarounds will not be neccessary with the release of Rails 5.1! Wohoooo!

What I ended up using for React development is the Rails Browserify Gem. It is the entry card to npm for the Rails developer, and it makes it easy to use NPM packages and Babel transforms for ES6.

    config.browserify_rails.commandline_options = "-t babelify -t envify"
    config.browserify_rails.source_map_environments << "development"

So now that the door to Javascript heaven is open, which of the thousands of libraries to use? When it comes to React and Flux, the most popular solution at the moment is Redux. It has the concept of having one big state object at a time, which can only be manipulated by reducers, that are triggered by actions. It also puts an emphasize on functional programming. If you are coming from MVC like Rails or Backbone, it is not that easy to get your head around first.

Now that React is integrated technically, how to use it in your Rails app? Several possibilities:

1. Use Rails purely as an API

This is the most costly variant. You would use a node.js app for the frontend, that handles the serverside rendering of the React-Redux app and use Rails purely as an api. This is a really time consuming process and in my opinion only makes sense for highly dynamic apps.

2. Replace some parts with React

That is the road I took. For my app Boutiquiee I use a pure React-Redux start page and React infinite lists. The rest is still good old Rails views. For me that is the best of both worlds. React for the dynamic parts, and Rails for the more static elements.

Boutiquiee Screen

So when you start with ES6 with Browserify, you first have to do one require:

In your JS file build by sprockets:

    require('./home/main')

Then in your main file you can use all the funky stuff.

    import thunkMiddleware from 'redux-thunk'
    import { createStore, applyMiddleware } from 'redux'
    import {setModel, receiveProducts} from './actions'
    import rootReducer from './reducers'
    import React from 'react'
    import { render } from 'react-dom'
    import { Provider } from 'react-redux'
    import App from './components/App'

    ...

    const store = createStore(
      rootReducer,
      applyMiddleware(
        thunkMiddleware
      )
    )

    function init() {
      render(
        <Provider store={store}>
          <App />
        </Provider>,
        document.getElementById('app')
      )
      store.dispatch(receiveProducts(window.bouti.products, 1, 'popular'))
      store.dispatch(setUser(window.bouti.user))
      store.dispatch(getCategories())
    }
    init()

So as you can see, I am giving the initial data to Redux through Rails and the window object. I hoped that Google would appreciate my efforts and index at least the initial markup. While I can confirm that Google can interpret Javascript, there seems to be some limitations. I am still investigating which limitations that are.

Google indexes everything but without the initial data. I experimented with the call order, called render first or last, but google does not render the products. Hints welcome 🙃

Update 3/30/2017: Found some time looking into it and actually succeeded after some tries. There must be some limitation in terms of call stack depth or similar, after setting the initial store state when using redux’s createStore instead of using a store.dispatch, the product list magically showed up. No ajax was going on before and after. This is the recommended way anyways, so no hacks neccessary.

    const store = createStore(
      rootReducer,
      {
        products: {
          items: window.bouti.products,
          page: 1,
          action: 'popular'
        }
      },
      applyMiddleware(
        thunkMiddleware
      )
    )

    render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('app')
    )
    store.dispatch(setUser(window.bouti.user))
    store.dispatch(getCategories())

Before Google said it would start crawling Javascript, the only solution was some kind of serverside rendering. While there are some bizarr solutions out there, like crawling your page yourself, now here comes one big plus for React.

This is one advantage of React over its competitors, for me Polymer or webcomponents in general. The official Rails React Gem even is able to render React components and mounting them. This is another solution for replacing some parts of your app with React components, and maybe exactly what is right for your project. There is also hypernova from the folks at Airbnb. But with Redux it does not help that much, because you have to render the app with an initial state.

That is where nodejs comes into play. As you see, the stack grows if you want to have a full seperate frontend-backend-thing app. This node server renders a simple counter Redux-React app, using Express and Jade.

import qs from 'qs'
import path from 'path'
import Express from 'express'
import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import counterApp from './reducers'
import App from './containers/App'
import { renderToString } from 'react-dom/server'
import { fetchCounter } from './api/counter'

const app = Express()
const port = 3000

app.use(Express.static('static'))
app.use(handleRender)

app.set('views', './views')
app.set('view engine', 'jade')


function handleRender(req, res) {
  const params = qs.parse(req.query)
  fetchCounter(apiResult => {
    const counter = parseInt(params.counter, 10) || apiResult || 0

    let initialState = { counter }

    // Create a new Redux store instance
    const store = createStore(counterApp, initialState)


    // Render the component to a string
    const html = renderToString(
      <Provider store={store}>
        <App />
      </Provider>
    )

    res.render('index', { html: content, state: store.getState()});
  })
}


app.listen(port)

If you are going full js for the frontend, you will of course need a router, probably the React router. Now every search engine can see your valuable content.

Conclusion

Using react with Rails is easy enough to do and will get even easier with the release of Rails 5.1. Depending if your team is more frontend or backend centered, you can choose between the Loved/Hated Turbolinks approach of rendering on the server and replacing in the frontend, or the render everything JS and use Rails as an API, or something in between. Nothing is dead or crushes anything, you can use what you like, best of both worlds, together ;) Aaaawwwww


Comments