Skip to content

Using Redux Provider: id mismatch? #878

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
silouanwright opened this issue Feb 23, 2018 · 14 comments
Closed

Using Redux Provider: id mismatch? #878

silouanwright opened this issue Feb 23, 2018 · 14 comments

Comments

@silouanwright
Copy link

silouanwright commented Feb 23, 2018

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

screen shot 2018-02-23 at 2 14 48 pm

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-rails is 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.
@ttanimichi
Copy link
Member

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.

@BookOfGreg
Copy link
Member

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.

@ttanimichi
Copy link
Member

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="{&quot;post&quot;:{&quot;id&quot;:&quot;42&quot;,&quot;body&quot;:&quot;foo bar&quot;}}" 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

@BookOfGreg
Copy link
Member

<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.

@ttanimichi
Copy link
Member

@BookOfGreg You're so kind. Don't get burnt out over this 🙂

@silouanwright
Copy link
Author

silouanwright commented Feb 26, 2018

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?

@programrails
Copy link

programrails commented Aug 7, 2018

@reywright

  1. I must set up the Redux store server-side first - and it's not empty, this a list of companies.
  2. 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.

@programrails
Copy link

@ttanimichi
How about transferring the Redux store client-side and hydrateing it there?

@ttanimichi
Copy link
Member

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.

@ttanimichi
Copy link
Member

Did you try this example? #878 (comment)

@programrails
Copy link

programrails commented Aug 8, 2018

@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.

@ttanimichi
Copy link
Member

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.

ReactDOM.hydrate(React.createElement(constructor, props), node);

react-rails 2.4.1+ supports React's hydration. ref. #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.

@programrails
Copy link

programrails commented Aug 8, 2018

@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

@alkesh26
Copy link
Collaborator

alkesh26 commented Nov 2, 2022

Going through the above discussion, the issue can be closed.

@alkesh26 alkesh26 closed this as completed Nov 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants