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

Add ClientPropsExtension support #1413

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ ReactOnRails.configure do |config|

# If you're using the standard rails/webpacker configuration of webpack, then rails/webpacker
# will automatically modify or create an assets:precompile task to build your assets. If so,
# set this value to nil. Alternatively, you can specify `config.build_production_command`
# set this value to nil. Alternatively, you can specify `config.build_production_command`
# to have react_on_rails invoke a command for you during assets:precompile.
# The command is either a script or a module containing a class method `call`
# In this example, the module BuildProductionCommand would have a class method `call`.
Expand Down Expand Up @@ -129,6 +129,11 @@ ReactOnRails.configure do |config|
# any server rendering issues immediately during development.
config.raise_on_prerender_error = Rails.env.development?

# This configuration allows logic to be applied to client rendered props, such as stripping props that are only used during server rendering.
# Add a module with an adjust_props_for_client_side_hydration method that expects the component's name & props hash
# See below for an example definition of RenderingPropsExtension
config.rendering_props_extension = RenderingPropsExtension

################################################################################
# Server Renderer Configuration for ExecJS
################################################################################
Expand Down Expand Up @@ -176,9 +181,8 @@ ReactOnRails.configure do |config|

# You can optionally add values to your rails_context. This object is passed
# every time a component renders.
# See example below for an example definition of RenderingExtension
#
# config.rendering_extension = RenderingExtension
# See below for an example definition of RenderingExtension
config.rendering_extension = RenderingExtension

################################################################################
################################################################################
Expand Down Expand Up @@ -216,12 +220,23 @@ ReactOnRails.configure do |config|
end
```

Example of a ReactOnRailsConfig module for `client_props_extension`:

```ruby
module RenderingPropsExtension
# The modify_props method will be called by ReactOnRails::ReactComponent::RenderOptions if config.client_props_extension is defined
def self.adjust_props_for_client_side_hydration(component_name, props)
component_name == 'HelloWorld' ? props.except(:server_side_only) : props
end
end
```

Example of a ReactOnRailsConfig module for `production_build_command`:

```ruby
module BuildProductionCommand
include FileUtils
# Method with the name of call will be called during assets:precompile
# The call method will be called during assets:precompile
def self.call
sh "bin/webpack"
end
Expand Down
6 changes: 4 additions & 2 deletions lib/react_on_rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def self.configuration
# skip_display_none is deprecated
webpack_generated_files: %w[manifest.json],
rendering_extension: nil,
rendering_props_extension: nil,
server_render_method: nil,
build_test_command: "",
build_production_command: "",
Expand All @@ -51,7 +52,7 @@ class Configuration
:build_production_command,
:i18n_dir, :i18n_yml_dir, :i18n_output_format,
:server_render_method, :random_dom_id,
:same_bundle_for_client_and_server
:same_bundle_for_client_and_server, :rendering_props_extension

# rubocop:disable Metrics/AbcSize
def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil,
Expand All @@ -65,7 +66,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
build_production_command: nil,
same_bundle_for_client_and_server: nil,
i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil,
random_dom_id: nil, server_render_method: nil)
random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil)
self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root
self.generated_assets_dirs = generated_assets_dirs
self.generated_assets_dir = generated_assets_dir
Expand All @@ -87,6 +88,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
self.trace = trace.nil? ? Rails.env.development? : trace
self.raise_on_prerender_error = raise_on_prerender_error
self.skip_display_none = skip_display_none
self.rendering_props_extension = rendering_props_extension

# Server rendering:
self.server_bundle_js_file = server_bundle_js_file
Expand Down
2 changes: 1 addition & 1 deletion lib/react_on_rails/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ def internal_react_component(react_component_name, options = {})
# Setup the page_loaded_js, which is the same regardless of prerendering or not!
# The reason is that React is smart about not doing extra work if the server rendering did its job.
component_specification_tag = content_tag(:script,
json_safe_and_pretty(render_options.props).html_safe,
json_safe_and_pretty(render_options.client_props).html_safe,
type: "application/json",
class: "js-react-on-rails-component",
"data-component-name" => render_options.react_component_name,
Expand Down
14 changes: 14 additions & 0 deletions lib/react_on_rails/react_component/render_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ def props
options.fetch(:props) { NO_PROPS }
end

def client_props
props_extension = ReactOnRails.configuration.rendering_props_extension
if props_extension.present?
if props_extension.respond_to?(:adjust_props_for_client_side_hydration)
return props_extension.adjust_props_for_client_side_hydration(react_component_name,
props.clone)
end

Rails.logger.warn "ReactOnRails: your rendering_props_extension module is missing the "\
"required adjust_props_for_client_side_hydration method & can not be used"
end
props
end

def random_dom_id
retrieve_configuration_value_for(:random_dom_id)
end
Expand Down
3 changes: 2 additions & 1 deletion spec/dummy/app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def data
@app_props_server_render = {
helloWorldData: {
name: "Mr. Server Side Rendering"
}.merge(xss_payload)
}.merge(xss_payload),
modificationTarget: "server-only"
}

@app_props_hello = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<%= react_component("HelloWorldHooks", props: @app_props_server_render, prerender: true, trace: true, id: "HelloWorld-react-component-0") %>
<%= react_component("HelloWorldHooks", props: @app_props_server_render, prerender: true, trace: true, id: "HelloWorldHooks-react-component-0") %>
<hr/>

<h1>React Rails Server Rendering with Hooks</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<%= react_component("HelloWorldProps", props: @app_props_server_render, prerender: true, trace: true, id: "HelloWorldProps-react-component-0") %>
<hr/>

<h1>React Rails Server Rendering with different props for server & client</h1>
5 changes: 4 additions & 1 deletion spec/dummy/app/views/shared/_header.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
<%= link_to "One Page with Many Examples at Once", root_path %>
</li>
<li>
<%= link_to "Hello World Component Server Rendered Hooks", server_side_hello_world_hooks_path %>
<%= link_to "Hello World Component Server Rendered with React Hooks", server_side_hello_world_hooks_path %>
</li>
<li>
<%= link_to "Hello World Component Server Rendered with Different Props for Client/Server", server_side_hello_world_props_path %>
</li>
<li>
<%= link_to "Hello World Component Client Rendered", client_side_hello_world_path %>
Expand Down
27 changes: 27 additions & 0 deletions spec/dummy/client/app/components/HelloWorldProps.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useState, useEffect } from 'react';
import css from './HelloWorld.module.scss';

function HelloWorldHooks(props) {
console.log(`HelloWorldProps modification target prop value: ${props.modificationTarget}`);

const [name, setName] = useState(props.helloWorldData.name);
// a trick to display a client-only prop value without creating a server/client conflict
const [delayedValue, setDelayedValue] = useState(null);

useEffect(() => {
setDelayedValue(props.modificationTarget);
}, []);

return (
<div>
<h3 className={css.brightColor}>Hello, {name}!</h3>
<p>
Say hello to:
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
</p>
<h4>Value of modification target prop: {delayedValue}</h4>
</div>
);
}

export default HelloWorldHooks;
2 changes: 2 additions & 0 deletions spec/dummy/client/app/packs/client-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'jquery-ujs';
import ReactOnRails from 'react-on-rails';

import HelloWorld from '../components/HelloWorld';
import HelloWorldProps from '../components/HelloWorldProps';
import HelloWorldHooks from '../components/HelloWorldHooks';
import HelloWorldHooksContext from '../components/HelloWorldHooksContext';
import ContextFunctionReturnInvalidJSX from '../components/ContextFunctionReturnInvalidJSX';
Expand Down Expand Up @@ -63,6 +64,7 @@ ReactOnRails.register({
HelloWorldHooksContext,
ContextFunctionReturnInvalidJSX,
PureComponentWrappedInFunction,
HelloWorldProps,
});

ReactOnRails.registerStore({
Expand Down
2 changes: 2 additions & 0 deletions spec/dummy/client/app/packs/server-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import HelloString from '../non_react/HelloString';

// React components
import HelloWorld from '../components/HelloWorld';
import HelloWorldProps from '../components/HelloWorldProps';
import HelloWorldHooks from '../components/HelloWorldHooks';
import HelloWorldHooksContext from '../components/HelloWorldHooksContext';

Expand Down Expand Up @@ -69,6 +70,7 @@ ReactOnRails.register({
ImageExample,
HelloWorldHooks,
HelloWorldHooksContext,
HelloWorldProps,
});

ReactOnRails.registerStore({
Expand Down
5 changes: 3 additions & 2 deletions spec/dummy/client/app/reducers/reducersIndex.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import helloWorldReducer from './HelloWorldReducer';
import railsContextReducer from './RailsContextReducer';
import nullReducer from './nullReducer';

// This is how you do a directory of reducers.
// The `import * as reducers` does not work for a directory, but only with a single file
export default {
helloWorldData: helloWorldReducer,
railsContext: railsContextReducer,
railsContext: nullReducer,
modificationTarget: nullReducer,
};
10 changes: 10 additions & 0 deletions spec/dummy/config/initializers/react_on_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ def self.custom_context(view_context)
end
end

module RenderingPropsExtension
# Return a Hash that contains custom props for all client rendered react_components
def self.adjust_props_for_client_side_hydration(component_name, props)
props[:modificationTarget] = "client-only" if component_name == "HelloWorldProps"
props
end
end

ReactOnRails.configure do |config|
config.server_bundle_js_file = "server-bundle.js"
config.random_dom_id = false # default is true
Expand All @@ -23,4 +31,6 @@ def self.custom_context(view_context)
# config.build_production_command = "RAILS_ENV=production NODE_ENV=production bin/webpack"
# config.webpack_generated_files = %w[server-bundle.js manifest.json]
config.rendering_extension = RenderingExtension

config.rendering_props_extension = RenderingPropsExtension
end
1 change: 1 addition & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
get "server_side_hello_world_shared_store_defer" => "pages#server_side_hello_world_shared_store_defer"
get "server_side_hello_world" => "pages#server_side_hello_world"
get "server_side_hello_world_hooks" => "pages#server_side_hello_world_hooks"
get "server_side_hello_world_props" => "pages#server_side_hello_world_props"
get "client_side_log_throw" => "pages#client_side_log_throw"
get "server_side_log_throw" => "pages#server_side_log_throw"
get "server_side_log_throw_plain_js" => "pages#server_side_log_throw_plain_js"
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
# # to individual examples or groups you care about by tagging them with
# # `:focus` metadata. When nothing is tagged with `:focus`, all examples
# # get run.
# config.filter_run :focus
# # config.filter_run :focus
# config.run_all_when_everything_filtered = true
#
# # Allows RSpec to persist some state between runs in order to support
Expand Down
11 changes: 11 additions & 0 deletions spec/dummy/spec/system/integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,17 @@ def finished_all_ajax_requests?
end
end

describe "use different props for server/client", :js do
subject { page }

before { visit "/server_side_hello_world_props" }

it "image_example should not have any errors" do
expect(page.html).to include("[SERVER] HelloWorldProps modification target prop value: server-only")
expect(page.text).to include("Value of modification target prop: client-only")
end
end

shared_examples "React Component Shared Store" do |url|
subject { page }

Expand Down