Skip to content
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

Allow to return multiple markup strings from react_component helper #793

Closed
udovenko opened this issue Apr 5, 2017 · 8 comments
Closed
Labels

Comments

@udovenko
Copy link
Contributor

udovenko commented Apr 5, 2017

There are some usefull libraries based on https://github.com/gaearon/react-side-effect package. For example https://github.com/nfl/react-helmet is a great package for building <head> contents, like <meta> tags and <title>.

To use it on server we need to call const helmet = Helmet.renderStatic(); after we called ReactDOMServer.renderToString(...) and insert markup strings form helmet object to HTML: https://github.com/nfl/react-helmet#server-usage.

To acheive this we need some kind of react_component helper but it should return an array of strings or a Hash. Lets call this method react_component_with_side_effects for now. Here is how it could look like:

<%
react_component_markup, title_tag, description_tag =
  react_component_with_side_effects('App', id: 'some_id', 
                                           props: {...},
                                           side_effects: ['const helmet = Helmet.renderStatic();return helmet.title.toString()',
                                                          'return helmet.meta.toString();'])
%>

<% content_for :title do %>
  <%= title_tag %>
<% end %>


<% content_for :description do %>
  <%= description_tag %>
<% end %>

<%= react_component_markup %>

This is just an example of what we could do. An implementation needs to be discussed.

@udovenko
Copy link
Contributor Author

udovenko commented Apr 5, 2017

Maybe even better to allow generator function on server to return Object instead of single React component. It could be detected by regular react_component helper to return back a Hash.

@justin808
Copy link
Member

justin808 commented Apr 6, 2017

@udovenko, could this help:

server_render_js(js_expression, options = {})

https://github.com/shakacode/react_on_rails/blob/master/README.md#server_render_js

Example

https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/app/views/pages/render_js.html.erb

<pre>
<%= server_render_js("(function() { var x = ReactOnRails.getComponent('HelloString').component.world(); console.warn('ran console.warn on the server()'); return x;})()") %>
</pre>

@udovenko
Copy link
Contributor Author

udovenko commented Apr 6, 2017

@justin808 I think it will not help. server_render_js helper will check out random free js_context form JS rendering pool. To ensure that Helmet will return correct metatags we will need to reinitialize stores and rerender React components inside server_render_js evaluated code again (about a double CPU usage).
The key thing that we need to extract side effects right after react component was rendered to string on the server.

@udovenko
Copy link
Contributor Author

udovenko commented Apr 6, 2017

This problem could be solved (theoretically) by introducing renderer function for serverside rendering (as an analog of clientside rendrer function). In this server side renderer function developer should take responsibility for calling ReactDOMServer.renderToString() himself. Function could return either string with generated HTML or array like [generated_html, additional_string_value_1, additional_string_value_2]. And if an array was returned, react_component could return an array instead of react component markup.

@justin808
Copy link
Member

The key thing that we need to extract side effects right after react component was rendered to string on the server.

What are the side effects?

@udovenko This sounds legit. Please try to craft the ideal API in terms of the JS code and the helper. Once you have that, I'll work with you on the implementation.

Please send me your email and I'll invite you to our slack channel.

@udovenko
Copy link
Contributor Author

udovenko commented Apr 7, 2017

What are the side effects?

By side effects I mean any additional content generated during ReactDOMServer.renderToString() call: metatags, page title and whatever. Sorry for such a misleading term.

@justin808 Ok, here is how I see the usage of our potential new feature:

  1. User creates renderer function for server rendering. It is responsible for calling ReactDOMServer.renderToString() internally and should return rendered string or an array. If an array was returned, its first element must be a result of ReactDOMServer.renderToString() and other elements are strings produced by side-effects:
// startup/ServerAppRenderer.js

export default (props, railsContext) => {
  let error;
  let redirectLocation;
  let routeProps;

  const { location } = railsContext;

  match({ routes, location }, (_error, _redirectLocation, _routeProps) => {
    error = _error;
    redirectLocation = _redirectLocation;
    routeProps = _routeProps;
  });

  if (error || redirectLocation) {
    return { error, redirectLocation };
  }

  const store = ReactOnRails.getStore('Store');
  const appComponent =  (
    <Provider store={store}>
      <RouterContext {...routeProps}/>
    </Provider>
  )

 const appHTML = ReactDOMServer.renderToString(appComponent);
 const helmet = Helmet.renderStatic();

 return [appHTML, helmet.title.toString(), helmet.meta.toString()];
};

Then user registers created renderer. The problem here is how to detect renderer function. On client side function signature is used for it, i.e. if function takes third argument - it is a renderer. But on the server side we do not need that third argument. So this part is to be discussed:

// startup/serverRegistration.js

import ServerAppRenderer from './ServerAppRenderer';
import ReactOnRails from 'react-on-rails';

ReactOnRails.register({
  App: ServerAppRenderer,
});

Now user can call react_component helper to receive all strings returned by renderer function.
For this purpose helper method can return an array:

# views/some_resource/show.html.erb (for example)

<% react_component_html, page_title, meta_tags = react_component_with_side_effects('App', id: 'some_id', props: {...}) %>

<% content_for :title do %>
  <%= page_title %>
<% end %>

<% content_for :meta do %>
  <%= meta_tags %>
<% end %>

<%= react_component_html %>

@justin808
Copy link
Member

justin808 commented Apr 7, 2017

// startup/ServerAppRenderer.js

export default (props, railsContext) => {
  let error;
  let redirectLocation;
  let routeProps;

  const { location } = railsContext;

  match({ routes, location }, (_error, _redirectLocation, _routeProps) => {
    error = _error;
    redirectLocation = _redirectLocation;
    routeProps = _routeProps;
  });

  if (error || redirectLocation) {
    return { error, redirectLocation };
  }

  const store = ReactOnRails.getStore('Store');
  const appComponent =  (
    <Provider store={store}>
      <RouterContext {...routeProps}/>
    </Provider>
  )

 const appHtml = ReactDOMServer.renderToString(appComponent);
 const helmet = Helmet.renderStatic();

 return { appHtml, pageTitle: helmet.title.toString(), metaTags: helmet.meta.toString()};
};

ERB

<% render_hash = react_component('App', id: 'some_id', props: {...}) %>
<% content_for :title do %>
  <%= render_hash['pageTitle'] %>
<% end %>

<% content_for :meta do %>
  <%= render_hash['metaTags'] %>
<% end %>

<%= render_hash['appHtml'] %>

API

  /**
   * Main entry point to using the react-on-rails npm package. This is how Rails will be able to
   * find you components for rendering. Components get called with props, or you may use a
   * "generator function" to return a React component or an object with the following shape:
   * { renderedHtml, redirectLocation, error }. renderedHtml may be an Object, which converts 
   * to a Ruby Hash. This is useful if you want to use something like React Helmut to return
   * additional values when rendering. See the doc page /docs/additional-reading/react-helmut.md
   * @param components (key is component name, value is component)
   */
  register(components)

@udovenko
Copy link
Contributor Author

@justin808 PR #800

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants