Skip to content

Refactor helper lifecycle #356

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

Merged
merged 4 commits into from
Sep 18, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

#### Bug Fixes

- Use controller lifecycle hooks for view helper (tests don't run middlewares)

## 1.3.0 (September 15, 2015)

#### Breaking Changes
Expand Down
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ rails g react:install
```

This will:
- create a `components.js` manifest file and a `app/assets/javascripts/components/` directory,
- create a `components.js` manifest file and a `app/assets/javascripts/components/` directory,
where you will put your components
- place the following in your `application.js`:

Expand All @@ -48,7 +48,7 @@ where you will put your components

### React.js builds

You can pick which React.js build (development, production, with or without [add-ons]((http://facebook.github.io/react/docs/addons.html)))
You can pick which React.js build (development, production, with or without [add-ons]((http://facebook.github.io/react/docs/addons.html)))
to serve in each environment by adding a config. Here are the defaults:

```ruby
Expand All @@ -71,10 +71,10 @@ MyApp::Application.configure do
end
```

After restarting your Rails server, `//= require react` will provide the build of React.js which
After restarting your Rails server, `//= require react` will provide the build of React.js which
was specified by the configurations.

`react-rails` offers a few other options for versions & builds of React.js.
`react-rails` offers a few other options for versions & builds of React.js.
See [VERSIONS.md](https://github.com/reactjs/react-rails/blob/master/VERSIONS.md) for more info about
using the `react-source` gem or dropping in your own copies of React.js.

Expand Down Expand Up @@ -121,7 +121,7 @@ config.react.jsx_transform_options = {

### Rendering & mounting

`react-rails` includes a view helper (`react_component`) and an unobtrusive JavaScript driver (`react_ujs`)
`react-rails` includes a view helper (`react_component`) and an unobtrusive JavaScript driver (`react_ujs`)
which work together to put React components on the page. You should require the UJS driver
in your manifest after `react` (and after `turbolinks` if you use [Turbolinks](https://github.com/rails/turbolinks)).

Expand All @@ -133,7 +133,7 @@ The __view helper__ puts a `div` on the page with the requested component class
<div data-react-class="HelloMessage" data-react-props="{&quot;name&quot;:&quot;John&quot;}"></div>
```

On page load, the __`react_ujs` driver__ will scan the page and mount components using `data-react-class`
On page load, the __`react_ujs` driver__ will scan the page and mount components using `data-react-class`
and `data-react-props`.

If Turbolinks is present components are mounted on the `page:change` event, and unmounted on `page:before-unload`.
Expand Down Expand Up @@ -169,9 +169,9 @@ _(It will be also be mounted by the UJS on page load.)_

There are some requirements for this to work:

- `react-rails` must load your code. By convention it uses `components.js`, which was created
- `react-rails` must load your code. By convention it uses `components.js`, which was created
by the install task. This file must include your components _and_ their dependencies (eg, Underscore.js).
- Your components must be accessible in the global scope.
- Your components must be accessible in the global scope.
If you are using `.js.jsx.coffee` files then the wrapper function needs to be taken into account:

```coffee
Expand All @@ -180,7 +180,7 @@ If you are using `.js.jsx.coffee` files then the wrapper function needs to be ta
render: ->
`<ExampleComponent videos={this.props.videos} />`
```
- Your code can't reference `document`. Prerender processes don't have access to `document`,
- Your code can't reference `document`. Prerender processes don't have access to `document`,
so jQuery and some other libs won't work in this environment :(

You can configure your pool of JS virtual machines and specify where it should load code:
Expand Down Expand Up @@ -222,10 +222,10 @@ By default, your current layout will be used and the component, rather than a vi

### Component generator

`react-rails` ships with a Rails generator to help you get started with a simple component scaffold.
You can run it using `rails generate react:component ComponentName (--es6)`.
The generator takes an optional list of arguments for default propTypes,
which follow the conventions set in the [Reusable Components](http://facebook.github.io/react/docs/reusable-components.html)
`react-rails` ships with a Rails generator to help you get started with a simple component scaffold.
You can run it using `rails generate react:component ComponentName (--es6)`.
The generator takes an optional list of arguments for default propTypes,
which follow the conventions set in the [Reusable Components](http://facebook.github.io/react/docs/reusable-components.html)
section of the React documentation.

For example:
Expand Down Expand Up @@ -294,7 +294,7 @@ Note that the arguments for `oneOf` and `oneOfType` must be enclosed in single q

### Jbuilder & react-rails

If you use Jbuilder to pass a JSON string to `react_component`, make sure your JSON is a stringified hash,
If you use Jbuilder to pass a JSON string to `react_component`, make sure your JSON is a stringified hash,
not an array. This is not the Rails default -- you should add the root node yourself. For example:

```ruby
Expand All @@ -313,7 +313,7 @@ end

## CoffeeScript

It is possible to use JSX with CoffeeScript. To use CoffeeScript, create files with an extension `.js.jsx.coffee`.
It is possible to use JSX with CoffeeScript. To use CoffeeScript, create files with an extension `.js.jsx.coffee`.
We also need to embed JSX code inside backticks so that CoffeeScript ignores the syntax it doesn't understand.
Here's an example:

Expand Down Expand Up @@ -348,8 +348,8 @@ Any subclass of `ExecJSRenderer` may use those hooks (for example, `SprocketsRen
`react-rails` uses a "helper implementation" class to generate the output of the `react_component` helper. The helper is initialized once per request and used for each `react_component` call during that request. You can provide a custom helper class to `config.react.view_helper_implementation`. The class must implement:

- `#react_component(name, props = {}, options = {}, &block)` to return a string to inject into the Rails view
- `#setup(rack_env)`, called when the helper is initialized at the start of the request
- `#teardown(rack_env)`, called at the end of the request
- `#setup(controller_instance)`, called when the helper is initialized at the start of the request
- `#teardown(controller_instance)`, called at the end of the request

`react-rails` provides one implementation, `React::Rails::ComponentMount`.

Expand Down
1 change: 0 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ Rake::TestTask.new(:test) do |t|
t.libs << 'test'
t.pattern = ENV['TEST_PATTERN'] || 'test/**/*_test.rb'
t.verbose = ENV['TEST_VERBOSE'] == '1'
t.warning = true
end

task default: :test
2 changes: 1 addition & 1 deletion lib/react/rails.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'react/rails/asset_variant'
require 'react/rails/engine'
require 'react/rails/railtie'
require 'react/rails/render_middleware'
require 'react/rails/controller_lifecycle'
require 'react/rails/version'
require 'react/rails/component_mount'
require 'react/rails/view_helper'
Expand Down
2 changes: 1 addition & 1 deletion lib/react/rails/component_mount.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class ComponentMount
include ActionView::Helpers::TextHelper
attr_accessor :output_buffer

# RenderMiddleware calls these hooks
# ControllerLifecycle calls these hooks
# You can use them in custom helper implementations
def setup(env)
end
Expand Down
24 changes: 24 additions & 0 deletions lib/react/rails/controller_lifecycle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module React
module Rails
module ControllerLifecycle
extend ActiveSupport::Concern

included do
# use old names to support Rails 3
before_filter :setup_react_component_helper
after_filter :teardown_react_component_helper
attr_reader :__react_component_helper
end

def setup_react_component_helper
new_helper = React::Rails::ViewHelper.helper_implementation_class.new
new_helper.setup(self)
@__react_component_helper = new_helper
end

def teardown_react_component_helper
@__react_component_helper.teardown(self)
end
end
end
end
4 changes: 2 additions & 2 deletions lib/react/rails/controller_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ class React::Rails::ControllerRenderer

attr_accessor :output_buffer

attr_reader :request
def initialize(options={})
@request = options[:request]
controller = options[:controller]
@__react_component_helper = controller.__react_component_helper
end

def call(name, options, &block)
Expand Down
9 changes: 7 additions & 2 deletions lib/react/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,26 @@ class Railtie < ::Rails::Railtie

# Include the react-rails view helper lazily
initializer "react_rails.setup_view_helpers", group: :all do |app|
app.config.middleware.use(::React::Rails::RenderMiddleware)

app.config.react.jsx_transformer_class ||= React::JSX::DEFAULT_TRANSFORMER
React::JSX.transformer_class = app.config.react.jsx_transformer_class
React::JSX.transform_options = app.config.react.jsx_transform_options

app.config.react.view_helper_implementation ||= React::Rails::ComponentMount
React::Rails::ViewHelper.helper_implementation_class = app.config.react.view_helper_implementation

ActiveSupport.on_load(:action_controller) do
include ::React::Rails::ControllerLifecycle
end

ActiveSupport.on_load(:action_view) do
include ::React::Rails::ViewHelper
end
end

initializer "react_rails.add_component_renderer", group: :all do |app|
ActionController::Renderers.add :component do |component_name, options|
renderer = ::React::Rails::ControllerRenderer.new(request: request)
renderer = ::React::Rails::ControllerRenderer.new(controller: self)
html = renderer.call(component_name, options)
render_options = options.merge(inline: html)
render(render_options)
Expand Down
19 changes: 0 additions & 19 deletions lib/react/rails/render_middleware.rb

This file was deleted.

7 changes: 3 additions & 4 deletions lib/react/rails/view_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ module Rails
module ViewHelper
# This class will be used for inserting tags into HTML.
# It should implement:
# - #setup(env)
# - #teardown(env)
# - #setup(controller_instance)
# - #teardown(controller_instance)
# - #react_component(name, props, options &block)
# The default is {React::Rails::ComponentMount}
mattr_accessor :helper_implementation_class

def react_component(*args, &block)
impl_key = React::Rails::RenderMiddleware::HELPER_IMPLEMENTATION_KEY
helper_obj = request.env[impl_key]
helper_obj = @__react_component_helper
helper_obj.react_component(*args, &block)
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
# calls to `react_component`
class DummyHelperImplementation
attr_reader :events

def initialize
@events = []
end

def setup(env)
@events << :setup
def setup(controller)
@events << (controller.params["param_test"] || :setup)
end

def teardown(env)
Expand All @@ -21,9 +22,7 @@ def react_component(*args)
end
end

class RenderMiddlewareTest < ActionDispatch::IntegrationTest
impl_key = React::Rails::RenderMiddleware::HELPER_IMPLEMENTATION_KEY

class ControllerLifecycleTest < ActionDispatch::IntegrationTest
def setup
@previous_helper_implementation = React::Rails::ViewHelper.helper_implementation_class
React::Rails::ViewHelper.helper_implementation_class = DummyHelperImplementation
Expand All @@ -35,22 +34,22 @@ def teardown

test "it creates a helper object and puts it in the request env" do
get '/pages/1'
helper_obj = request.env[impl_key]
helper_obj = controller.__react_component_helper
assert(helper_obj.is_a?(DummyHelperImplementation), "It uses the view helper implementation class")
end

test "it calls setup and teardown methods" do
get '/pages/1'
helper_obj = request.env[impl_key]
lifecycle_steps = [:setup, :react_component, :teardown]
get '/pages/1?param_test=123'
helper_obj = controller.__react_component_helper
lifecycle_steps = ["123", :react_component, :teardown]
assert_equal(lifecycle_steps, helper_obj.events)
end

test "there's a new helper object for every request" do
get '/pages/1'
first_helper = request.env[impl_key]
first_helper = controller.__react_component_helper
get '/pages/1'
second_helper = request.env[impl_key]
second_helper = controller.__react_component_helper
assert(first_helper != second_helper, "The helper for the second request is brand new")
end
end
8 changes: 8 additions & 0 deletions test/react/rails/pages_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'test_helper'

class PagesControllerTest < ActionController::TestCase
test 'renders successfully' do
get :show, id: 1
assert_equal(200, response.status)
end
end