react-rails
react-rails copied to clipboard
Using Redux Provider: id mismatch?
Steps to reproduce
Render any react component with a Provider on the server (set prerender as true)
Expected behavior
The component should be generated from the server, then UJS should mount the client version
Actual behavior

System configuration
Sprockets or Webpacker version: No version (Latest) React-Rails version: No Version (Latest) Rect_UJS version: React-Rails specified. Rails version: 5.1.4 Ruby version:2.5.0
I'm attempting to pre-render a component with a Provider, but it has issues when UJS tries to automatically mount the client version on top of it.
- My setup absolutely works outside of redux. Rendering any normal component works.
react-on-railsis not only too far of a departure, but it's too battery included, no migration instructions, and I'd like to depend on this more official release.
Render any react component with a Provider on the server (set prerender as true)
I'm also rendering react components with a redux Provider on the server with rails 5.1.4 and ruby 2.5.0, but it doesn't reproduce.
When I have time I'll try add a redux example branch to react-rails-example-apps. If it works then I'll add it as doc, if not then I'll mark this as a bug. Might take me some time to get around to it however.
add a redux example
+1. Although Redux has nothing to do with this gem, many users of this gem seems to be struggling to use Redux with this gem.
FYI. My usage example of Redux with this gem is below:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def show
render component: 'Post', props: { post: { id: params[:id], body: 'foo bar' } }, prerender: true
end
end
// app/javascript/components/Post.js
import React from "react"
import { Provider, connect } from 'react-redux';
import { createStore, combineReducers } from 'redux';
function post(state = null, action) {
return state;
}
const reducer = combineReducers({ post });
function PostComponent({ id, body }) {
return (
<div>
<h1>Post</h1>
<div>{id}</div>
<div>{body}</div>
</div>
);
}
function mapStateToProps(state) {
return state.post;
}
const PostContainer = connect(mapStateToProps)(PostComponent);
export default function Post(props) {
const store = createStore(reducer, props);
return (
<Provider store={store}>
<PostContainer />
</Provider>
);
}
$ curl localhost:3000/posts/42
<!DOCTYPE html>
<html>
<head>
<title>ReactRailsWithReduxExample</title>
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="rdTZeqYi1qXrPofTTl/7yEGe9SnRe2zInWSPbuk2nrU/WmcKLaMZmRImvMfw3L2+lSpWwI8fEWzy2PvuShcAEA==" />
<script src="/packs/application-ad21c81663060032e2ac.js"></script>
</head>
<body>
<div data-react-class="Post" data-react-props="{"post":{"id":"42","body":"foo bar"}}" data-hydrate="t"><div data-reactroot=""><h1>Post</h1><div>42</div><div>foo bar</div></div></div>
</body>
</html>
repo: https://github.com/ttanimichi/react_rails_with_redux_example
<3 @ttanimichi thank you for that example!
Yes I've noticed there are a fair few people who use react-rails as the entry point for transitioning from rails to react, so there are a lot of cases where it's absolutely nothing to do with react-rails and everything to do with Webpack or React themselves but I do still try to help when and where they can.
There is probably a real issue where react-rails doesn't define exactly how little it does for people so it's not possible for them to tell when it's an issue using the gem or an issue using something deeper. I've added examples and Wiki pages where possible to help out for those that do read them, not sure how much further to go as beyond a certain point they're better reading the source materials for tools mentioned.
@BookOfGreg You're so kind. Don't get burnt out over this 🙂
thank you @BookOfGreg & @ttanimichi for the all the responses
This was actually due to user error. We were generating unique id's somehow in the components, causing a mismatch.
Here's a question though: https://redux.js.org/recipes/server-rendering
Based on what redux says on their official site, you always want to use the exact store generated from the server side, on the client. We were able to do this by doing the following:
// server-rendering.js
import store from './store'
global.setup = function() {
return JSON.stringify(store.getState())
}
--------------------
// controller.rb
// gon is just a convenience utility for window... window.gon to be specific
gon = react_rails_prerenderer.context.eval('self.setup()')
------------------
// store.js
configureStore(reducers, JSON.parse(window.gon.state))
Let me know what you think?
@reywright
- I must set up the Redux store server-side first - and it's not empty, this a list of companies.
- Only then I render my component server-side - because the component takes the data out of the store.
How do you do that? I tried react_rails_prerenderer.context.eval to set up the Redux store - does not work as wanted - the component gets rendered first. :(
Here's my code: lib/appstate_renderer.rb
module React
module ServerRendering
class AppstateRenderer < BundleRenderer
def before_render(component_name, props, prerender_options)
super(component_name, props, prerender_options)
companies = Company.all
'global.showCompanies(' + companies.to_json + ')'
end
end
end
end
app/javascript/packs/server_rendering.js
// By default, this pack is loaded for server-side rendering.
// It must expose react_ujs as `ReactRailsUJS` and prepare a require context.
import store from '../redux/store';
var componentRequireContext = require.context("components", true)
var ReactRailsUJS = require("react_ujs")
ReactRailsUJS.useContext(componentRequireContext)
global.showCompanies = function(companies) {
store.dispatch({
type: 'COMPANIES_LIST',
companies: companies
})
}
Currently, the Redux store is rendered server-side and the list of companies is also rendered server-side (taking the data out of store). But I can't transfer the store to the client for the moment. Any ideas?
Besides, companies should not be queried inside lib/appstate_renderer.rb - but I haven't yet found a better approach.
@ttanimichi
How about transferring the Redux store client-side and hydrateing it there?
I don’t understand what you mean. Hydration is a matter of React’s SSR and it’s not related to Redux stroe.
You can pass props to a component by using react_component and from the props you can create a store in client side. You don’t need to hydrate components by yourself. This gem hydrates components automatically.
Did you try this example? https://github.com/reactjs/react-rails/issues/878#issuecomment-368481036
@ttanimichi
Hydration is a matter of React’s SSR and it’s not related to Redux stroe.
By 'hydration' I meant this: https://redux.js.org/recipes/serverrendering#client-js
This gem hydrates components automatically.
Surprisingly, looks like you're right. I just tried to do it myself - it works - I don't understand how - but it works. But I made it all a bit easier. I do not create a store filled - I dispatch a filling action - for an empty store.
Here's my code (that's not an SPA, so react-router is not involved, I'll try react-router in future):
app/views/companies/index.html.erb
<%= react_component("ProviderIndexContainer", {companies: @companies}, {prerender: true}) %>
app/javascript/components/ProviderIndexContainer.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import store from '../redux/store';
import CompanyListContainer from '../components/CompanyListContainer';
export default class ProviderIndexContainer extends React.Component {
render() {
store.dispatch({
type: 'COMPANIES_LIST',
companies: this.props.companies
})
return (
<Provider store={store}><CompanyListContainer/></Provider>
)
}
}
app/javascript/components/CompanyListContainer.js
import React from 'react';
import axios from 'axios'
import { connect } from 'react-redux';
import store from '../redux/store';
import CompanyList from './CompanyList';
class CompanyListContainer extends React.Component {
render() {
return (
<div>
<CompanyList companies={this.props.companies} deleteCompany={this.props.deleteCompany} companiesList={this.props.companiesList} />
</div>
)
}
}
const mapStateToProps = function(store) {
return {
companies: store.companies
}
}
const mapDispatchToProps = function(dispatch, ownProps) {
return {
deleteCompany: function(company_id) {
axios.delete('companies/' + company_id + '.json').then(response => {
dispatch({
type: 'COMPANY_DELETE',
company_id: response.data.id
})
})
},
companiesList: function() {
axios.get('/companies.json').then(response => {
dispatch({
type: 'COMPANIES_LIST',
companies: response.data
})
})
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CompanyListContainer);
app/javascript/components/CompanyList.js
import React from 'react';
import { Link } from 'react-router-dom'
// Presentational Component
export default class CompanyList extends React.Component {
deleteCompany(props, company_id) {
let res = confirm("Are you sure you want to delete?");
if (res) {
props.deleteCompany(company_id)
}
}
render() {
let _this = this;
return (
<div id="table"><div className="margin-bottom">The list of companies</div>
{this.props.companies.map(function(company) {
return (
<div className="row" key={company.id}>
<span className="cell"><a href={'/companies/' + company.id}>{company.name}</a></span>
<button className="cell" onClick={_this.deleteCompany.bind(null, _this.props, company.id)} className="company-button">Delete</button>
</div>
);
})}
<button className="no-wrap" onClick={_this.props.companiesList.bind(null)}>Refresh companies list</button>
</div>
)
}
}
app/javascript/redux/store/index.js
import { createStore } from 'redux';
import reducers from '../reducers';
import initialState from './initial-state';
const store = createStore(reducers, initialState);
export default store;
app/javascript/redux/store/initial-state.js
import axios from 'axios'
let initialState = {
companies: [],
company: {name: '', price: ''}
};
export default initialState;
It looks like that gem react-rails does all the server-rendering job itself - under the hood - and even transfers the Redux server-rendered store to the client side automatically.
To check it practically I modified app/javascript/components/CompanyListContainer.js:
const mapDispatchToProps = function(dispatch, ownProps) {
return {
...
companiesList: function() {
console.log(store.getState()) // the added part
axios.get('/companies.json').then(response => {
dispatch({
type: 'COMPANIES_LIST',
companies: response.data
})
})
}
}
}
and I saw the Redux store client-side, in the console (when pressed the 'Refresh companies list' button).
How does react-rails transfer the Redux store from the server-side to the client-side - is a mystery to me. And does it really do it? What if it just doubles dispatching - both server-side and client-side?
And switching from rendering the Redux store server-side to client-side is easy - all that's needed is simply to change:
<%= react_component("ProviderIndexContainer", {companies: @companies}, {prerender: true}) %>
to
<%= react_component("ProviderIndexContainer", {companies: @companies}) %>
and the rendered companies list respectively disappears from the fetched HTML.
PS
One thing that bothers me is whether I (and @ttanimichi) do it all correctly. Because what I portrayed here is so much far away from the https://redux.js.org/recipes/serverrendering picture.
But one thing is clear - I hate the https://github.com/shakacode/react_on_rails gem because it looks so ugly, ponderous, poorly documented and even partially commercial (!). I am fed up with their ads like "hire us" on the docs page. :) That gem has to be punished by no-usage.
By 'hydration' I meant this: https://redux.js.org/recipes/serverrendering#client-js
As I already told you, the hydration in the example has nothing to do with Redux. hydrate is a function of react-dom and it has everything to do with React.
it works
Congrats.
I don't understand how
If you specify prerender: true, this gem automatically hydrate the component in client side.
https://github.com/reactjs/react-rails/blob/f02a3afd373761946df381acd685d9ba2b1754dd/react_ujs/index.js#L98
react-rails 2.4.1+ supports React's hydration. ref. https://github.com/reactjs/react-rails/pull/828
How does react-rails transfer the Redux store from the server-side to the client-side - is a mystery to me. And does it really do it?
No, it doesn't.
it just doubles dispatching - both server-side and client-side?
Yes.
all that's needed is simply to change:
Without the option {prerender: true}, SSR isn't performed.
@ttanimichi
it just doubles dispatching - both server-side and client-side? Yes.
If you have any confirming references to this statement, it would be nice to see them. Not necessarily right now, but maybe someday later. I hope everybody would be interested to see it.
Without the option {prerender: true}, SSR isn't performed.
I just mean that it is easy to globally change a website's SSR presence - in a single place:
global_prerender_mode = true
<%= react_component("ProviderIndexContainer", {companies: @companies}, {prerender: global_prerender_mode}) %>
.....
<%= react_component("AnyOtherComponent", {props: @something}, {prerender: global_prerender_mode}) %>
Looks like that by default it is nice to enable SSR website-wide (if this is as easy as to change global_prerender_mode from false to true).
PS My next step - to try to use react-router + SSR + Redux for this gem.
Here's another hydrate explanation:
https://stackoverflow.com/questions/46516395/whats-the-difference-between-hydrate-and-render-in-react-16
Going through the above discussion, the issue can be closed.