diff --git a/README.md b/README.md index 4cd598351..3506f1ba1 100644 --- a/README.md +++ b/README.md @@ -37,866 +37,50 @@ Read the [full review here](https://clutch.co/profile/shakacode#reviews?sort_by= * If you are upgrading, you might consider migrating to the [react_on_rails](https://github.com/shakacode/react_on_rails) gem. * Source code example utilizing React-Rails: https://github.com/BookOfGreg/react-rails-example-app - - -## Contents - - - [Get started with Shakapacker](#get-started-with-shakapacker) - - [Component name](#component-name) - - [File naming](#file-naming) - - [Typescript support](#typescript-support) - - [Test component](#test-component) - - [Use with Asset Pipeline](#use-with-asset-pipeline) - - [Custom JSX Transformer](#custom-jsx-transformer) - - [Transform Plugin Options](#transform-plugin-options) - - [React.js versions](#reactjs-versions) - - [View Helper](#view-helper) - - [Custom View Helper](#custom-view-helper) - - [UJS](#ujs) - - [Mounting & Unmounting](#mounting--unmounting) - - [Event Handling](#event-handling) - - [`getConstructor`](#getconstructor) - - [Server-Side Rendering](#server-side-rendering) - - [Configuration](#configuration) - - [JavaScript State](#javascript-state) - - [Custom Server Renderer](#custom-server-renderer) - - [Controller Actions](#controller-actions) - - [Component Generator](#component-generator) - - [Use with JBuilder](#use-with-jbuilder) - - [Camelize Props](#camelize-props) - - [Changing Component Templates](#changing-component-templates) - - [Upgrading](#upgrading) - - [2.7 to 3.0](#27-to-30) - - [2.3 to 2.4](#23-to-24) - - [Migrating from `react-rails` to `react_on_rails`](#migrating-from-react-rails-to-react_on_rails) - - [Why to migrate?](#why-to-migrate) - - [Steps to migrate](#steps-to-migrate) - - [Common Errors](#common-errors) - - [Getting warning for `Can't resolve 'react-dom/client'` in React < 18](#getting-warning-for-cant-resolve-react-domclient-in-react--18) - - [Undefined Set](#undefined-set) - - [Using TheRubyRacer](#using-therubyracer) - - [HMR](#hmr) - - [Related Projects](#related-projects) - - [Contributing](#contributing) -- [Supporters](#supporters) - - +## Documentation + +- [Get started](docs/get-started.md) + - [Use with Shakapacker](docs/get-started.md#use-with-shakapacker) + - [Component name](docs/get-started.md#component-name) + - [File naming](docs/get-started.md#file-naming) + - [Typescript support](docs/get-started.md#typescript-support) + - [Test component](docs/get-started.md#test-component) + - [Use with Asset Pipeline](docs/get-started.md#use-with-asset-pipeline) + - [Custom JSX Transformer](docs/get-started.md#custom-jsx-transformer) + - [Transform Plugin Options](docs/get-started.md#transform-plugin-options) + - [React.js versions](docs/get-started.md#reactjs-versions) +- [View Helper](docs/view-helper.md) + - [Custom View Helper](docs/view-helper.md#custom-view-helper) +- [UJS](docs/ujs.md) + - [Mounting & Unmounting](docs/ujs.md#mounting--unmounting) + - [Event Handling](docs/ujs.md#event-handling) + - [`getConstructor`](docs/ujs.md#getconstructor) +- [Server-Side Rendering](docs/server-side-rendering.md) + - [Configuration](docs/server-side-rendering.md#configuration) + - [JavaScript State](docs/server-side-rendering.md#javascript-state) + - [Custom Server Renderer](docs/server-side-rendering.md#custom-server-renderer) +- [Controller Actions](docs/controller-actions.md) +- [Component Generator](docs/component-generator.md) + - [Use with JBuilder](docs/component-generator.md#use-with-jbuilder) + - [Camelize Props](docs/component-generator.md#camelize-props) + - [Changing Component Templates](docs/component-generator.md#changing-component-templates) +- [Upgrading](docs/upgrading.md) + - [2.7 to 3.0](docs/upgrading.md#27-to-30) + - [2.3 to 2.4](docs/upgrading.md#23-to-24) +- [Migrating from `react-rails` to `react_on_rails`](docs/migrating-from-react-rails-to-react_on_rails.md) + - [Why migrate?](docs/migrating-from-react-rails-to-react_on_rails.md#why-migrate) + - [Steps to migrate](docs/migrating-from-react-rails-to-react_on_rails.md#steps-to-migrate) +- [Common Errors](docs/common-errors.md) + - [Getting warning for `Can't resolve 'react-dom/client'` in React < 18](docs/common-errors.md#getting-warning-for-cant-resolve-react-domclient-in-react--18) + - [Undefined Set](docs/common-errors.md#undefined-set) + - [Using TheRubyRacer](docs/common-errors.md#using-therubyracer) + - [HMR](docs/common-errors.md#hmr) After reading this README file, additional information about React-Rails can be found in the Wiki page: https://github.com/reactjs/React-Rails/wiki The Wiki page features a significant amount of additional information about React-Rails which includes instructional articles and answers to the most frequently asked questions. - -## Get started with Shakapacker - -_Alternatively, get started with [Sprockets](#use-with-asset-pipeline)_ - -1. Create a new Rails app: -Prevent installing default javascript dependencies by using `--skip-javascript` option: - -```bash -rails new my-app --skip-javascript -cd my-app -``` - -2. Install `shakapacker`: -```bash -bundle add shakapacker --strict -rails shakapacker:install -``` - -3. Install `react` and some other required npm packages: -```bash -yarn add react react-dom @babel/preset-react prop-types \ - css-loader style-loader mini-css-extract-plugin css-minimizer-webpack-plugin -``` - -Also update the Babel configuration in the `package.json` file: - -```diff -"babel": { - "presets": [ -- "./node_modules/shakapacker/package/babel/preset.js" -+ "./node_modules/shakapacker/package/babel/preset.js", -+ "@babel/preset-react" - ] -}, -``` - -4. Install `react-rails`: -```bash -$ bundle add 'react-rails' --strict -$ rails generate react:install -``` - -This gives you: - -- `app/javascript/components/` directory for your React components -- [`ReactRailsUJS`](#ujs) setup in `app/javascript/packs/application.js` -- `app/javascript/packs/server_rendering.js` for [server-side rendering](#server-side-rendering) - -5. Generate your first component: -```bash -$ rails g react:component HelloWorld greeting:string -``` - -You can also generate your component in a subdirectory: - -```bash -$ rails g react:component my_subdirectory/HelloWorld greeting:string -``` - -Note: Your component is added to `app/javascript/components/` by default. - -Note: If your component is in a subdirectory you will append the directory path to your erb component call. - -Example: -```erb -<%= react_component("my_subdirectory/HelloWorld", { greeting: "Hello from react-rails." }) %> -``` - -6. [Render it in a Rails view](#view-helper): - -```erb - -<%= react_component("HelloWorld", { greeting: "Hello from react-rails." }) %> -``` - -7. Lets Start the app: -```bash -$ rails s -``` -Output: greeting: Hello from react-rails", inspect webpage in your browser to see the change in tag props. - -8. Run dev server (optional) -In order to run dev server with HMR feature you need to parallely run: - -```bash -$ ./bin/shakapacker-dev-server -``` - -Note: On Rails 6 you need to specify `webpack-dev-server` host. To this end, update `config/initializers/content_security_policy.rb` and uncomment relevant lines. - -### Component name - -The component name tells `react-rails` where to load the component. For example: - -`react_component` call | component `require` ------|----- -`react_component("Item")` | `require("Item")` -`react_component("items/index")` | `require("items/index")` -`react_component("items.Index")` | `require("items").Index` -`react_component("items.Index.Header")` | `require("items").Index.Header` - -This way, you can access top-level, default, or named exports. - -The `require.context` inserted into `packs/application.js` is used to load components. If you want to load components from a different directory, override it by calling `ReactRailsUJS.useContext`: - -```js -var myCustomContext = require.context("custom_components", true) -var ReactRailsUJS = require("react_ujs") -// use `custom_components/` for <%= react_component(...) %> calls -ReactRailsUJS.useContext(myCustomContext) -``` - -If `require` fails to find your component, [`ReactRailsUJS`](#ujs) falls back to the global namespace, described in [Use with Asset Pipeline](#use-with-asset-pipeline). - -In some cases, having multiple `require.context` entries may be desired. Examples of this include: - -- Refactoring a typical Rails application into a Rails API with an (eventually) separate Single Page Application (SPA). For this use case, one can add a separate pack in addition to the typical `application` one. React components can be shared between the packs but the new pack can use a minimal Rails view layout, different default styling, etc. -- In a larger application, you might find it helpful to split your JavaScript by routes/controllers to avoid serving unused components and improve your site performance by keeping bundles smaller. For example, you might have separate bundles for homepage, search, and checkout routes. In that scenario, you can add an array of `require.context` component directory paths via `useContexts` to `server_rendering.js`, to allow for [Server-Side Rendering](#server-side-rendering) across your application: - -```js -// server_rendering.js -var homepageRequireContext = require.context('homepage', true); -var searchRequireContext = require.context('search', true); -var checkoutRequireContext = require.context('checkout', true); - -var ReactRailsUJS = require('react_ujs'); -ReactRailsUJS.useContexts([ - homepageRequireContext, - searchRequireContext, - checkoutRequireContext -]); -``` -### File naming - -React-Rails supports plenty of file extensions such as: .js, .jsx.js, .js.jsx, .es6.js, .coffee, etcetera! -Sometimes this will cause a stumble when searching for filenames. - -Component File Name | `react_component` call ------|----- -`app/javascript/components/samplecomponent.js` | `react_component("samplecomponent")` -`app/javascript/components/sample_component.js` | `react_component("sample_component")` -`app/javascript/components/SampleComponent.js` | `react_component("SampleComponent")` -`app/javascript/components/SampleComponent.js.jsx` | Has to be renamed to SampleComponent.jsx, then use `react_component("SampleComponent")` - -### Typescript support - -```bash -yarn add typescript @babel/preset-typescript -``` - -Babel won’t perform any type-checking on TypeScript code. To optionally use type-checking run: - -```bash -yarn add fork-ts-checker-webpack-plugin -``` - -Add `tsconfig.json` with the following content: - -```json -{ - "compilerOptions": { - "declaration": false, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "lib": ["es6", "dom"], - "module": "es6", - "moduleResolution": "node", - "sourceMap": true, - "target": "es5", - "jsx": "react", - "noEmit": true - }, - "exclude": ["**/*.spec.ts", "node_modules", "vendor", "public"], - "compileOnSave": false -} -``` - -Then modify the webpack config to use it as a plugin: - -```js -// config/webpack/webpack.config.js -const { webpackConfig, merge } = require("shakapacker"); -const ForkTSCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); - -module.exports = merge(webpackConfig, { - plugins: [new ForkTSCheckerWebpackPlugin()], -}); -``` - -Doing this will allow React-Rails to support the .tsx extension. Additionally, it is recommended to add `ts` and `tsx` to the `server_renderer_extensions` in your application configuration: - -```ruby -config.react.server_renderer_extensions = ["jsx", "js", "tsx", "ts"] -``` - -### Test component - -You can use `assert_react_component` to test component render: - -```erb - - -<%= react_component("HelloWorld", { greeting: "Hello from react-rails.", info: { name: "react-rails" } }, { class: "hello-world" }) %> -``` - -```rb -class WelcomeControllerTest < ActionDispatch::IntegrationTest - test 'assert_react_component' do - get "/welcome" - assert_equal 200, response.status - - # assert rendered react component and check the props - assert_react_component "HelloWorld" do |props| - assert_equal "Hello from react-rails.", props[:greeting] - assert_equal "react-rails", props[:info][:name] - assert_select "[class=?]", "hello-world" - end - - # or just assert component rendered - assert_react_component "HelloWorld" - end -end -``` - -## Use with Asset Pipeline - -`react-rails` provides a pre-bundled React.js & a UJS driver to the Rails asset pipeline. Get started by adding the `react-rails` gem: - -```ruby -gem 'react-rails' -``` - -And then install the react generator: - -``` -$ rails g react:install -``` - -Then restart your development server. - -This will: - -- add some `//= require`s to `application.js` -- add a `components/` directory for React components -- add `server_rendering.js` for [server-side rendering](#server-side-rendering) - -Now, you can create React components in `.jsx` files: - -```JSX -// app/assets/javascripts/components/post.jsx - -window.Post = createReactClass({ - render: function() { - return

{this.props.title}

- } -}) - -// or, equivalent: -class Post extends React.Component { - render() { - return

{this.props.title}

- } -} -``` - -Then, you can render those [components in views](#view-helper): - -```erb -<%= react_component("Post", {title: "Hello World"}) %> -``` - -Components must be accessible from the top level, but they may be namespaced, for example: - -```erb -<%= react_component("Comments.NewForm", {post_id: @post.id}) %> - -``` - -### Custom JSX Transformer - -`react-rails` uses a transformer class to transform JSX in the asset pipeline. The transformer is initialized once, at boot. You can provide a custom transformer to `config.react.jsx_transformer_class`. The transformer must implement: - -- `#initialize(options)`, where options is the value passed to `config.react.jsx_transform_options` -- `#transform(code_string)` to return a string of transformed code - -`react-rails` provides two transformers, `React::JSX::BabelTransformer` (which uses [ruby-babel-transpiler](https://github.com/babel/ruby-babel-transpiler)) and `React::JSX::JSXTransformer` (which uses the deprecated `JSXTransformer.js`). - -#### Transform Plugin Options - -To supply additional transform plugins to your JSX Transformer, assign them to `config.react.jsx_transform_options` - -`react-rails` uses the Babel version of the `babel-source` gem. - -For example, to use `babel-plugin-transform-class-properties` : - - config.react.jsx_transform_options = { - optional: ['es7.classProperties'] - } - -### React.js versions - -`//= require react` brings `React` into your project. - -By default, React's [development version] is provided to `Rails.env.development`. You can override the React build with a config: - -```ruby -# Here are the defaults: -# config/environments/development.rb -MyApp::Application.configure do - config.react.variant = :development -end - -# config/environments/production.rb -MyApp::Application.configure do - config.react.variant = :production -end -``` - -Be sure to restart your Rails server after changing these files. See [VERSIONS.md](https://github.com/reactjs/react-rails/blob/master/VERSIONS.md) to learn which version of React.js is included with your `react-rails` version. In some edge cases you may need to bust the sprockets cache with `rake tmp:clear` - - -## View Helper - -`react-rails` includes a view helper and an [unobtrusive JavaScript driver](#ujs) which work together to put React components on the page. - -The view helper (`react_component`) puts a `div` on the page with the requested component class & props. For example: - -```erb -<%= react_component('HelloMessage', name: 'John') %> - -
-``` - -On page load, the [`react_ujs` driver](#ujs) will scan the page and mount components using `data-react-class` -and `data-react-props`. - -The view helper's signature is: - -```ruby -react_component(component_class_name, props={}, html_options={}) -``` - -- `component_class_name` is a string which identifies a component. See [getConstructor](#getconstructor) for details. -- `props` is either: - - an object that responds to `#to_json`; or - - an already-stringified JSON object (see [JBuilder note](#use-with-jbuilder) below). -- `html_options` may include: - - `tag:` to use an element other than a `div` to embed `data-react-class` and `data-react-props`. - - `prerender: true` to render the component on the server. - - `camelize_props` to [transform a props hash](#camelize-props) - - `**other` Any other arguments (eg `class:`, `id:`) are passed through to [`content_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag). - - -#### Custom View Helper - -`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(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`. - -## UJS - -`react-rails`'s JavaScript is available as `"react_ujs"` in the asset pipeline or from NPM. It attaches itself to the window as `ReactRailsUJS`. - -### Mounting & Unmounting - -Usually, `react-rails` mounts & unmounts components automatically as described in [Event Handling](#event-handling) below. - -You can also mount & unmount components from `<%= react_component(...) %>` tags using UJS: - -```js -// Mount all components on the page: -ReactRailsUJS.mountComponents() -// Mount components within a selector: -ReactRailsUJS.mountComponents(".my-class") -// Mount components within a specific node: -ReactRailsUJS.mountComponents(specificDOMnode) - -// Unmounting works the same way: -ReactRailsUJS.unmountComponents() -ReactRailsUJS.unmountComponents(".my-class") -ReactRailsUJS.unmountComponents(specificDOMnode) -``` - -You can use this when the DOM is modified by AJAX calls or modal windows. - -### Event Handling - -`ReactRailsUJS` checks for various libraries to support their page change events: - -- `Turbolinks` -- `pjax` -- `jQuery` -- Native DOM events - -`ReactRailsUJS` will automatically mount components on `<%= react_component(...) %>` tags and unmount them when appropriate. - -If you need to re-detect events, you can call `detectEvents`: - -```js -// Remove previous event handlers and add new ones: -ReactRailsUJS.detectEvents() -``` - -For example, if `Turbolinks` is loaded _after_ `ReactRailsUJS`, you'll need to call this again. This function removes previous handlers before adding new ones, so it's safe to call as often as needed. - -If `Turbolinks` is `import`ed via Shakapacker (and thus not available globally), `ReactRailsUJS` will be unable to locate it. To fix this, you can temporarily add it to the global namespace: - -```js -// Order is particular. First start Turbolinks: -Turbolinks.start(); -// Add Turbolinks to the global namespace: -window.Turbolinks = Turbolinks; -// Remove previous event handlers and add new ones: -ReactRailsUJS.detectEvents(); -// (Optional) Clean up global namespace: -delete window.Turbolinks; -``` - -### `getConstructor` - -Components are loaded with `ReactRailsUJS.getConstructor(className)`. This function has two default implementations, depending on if you're using the asset pipeline or Shakapacker: - -- On the asset pipeline, it looks up `className` in the global namespace (`ReactUJS.constructorFromGlobal`). -- On Shakapacker, it `require`s files and accesses named exports, as described in [Get started with Shakapacker](#get-started-with-shakapacker), falling back to the global namespace (`ReactUJS.constructorFromRequireContextWithGlobalFallback`). - -You can override this function to customize the mapping of name-to-constructor. [Server-side rendering](#server-side-rendering) also uses this function. - -For example, the fallback behavior of -`ReactUJS.constructorFromRequireContextWithGlobalFallback` can sometimes make -server-side rendering errors hard to debug as it will swallow the original error -(more info -[here](https://github.com/reactjs/react-rails/issues/264#issuecomment-552326663)). -`ReactUJS.constructorFromRequireContext` is provided for this reason. You can -use it like so: - -```js -// Replaces calls to `ReactUJS.useContext` -ReactUJS.getConstructor = ReactUJS.constructorFromRequireContext(require.context('components', true)); -``` - - -## Server-Side Rendering - -You can render React components inside your Rails server with `prerender: true`: - -```erb -<%= react_component('HelloMessage', {name: 'John'}, {prerender: true}) %> - -
-

Hello, John!

-
-``` - -_(It will also be mounted by the [UJS](#ujs) on page load.)_ - -Server rendering is powered by [`ExecJS`](https://github.com/rails/execjs) and subject to some requirements: - -- `react-rails` must load your code. By convention, it uses `server_rendering.js`, which was created -by the install task. This file must include your components _and_ their dependencies (eg, Underscore.js). -- Requires separate compilations for server & client bundles (see [Webpack config](https://github.com/reactjs/react-rails/tree/master/test/dummy/config/webpack)) -- Your code can't reference `document` or `window`. Prerender processes don't have access to `document` or `window`, -so jQuery and some other libs won't work in this environment :( - -`ExecJS` supports many backends. CRuby users will get the best performance from [`mini_racer`](https://github.com/discourse/mini_racer#performance). - -#### Configuration - -Server renderers are stored in a pool and reused between requests. Threaded Rubies (eg jRuby) may see a benefit to increasing the pool size beyond the default `0`. - -These are the default configurations: - -```ruby -# config/application.rb -# These are the defaults if you don't specify any yourself -module MyApp - class Application < Rails::Application - # Settings for the pool of renderers: - config.react.server_renderer_pool_size ||= 1 # ExecJS doesn't allow more than one on MRI - config.react.server_renderer_timeout ||= 20 # seconds - config.react.server_renderer = React::ServerRendering::BundleRenderer - config.react.server_renderer_options = { - files: ["server_rendering.js"], # files to load for prerendering - replay_console: true, # if true, console.* will be replayed client-side - } - # Changing files matching these dirs/exts will cause the server renderer to reload: - config.react.server_renderer_extensions = ["jsx", "js"] - config.react.server_renderer_directories = ["/app/assets/javascripts", "/app/javascript/"] - end -end -``` - -#### JavaScript State - -Some of ExecJS's backends are stateful (eg, mini_racer, therubyracer). This means that any side-effects of a prerender will affect later renders with that renderer. - -To manage state, you have a couple options: - -- Make a custom renderer with `#before_render` / `#after_render` hooks as [described below](#custom-server-renderer) -- Use `per_request_react_rails_prerenderer` to manage state for a whole controller action. - -To check out a renderer for the duration of a controller action, call the `per_request_react_rails_prerenderer` helper in the controller class: - -```ruby -class PagesController < ApplicationController - # Use the same React server renderer for the entire request: - per_request_react_rails_prerenderer -end -``` - -Then, you can access the ExecJS context directly with `react_rails_prerenderer.context`: - -```ruby -def show - react_rails_prerenderer # => # - react_rails_prerenderer.context # => # - - # Execute arbitrary JavaScript code - # `self` is the global context - react_rails_prerenderer.context.exec("self.Store.setup()") - render :show - react_rails_prerenderer.context.exec("self.Store.teardown()") -end -``` - -`react_rails_prerenderer` may also be accessed in before- or after-actions. - -#### Custom Server Renderer - -`react-rails` depends on a renderer class for rendering components on the server. You can provide a custom renderer class to `config.react.server_renderer`. The class must implement: - -- `#initialize(options={})`, which accepts the hash from `config.react.server_renderer_options` -- `#render(component_name, props, prerender_options)` to return a string of HTML - -`react-rails` provides two renderer classes: `React::ServerRendering::ExecJSRenderer` and `React::ServerRendering::BundleRenderer`. - -`ExecJSRenderer` offers two other points for extension: - -- `#before_render(component_name, props, prerender_options)` to return a string of JavaScript to execute _before_ calling `React.render` -- `#after_render(component_name, props, prerender_options)` to return a string of JavaScript to execute _after_ calling `React.render` - -Any subclass of `ExecJSRenderer` may use those hooks (for example, `BundleRenderer` uses them to handle `console.*` on the server). - -## Controller Actions - -Components can also be server-rendered directly from a controller action with the custom `component` renderer. For example: - -```ruby -class TodoController < ApplicationController - def index - @todos = Todo.all - render component: 'TodoList', props: { todos: @todos }, tag: 'span', class: 'todo' - end -end -``` - -You can also provide the "usual" `render` arguments: `content_type`, `layout`, `location` and `status`. By default, your current layout will be used and the component, rather than a view, will be rendered in place of `yield`. Custom data-* attributes can be passed like `data: {remote: true}`. - -Prerendering is set to `true` by default, but can be turned off with `prerender: false`. - -## Component Generator - -You can generate a new component file with: - -```sh -rails g react:component ComponentName prop1:type prop2:type ... [options] -``` - -For example, - -```sh -rails g react:component Post title:string published:bool published_by:instanceOf{Person} -``` - -would generate: - -```JSX -var Post = createReactClass({ - propTypes: { - title: PropTypes.string, - published: PropTypes.bool, - publishedBy: PropTypes.instanceOf(Person) - }, - - render: function() { - return ( - - Title: {this.props.title} - Published: {this.props.published} - Published By: {this.props.publishedBy} - - ); - } -}); -``` - -The generator also accepts options: - -- `--es6`: generates a function component -- `--coffee`: use CoffeeScript - -For example, - -```sh -rails g react:component ButtonComponent title:string --es6 -``` - -would generate: - -```jsx -import React from "react" -import PropTypes from "prop-types" - -function ButtonComponent(props) { - return ( - - Title: {this.props.title} - - ); -} - -ButtonComponent.propTypes = { - title: PropTypes.string -}; - -export default ButtonComponent -``` - -**Note:** In a Shakapacker project, es6 template is the default template in the generator. - -Accepted PropTypes are: - -- Plain types: `any`, `array`, `bool`, `element`, `func`, `number`, `object`, `node`, `shape`, `string` -- `instanceOf` takes an optional class name in the form of `instanceOf{className}`. -- `oneOf` behaves like an enum, and takes an optional list of strings in the form of `'name:oneOf{one,two,three}'`. -- `oneOfType` takes an optional list of react and custom types in the form of `'model:oneOfType{string,number,OtherType}'`. - -Note that the arguments for `oneOf` and `oneOfType` must be enclosed in single quotes - to prevent your terminal from expanding them into an argument list. - -#### Use with JBuilder - -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 -# BAD: returns a stringified array -json.array!(@messages) do |message| - json.extract! message, :id, :name - json.url message_url(message, format: :json) -end - -# GOOD: returns a stringified hash -json.messages(@messages) do |message| - json.extract! message, :id, :name - json.url message_url(message, format: :json) -end -``` - -### Camelize Props - -You can configure `camelize_props` option: - -```ruby -MyApp::Application.configure do - config.react.camelize_props = true # default false -end -``` - -Now, Ruby hashes given to `react_component(...)` as props will have their keys transformed from _underscore_- to _camel_-case, for example: - -```ruby -{ all_todos: @todos, current_status: @status } -# becomes: -{ "allTodos" => @todos, "currentStatus" => @status } -``` - -You can also specify this option in `react_component`: - -```erb -<%= react_component('HelloMessage', {name: 'John'}, {camelize_props: true}) %> -``` - -### Changing Component Templates - -To make simple changes to Component templates, copy the respective template file to your Rails project at `lib/templates/react/component/template_filename`. - -For example, to change the [ES6 Component template](https://github.com/reactjs/react-rails/blob/master/lib/generators/templates/component.es6.jsx), copy it to `lib/templates/react/component/component.es6.jsx` and modify it. - -## Upgrading - -### 2.7 to 3.0 -- Keep your `react_ujs` up to date: `yarn upgrade` -- **Drop support for Webpacker:** Before any ReactRails upgrade, make sure upgrading from Webpacker to Shakapacker 7. For more information check out Shakapacker -- **SSR:** ReactRails 3.x requires separate compilations for server & client bundles. See [Webpack config](https://github.com/reactjs/react-rails/tree/master/test/dummy/config/webpack) directory in the dummy app to addapt the new implementation. - -### 2.3 to 2.4 - -Keep your `react_ujs` up to date, `yarn upgrade` - -React-Rails 2.4.x uses React 16+ which no longer has React Addons. Therefore the pre-bundled version of react no longer has an addons version, if you need addons still, there is the 2.3.1+ version of the gem that still has addons. - -If you need to make changes in your components for the prebundled react, see the migration docs here: - -- https://reactjs.org/blog/2016/11/16/react-v15.4.0.html -- https://reactjs.org/blog/2017/04/07/react-v15.5.0.html -- https://reactjs.org/blog/2017/06/13/react-v15.6.0.html - - -For the vast majority of cases this will get you most of the migration: -- global find+replace `React.Prop` -> `Prop` -- add `import PropTypes from 'prop-types'` (Webpacker only) -- re-run `bundle exec rails webpacker:install:react` to update npm packages (Webpacker only) - -## Migrating from `react-rails` to `react_on_rails` - -### Why to migrate? - -[`react_on_rails`](https://github.com/shakacode/react_on_rails/) offers several additional features for a Rails + React application. The following is a table of features comparison. - -| **Feature** | **react-rails** | **react-on-rails** | -| ----------------------- |:---------------:|:------------------:| -| Sprockets | ✅ | ❌ | -| Shakapacker | ✅ | ✅ | -| SSR | ✅ | ✅ | -| SSR with HMR | ✅ | ✅ | -| SSR with React-Router | ❌ | ✅ | -| SSR with Code Splitting | ❌ | ✅ | -| Node SSR | ❌ | ✅ | -| Advanced Redux support | ❌ | ✅ | -| ReScript support | ❌ | ✅ | -| I18n support | ❌ | ✅ | - -`react_on_rails` offers better performance and bundle optimizations, especially with the option of getting a subscription to `react_on_rails_pro`. - -### Steps to migrate - -In this guide, it is assumed that you have upgraded the `react-rails` project to use `shakapacker` version 7. To this end, check out [Shakapacker v7 upgrade guide](https://github.com/shakacode/shakapacker/tree/master/docs/v7_upgrade.md). Upgrading `react-rails` to version 3 can make the migration smoother but it is not required. - -1. Update Deps - - 1. Replace `react-rails` in `Gemfile` with the latest version of `react_on_rails` and run `bundle install`. - 2. Remove `react_ujs` from `package.json` and run `yarn install`. - 3. Commit changes! - -2. Run `rails g react_on_rails:install` but do not commit the change. `react_on_rails` installs node dependencies and also creates sample react component, Rails view/controller, and update `config/routes.rb`. - -3. Adapt the project: Check the changes and carefully accept, reject, or modify them as per your project's needs. Besides changes in `config/shakapacker` or `babel.config` which are project-specific, here are the most noticeable changes to address: - - 1. Check webpack config files at `config/webpack/*`. If coming from `react-rails` v3, the changes are minor since you have already made separate configurations for client and server bundles. The most important change here is to notice the different names for the server bundle entry file. You may choose to stick with `server_rendering.js` or use `server-bundle.js` which is the default name in `react_on_rails`. The decision made here, affects the other steps. - - 2. In `app/javascript` directory you may notice some changes. - - 1. `react_on_rails` by default uses `bundles` directory for the React components. You may choose to rename `components` into `bundles` to follow the convention. - - 2. `react_on_rails` uses `client-bundle.js` and `server-bundle.js` instead of `application.js` and `server_rendering.js`. There is nothing special about these names. It can be set to use any other name (as mentioned above). If you too choose to follow the new names, consider updating the relevant `javascript_pack_tag` in your Rails views. - - 3. Update the content of these files to register your React components for client or server-side rendering. Checking the generated files by `react_on_rails` installation process should give enough hints. - - 3. Check Rails views. In `react_on_rails`, `react_component` view helper works slightly differently. It takes two arguments: the component name, and options. Props is one of the options. Take a look at the following example: - - ```diff - - <%= react_component('Post', { title: 'New Post' }, { prerender: true }) %> - + <%= react_component('Post', { props: { title: 'New Post' }, prerender: true }) %> - ``` - -You can also check [react-rails-to-react-on-rails](https://github.com/shakacode/react-rails-example-app/tree/react-rails-to-react-on-rails) branch on [react-rails example app](https://github.com/shakacode/react-rails-example-app) for an example of migration from `react-rails` v3 to `react_on_rails` v13.4. - - -## Common Errors -### Getting warning for `Can't resolve 'react-dom/client'` in React < 18 - -You may see a warning like this when building a Webpack bundle using any version of React below 18. This warning can be safely [suppressed](https://webpack.js.org/configuration/other-options/#ignorewarnings) in your Webpack configuration. The following is an example of this suppression in `config/webpack/webpack.config.js`: - -```diff -- const { webpackConfig } = require('shakapacker') -+ const { webpackConfig, merge } = require('shakapacker') - -+const ignoreWarningsConfig = { -+ ignoreWarnings: [/Module not found: Error: Can't resolve 'react-dom\/client'/], -+}; - -- module.exports = webpackConfig -+ module.exports = merge({}, webpackConfig, ignoreWarningsConfig) -``` - -### Undefined Set -``` -ExecJS::ProgramError (identifier 'Set' undefined): - -(execjs):1 -``` -If you see any variation of this issue, see [Using TheRubyRacer](#using-therubyracer) - - -### Using TheRubyRacer -TheRubyRacer [hasn't updated LibV8](https://github.com/cowboyd/therubyracer/blob/master/therubyracer.gemspec#L20) (The library that powers Node.js) from v3 in 2 years, any new features are unlikely to work. - -LibV8 itself is already [beyond version 7](https://github.com/cowboyd/libv8/releases/tag/v7.3.492.27.1) therefore many serverside issues are caused by old JS engines and fixed by using an up to date one such as [MiniRacer](https://github.com/discourse/mini_racer) or [TheRubyRhino](https://github.com/cowboyd/therubyrhino) on JRuby. - -### HMR - -Check out [Enabling Hot Module Replacement (HMR)](https://github.com/shakacode/shakapacker/blob/master/docs/react.md#enabling-hot-module-replacement-hmr) in Shakapacker documentation. - -One caveat is that currently you [cannot Server-Side Render along with HMR](https://github.com/reactjs/react-rails/issues/925#issuecomment-415469572). - ## Related Projects - [webpacker-react](https://github.com/renchap/webpacker-react): Integration of React with Rails utilizing Webpack with Hot Module Replacement (HMR). diff --git a/docs/common-errors.md b/docs/common-errors.md new file mode 100644 index 000000000..3521e7a19 --- /dev/null +++ b/docs/common-errors.md @@ -0,0 +1,47 @@ +# Common Errors + + + + +- [Getting warning for `Can't resolve 'react-dom/client'` in React < 18](#getting-warning-for-cant-resolve-react-domclient-in-react--18) +- [Undefined Set](#undefined-set) +- [Using TheRubyRacer](#using-therubyracer) +- [HMR](#hmr) + + + +## Getting warning for `Can't resolve 'react-dom/client'` in React < 18 + +You may see a warning like this when building a Webpack bundle using any version of React below 18. This warning can be safely [suppressed](https://webpack.js.org/configuration/other-options/#ignorewarnings) in your Webpack configuration. The following is an example of this suppression in `config/webpack/webpack.config.js`: + +```diff +- const { webpackConfig } = require('shakapacker') ++ const { webpackConfig, merge } = require('shakapacker') + ++const ignoreWarningsConfig = { ++ ignoreWarnings: [/Module not found: Error: Can't resolve 'react-dom\/client'/], ++}; + +- module.exports = webpackConfig ++ module.exports = merge({}, webpackConfig, ignoreWarningsConfig) +``` + +## Undefined Set +``` +ExecJS::ProgramError (identifier 'Set' undefined): + +(execjs):1 +``` +If you see any variation of this issue, see [Using TheRubyRacer](#using-therubyracer) + + +## Using TheRubyRacer +TheRubyRacer [hasn't updated LibV8](https://github.com/cowboyd/therubyracer/blob/master/therubyracer.gemspec#L20) (The library that powers Node.js) from v3 in 2 years, any new features are unlikely to work. + +LibV8 itself is already [beyond version 7](https://github.com/cowboyd/libv8/releases/tag/v7.3.492.27.1) therefore many serverside issues are caused by old JS engines and fixed by using an up to date one such as [MiniRacer](https://github.com/discourse/mini_racer) or [TheRubyRhino](https://github.com/cowboyd/therubyrhino) on JRuby. + +## HMR + +Check out [Enabling Hot Module Replacement (HMR)](https://github.com/shakacode/shakapacker/blob/master/docs/react.md#enabling-hot-module-replacement-hmr) in Shakapacker documentation. + +One caveat is that currently you [cannot Server-Side Render along with HMR](https://github.com/reactjs/react-rails/issues/925#issuecomment-415469572). diff --git a/docs/component-generator.md b/docs/component-generator.md new file mode 100644 index 000000000..140a05932 --- /dev/null +++ b/docs/component-generator.md @@ -0,0 +1,138 @@ +# Component Generator + + + + +- [Use with JBuilder](#use-with-jbuilder) +- [Camelize Props](#camelize-props) +- [Changing Component Templates](#changing-component-templates) + + + + +You can generate a new component file with: + +```sh +rails g react:component ComponentName prop1:type prop2:type ... [options] +``` + +For example, + +```sh +rails g react:component Post title:string published:bool published_by:instanceOf{Person} +``` + +would generate: + +```JSX +var Post = createReactClass({ + propTypes: { + title: PropTypes.string, + published: PropTypes.bool, + publishedBy: PropTypes.instanceOf(Person) + }, + + render: function() { + return ( + + Title: {this.props.title} + Published: {this.props.published} + Published By: {this.props.publishedBy} + + ); + } +}); +``` + +The generator also accepts options: + +- `--es6`: generates a function component +- `--coffee`: use CoffeeScript + +For example, + +```sh +rails g react:component ButtonComponent title:string --es6 +``` + +would generate: + +```jsx +import React from "react" +import PropTypes from "prop-types" + +function ButtonComponent(props) { + return ( + + Title: {this.props.title} + + ); +} + +ButtonComponent.propTypes = { + title: PropTypes.string +}; + +export default ButtonComponent +``` + +**Note:** In a Shakapacker project, es6 template is the default template in the generator. + +Accepted PropTypes are: + +- Plain types: `any`, `array`, `bool`, `element`, `func`, `number`, `object`, `node`, `shape`, `string` +- `instanceOf` takes an optional class name in the form of `instanceOf{className}`. +- `oneOf` behaves like an enum, and takes an optional list of strings in the form of `'name:oneOf{one,two,three}'`. +- `oneOfType` takes an optional list of react and custom types in the form of `'model:oneOfType{string,number,OtherType}'`. + +Note that the arguments for `oneOf` and `oneOfType` must be enclosed in single quotes + to prevent your terminal from expanding them into an argument list. + +## Use with JBuilder + +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 +# BAD: returns a stringified array +json.array!(@messages) do |message| + json.extract! message, :id, :name + json.url message_url(message, format: :json) +end + +# GOOD: returns a stringified hash +json.messages(@messages) do |message| + json.extract! message, :id, :name + json.url message_url(message, format: :json) +end +``` + +## Camelize Props + +You can configure `camelize_props` option: + +```ruby +MyApp::Application.configure do + config.react.camelize_props = true # default false +end +``` + +Now, Ruby hashes given to `react_component(...)` as props will have their keys transformed from _underscore_- to _camel_-case, for example: + +```ruby +{ all_todos: @todos, current_status: @status } +# becomes: +{ "allTodos" => @todos, "currentStatus" => @status } +``` + +You can also specify this option in `react_component`: + +```erb +<%= react_component('HelloMessage', {name: 'John'}, {camelize_props: true}) %> +``` + +## Changing Component Templates + +To make simple changes to Component templates, copy the respective template file to your Rails project at `lib/templates/react/component/template_filename`. + +For example, to change the [ES6 Component template](https://github.com/reactjs/react-rails/blob/master/lib/generators/templates/component.es6.jsx), copy it to `lib/templates/react/component/component.es6.jsx` and modify it. diff --git a/docs/controller-actions.md b/docs/controller-actions.md new file mode 100644 index 000000000..3a0815507 --- /dev/null +++ b/docs/controller-actions.md @@ -0,0 +1,22 @@ +# Controller Actions + + + + + + + +Components can also be server-rendered directly from a controller action with the custom `component` renderer. For example: + +```ruby +class TodoController < ApplicationController + def index + @todos = Todo.all + render component: 'TodoList', props: { todos: @todos }, tag: 'span', class: 'todo' + end +end +``` + +You can also provide the "usual" `render` arguments: `content_type`, `layout`, `location` and `status`. By default, your current layout will be used and the component, rather than a view, will be rendered in place of `yield`. Custom data-* attributes can be passed like `data: {remote: true}`. + +Prerendering is set to `true` by default, but can be turned off with `prerender: false`. diff --git a/docs/get-started.md b/docs/get-started.md new file mode 100644 index 000000000..b82c5a6c1 --- /dev/null +++ b/docs/get-started.md @@ -0,0 +1,334 @@ +# Get Started + + + + +- [Use with Shakapacker](#use-with-shakapacker) + - [Component name](#component-name) + - [File naming](#file-naming) + - [Typescript support](#typescript-support) + - [Test component](#test-component) +- [Use with Asset Pipeline](#use-with-asset-pipeline) + - [Custom JSX Transformer](#custom-jsx-transformer) + - [Transform Plugin Options](#transform-plugin-options) + - [React.js versions](#reactjs-versions) + + + +## Use with Shakapacker + +1. Create a new Rails app: +Prevent installing default javascript dependencies by using `--skip-javascript` option: + +```bash +rails new my-app --skip-javascript +cd my-app +``` + +2. Install `shakapacker`: +```bash +bundle add shakapacker --strict +rails shakapacker:install +``` + +3. Install `react` and some other required npm packages: +```bash +yarn add react react-dom @babel/preset-react prop-types \ + css-loader style-loader mini-css-extract-plugin css-minimizer-webpack-plugin +``` + +Also update the Babel configuration in the `package.json` file: + +```diff +"babel": { + "presets": [ +- "./node_modules/shakapacker/package/babel/preset.js" ++ "./node_modules/shakapacker/package/babel/preset.js", ++ "@babel/preset-react" + ] +}, +``` + +4. Install `react-rails`: +```bash +$ bundle add 'react-rails' --strict +$ rails generate react:install +``` + +This gives you: + +- `app/javascript/components/` directory for your React components +- [`ReactRailsUJS`](./ujs.md) setup in `app/javascript/packs/application.js` +- `app/javascript/packs/server_rendering.js` for [server-side rendering](#server-side-rendering) + +5. Generate your first component: +```bash +$ rails g react:component HelloWorld greeting:string +``` + +You can also generate your component in a subdirectory: + +```bash +$ rails g react:component my_subdirectory/HelloWorld greeting:string +``` + +Note: Your component is added to `app/javascript/components/` by default. + +Note: If your component is in a subdirectory you will append the directory path to your erb component call. + +Example: +```erb +<%= react_component("my_subdirectory/HelloWorld", { greeting: "Hello from react-rails." }) %> +``` + +6. [Render it in a Rails view](./view-helper.md): + +```erb + +<%= react_component("HelloWorld", { greeting: "Hello from react-rails." }) %> +``` + +7. Lets Start the app: +```bash +$ rails s +``` +Output: greeting: Hello from react-rails", inspect webpage in your browser to see the change in tag props. + +8. Run dev server (optional) +In order to run dev server with HMR feature you need to parallely run: + +```bash +$ ./bin/shakapacker-dev-server +``` + +Note: On Rails 6 you need to specify `webpack-dev-server` host. To this end, update `config/initializers/content_security_policy.rb` and uncomment relevant lines. + +### Component name + +The component name tells `react-rails` where to load the component. For example: + +`react_component` call | component `require` +-----|----- +`react_component("Item")` | `require("Item")` +`react_component("items/index")` | `require("items/index")` +`react_component("items.Index")` | `require("items").Index` +`react_component("items.Index.Header")` | `require("items").Index.Header` + +This way, you can access top-level, default, or named exports. + +The `require.context` inserted into `packs/application.js` is used to load components. If you want to load components from a different directory, override it by calling `ReactRailsUJS.useContext`: + +```js +var myCustomContext = require.context("custom_components", true) +var ReactRailsUJS = require("react_ujs") +// use `custom_components/` for <%= react_component(...) %> calls +ReactRailsUJS.useContext(myCustomContext) +``` + +If `require` fails to find your component, [`ReactRailsUJS`](./ujs.md) falls back to the global namespace, described in [Use with Asset Pipeline](#use-with-asset-pipeline). + +In some cases, having multiple `require.context` entries may be desired. Examples of this include: + +- Refactoring a typical Rails application into a Rails API with an (eventually) separate Single Page Application (SPA). For this use case, one can add a separate pack in addition to the typical `application` one. React components can be shared between the packs but the new pack can use a minimal Rails view layout, different default styling, etc. +- In a larger application, you might find it helpful to split your JavaScript by routes/controllers to avoid serving unused components and improve your site performance by keeping bundles smaller. For example, you might have separate bundles for homepage, search, and checkout routes. In that scenario, you can add an array of `require.context` component directory paths via `useContexts` to `server_rendering.js`, to allow for [Server-Side Rendering](./server-side-rendering.md) across your application: + +```js +// server_rendering.js +var homepageRequireContext = require.context('homepage', true); +var searchRequireContext = require.context('search', true); +var checkoutRequireContext = require.context('checkout', true); + +var ReactRailsUJS = require('react_ujs'); +ReactRailsUJS.useContexts([ + homepageRequireContext, + searchRequireContext, + checkoutRequireContext +]); +``` +### File naming + +React-Rails supports plenty of file extensions such as: .js, .jsx.js, .js.jsx, .es6.js, .coffee, etcetera! +Sometimes this will cause a stumble when searching for filenames. + +Component File Name | `react_component` call +-----|----- +`app/javascript/components/samplecomponent.js` | `react_component("samplecomponent")` +`app/javascript/components/sample_component.js` | `react_component("sample_component")` +`app/javascript/components/SampleComponent.js` | `react_component("SampleComponent")` +`app/javascript/components/SampleComponent.js.jsx` | Has to be renamed to SampleComponent.jsx, then use `react_component("SampleComponent")` + +### Typescript support + +```bash +yarn add typescript @babel/preset-typescript +``` + +Babel won’t perform any type-checking on TypeScript code. To optionally use type-checking run: + +```bash +yarn add fork-ts-checker-webpack-plugin +``` + +Add `tsconfig.json` with the following content: + +```json +{ + "compilerOptions": { + "declaration": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": ["es6", "dom"], + "module": "es6", + "moduleResolution": "node", + "sourceMap": true, + "target": "es5", + "jsx": "react", + "noEmit": true + }, + "exclude": ["**/*.spec.ts", "node_modules", "vendor", "public"], + "compileOnSave": false +} +``` + +Then modify the webpack config to use it as a plugin: + +```js +// config/webpack/webpack.config.js +const { webpackConfig, merge } = require("shakapacker"); +const ForkTSCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); + +module.exports = merge(webpackConfig, { + plugins: [new ForkTSCheckerWebpackPlugin()], +}); +``` + +Doing this will allow React-Rails to support the .tsx extension. Additionally, it is recommended to add `ts` and `tsx` to the `server_renderer_extensions` in your application configuration: + +```ruby +config.react.server_renderer_extensions = ["jsx", "js", "tsx", "ts"] +``` + +### Test component + +You can use `assert_react_component` to test component render: + +```erb + + +<%= react_component("HelloWorld", { greeting: "Hello from react-rails.", info: { name: "react-rails" } }, { class: "hello-world" }) %> +``` + +```rb +class WelcomeControllerTest < ActionDispatch::IntegrationTest + test 'assert_react_component' do + get "/welcome" + assert_equal 200, response.status + + # assert rendered react component and check the props + assert_react_component "HelloWorld" do |props| + assert_equal "Hello from react-rails.", props[:greeting] + assert_equal "react-rails", props[:info][:name] + assert_select "[class=?]", "hello-world" + end + + # or just assert component rendered + assert_react_component "HelloWorld" + end +end +``` + +## Use with Asset Pipeline + +`react-rails` provides a pre-bundled React.js & a UJS driver to the Rails asset pipeline. Get started by adding the `react-rails` gem: + +```ruby +gem 'react-rails' +``` + +And then install the react generator: + +``` +$ rails g react:install +``` + +Then restart your development server. + +This will: + +- add some `//= require`s to `application.js` +- add a `components/` directory for React components +- add `server_rendering.js` for [server-side rendering](./server-side-rendering.md) + +Now, you can create React components in `.jsx` files: + +```JSX +// app/assets/javascripts/components/post.jsx + +window.Post = createReactClass({ + render: function() { + return

{this.props.title}

+ } +}) + +// or, equivalent: +class Post extends React.Component { + render() { + return

{this.props.title}

+ } +} +``` + +Then, you can render those [components in views](./view-helper.md): + +```erb +<%= react_component("Post", {title: "Hello World"}) %> +``` + +Components must be accessible from the top level, but they may be namespaced, for example: + +```erb +<%= react_component("Comments.NewForm", {post_id: @post.id}) %> + +``` + +### Custom JSX Transformer + +`react-rails` uses a transformer class to transform JSX in the asset pipeline. The transformer is initialized once, at boot. You can provide a custom transformer to `config.react.jsx_transformer_class`. The transformer must implement: + +- `#initialize(options)`, where options is the value passed to `config.react.jsx_transform_options` +- `#transform(code_string)` to return a string of transformed code + +`react-rails` provides two transformers, `React::JSX::BabelTransformer` (which uses [ruby-babel-transpiler](https://github.com/babel/ruby-babel-transpiler)) and `React::JSX::JSXTransformer` (which uses the deprecated `JSXTransformer.js`). + +#### Transform Plugin Options + +To supply additional transform plugins to your JSX Transformer, assign them to `config.react.jsx_transform_options` + +`react-rails` uses the Babel version of the `babel-source` gem. + +For example, to use `babel-plugin-transform-class-properties` : + + config.react.jsx_transform_options = { + optional: ['es7.classProperties'] + } + +### React.js versions + +`//= require react` brings `React` into your project. + +By default, React's [development version] is provided to `Rails.env.development`. You can override the React build with a config: + +```ruby +# Here are the defaults: +# config/environments/development.rb +MyApp::Application.configure do + config.react.variant = :development +end + +# config/environments/production.rb +MyApp::Application.configure do + config.react.variant = :production +end +``` + +Be sure to restart your Rails server after changing these files. See [VERSIONS.md](https://github.com/reactjs/react-rails/blob/master/VERSIONS.md) to learn which version of React.js is included with your `react-rails` version. In some edge cases you may need to bust the sprockets cache with `rake tmp:clear` diff --git a/docs/migrating-from-react-rails-to-react_on_rails.md b/docs/migrating-from-react-rails-to-react_on_rails.md new file mode 100644 index 000000000..53394177d --- /dev/null +++ b/docs/migrating-from-react-rails-to-react_on_rails.md @@ -0,0 +1,63 @@ +# Migrating from `react-rails` to `react_on_rails` + + + + +- [Why migrate?](#why-migrate) +- [Steps to migrate](#steps-to-migrate) + + + + +## Why migrate? + +[`react_on_rails`](https://github.com/shakacode/react_on_rails/) offers several additional features for a Rails + React application. The following is a table of features comparison. + +| **Feature** | **react-rails** | **react-on-rails** | +| ----------------------- |:---------------:|:------------------:| +| Sprockets | ✅ | ❌ | +| Shakapacker | ✅ | ✅ | +| SSR | ✅ | ✅ | +| SSR with HMR | ✅ | ✅ | +| SSR with React-Router | ❌ | ✅ | +| SSR with Code Splitting | ❌ | ✅ | +| Node SSR | ❌ | ✅ | +| Advanced Redux support | ❌ | ✅ | +| ReScript support | ❌ | ✅ | +| I18n support | ❌ | ✅ | + +`react_on_rails` offers better performance and bundle optimizations, especially with the option of getting a subscription to `react_on_rails_pro`. + +## Steps to migrate + +In this guide, it is assumed that you have upgraded the `react-rails` project to use `shakapacker` version 7. To this end, check out [Shakapacker v7 upgrade guide](https://github.com/shakacode/shakapacker/tree/master/docs/v7_upgrade.md). Upgrading `react-rails` to version 3 can make the migration smoother but it is not required. + +1. Update Deps + + 1. Replace `react-rails` in `Gemfile` with the latest version of `react_on_rails` and run `bundle install`. + 2. Remove `react_ujs` from `package.json` and run `yarn install`. + 3. Commit changes! + +2. Run `rails g react_on_rails:install` but do not commit the change. `react_on_rails` installs node dependencies and also creates sample react component, Rails view/controller, and update `config/routes.rb`. + +3. Adapt the project: Check the changes and carefully accept, reject, or modify them as per your project's needs. Besides changes in `config/shakapacker` or `babel.config` which are project-specific, here are the most noticeable changes to address: + + 1. Check webpack config files at `config/webpack/*`. If coming from `react-rails` v3, the changes are minor since you have already made separate configurations for client and server bundles. The most important change here is to notice the different names for the server bundle entry file. You may choose to stick with `server_rendering.js` or use `server-bundle.js` which is the default name in `react_on_rails`. The decision made here, affects the other steps. + + 2. In `app/javascript` directory you may notice some changes. + + 1. `react_on_rails` by default uses `bundles` directory for the React components. You may choose to rename `components` into `bundles` to follow the convention. + + 2. `react_on_rails` uses `client-bundle.js` and `server-bundle.js` instead of `application.js` and `server_rendering.js`. There is nothing special about these names. It can be set to use any other name (as mentioned above). If you too choose to follow the new names, consider updating the relevant `javascript_pack_tag` in your Rails views. + + 3. Update the content of these files to register your React components for client or server-side rendering. Checking the generated files by `react_on_rails` installation process should give enough hints. + + 3. Check Rails views. In `react_on_rails`, `react_component` view helper works slightly differently. It takes two arguments: the component name, and options. Props is one of the options. Take a look at the following example: + + ```diff + - <%= react_component('Post', { title: 'New Post' }, { prerender: true }) %> + + <%= react_component('Post', { props: { title: 'New Post' }, prerender: true }) %> + ``` + +You can also check [react-rails-to-react-on-rails](https://github.com/shakacode/react-rails-example-app/tree/react-rails-to-react-on-rails) branch on [react-rails example app](https://github.com/shakacode/react-rails-example-app) for an example of migration from `react-rails` v3 to `react_on_rails` v13.4. + diff --git a/docs/server-side-rendering.md b/docs/server-side-rendering.md new file mode 100644 index 000000000..55c770991 --- /dev/null +++ b/docs/server-side-rendering.md @@ -0,0 +1,110 @@ +# Server-Side Rendering + + + + +- [Configuration](#configuration) +- [JavaScript State](#javascript-state) +- [Custom Server Renderer](#custom-server-renderer) + + + + +You can render React components inside your Rails server with `prerender: true`: + +```erb +<%= react_component('HelloMessage', {name: 'John'}, {prerender: true}) %> + +
+

Hello, John!

+
+``` + +_(It will also be mounted by the [UJS](./ujs.md) on page load.)_ + +Server rendering is powered by [`ExecJS`](https://github.com/rails/execjs) and subject to some requirements: + +- `react-rails` must load your code. By convention, it uses `server_rendering.js`, which was created +by the install task. This file must include your components _and_ their dependencies (eg, Underscore.js). +- Requires separate compilations for server & client bundles (see [Webpack config](https://github.com/reactjs/react-rails/tree/master/test/dummy/config/webpack)) +- Your code can't reference `document` or `window`. Prerender processes don't have access to `document` or `window`, +so jQuery and some other libs won't work in this environment :( + +`ExecJS` supports many backends. CRuby users will get the best performance from [`mini_racer`](https://github.com/discourse/mini_racer#performance). + +## Configuration + +Server renderers are stored in a pool and reused between requests. Threaded Rubies (eg jRuby) may see a benefit to increasing the pool size beyond the default `0`. + +These are the default configurations: + +```ruby +# config/application.rb +# These are the defaults if you don't specify any yourself +module MyApp + class Application < Rails::Application + # Settings for the pool of renderers: + config.react.server_renderer_pool_size ||= 1 # ExecJS doesn't allow more than one on MRI + config.react.server_renderer_timeout ||= 20 # seconds + config.react.server_renderer = React::ServerRendering::BundleRenderer + config.react.server_renderer_options = { + files: ["server_rendering.js"], # files to load for prerendering + replay_console: true, # if true, console.* will be replayed client-side + } + # Changing files matching these dirs/exts will cause the server renderer to reload: + config.react.server_renderer_extensions = ["jsx", "js"] + config.react.server_renderer_directories = ["/app/assets/javascripts", "/app/javascript/"] + end +end +``` + +## JavaScript State + +Some of ExecJS's backends are stateful (eg, mini_racer, therubyracer). This means that any side-effects of a prerender will affect later renders with that renderer. + +To manage state, you have a couple options: + +- Make a custom renderer with `#before_render` / `#after_render` hooks as [described below](#custom-server-renderer) +- Use `per_request_react_rails_prerenderer` to manage state for a whole controller action. + +To check out a renderer for the duration of a controller action, call the `per_request_react_rails_prerenderer` helper in the controller class: + +```ruby +class PagesController < ApplicationController + # Use the same React server renderer for the entire request: + per_request_react_rails_prerenderer +end +``` + +Then, you can access the ExecJS context directly with `react_rails_prerenderer.context`: + +```ruby +def show + react_rails_prerenderer # => # + react_rails_prerenderer.context # => # + + # Execute arbitrary JavaScript code + # `self` is the global context + react_rails_prerenderer.context.exec("self.Store.setup()") + render :show + react_rails_prerenderer.context.exec("self.Store.teardown()") +end +``` + +`react_rails_prerenderer` may also be accessed in before- or after-actions. + +## Custom Server Renderer + +`react-rails` depends on a renderer class for rendering components on the server. You can provide a custom renderer class to `config.react.server_renderer`. The class must implement: + +- `#initialize(options={})`, which accepts the hash from `config.react.server_renderer_options` +- `#render(component_name, props, prerender_options)` to return a string of HTML + +`react-rails` provides two renderer classes: `React::ServerRendering::ExecJSRenderer` and `React::ServerRendering::BundleRenderer`. + +`ExecJSRenderer` offers two other points for extension: + +- `#before_render(component_name, props, prerender_options)` to return a string of JavaScript to execute _before_ calling `React.render` +- `#after_render(component_name, props, prerender_options)` to return a string of JavaScript to execute _after_ calling `React.render` + +Any subclass of `ExecJSRenderer` may use those hooks (for example, `BundleRenderer` uses them to handle `console.*` on the server). diff --git a/docs/ujs.md b/docs/ujs.md new file mode 100644 index 000000000..1df6911e3 --- /dev/null +++ b/docs/ujs.md @@ -0,0 +1,91 @@ +# UJS + + + + +- [Mounting & Unmounting](#mounting--unmounting) +- [Event Handling](#event-handling) +- [`getConstructor`](#getconstructor) + + + + +`react-rails`'s JavaScript is available as `"react_ujs"` in the asset pipeline or from NPM. It attaches itself to the window as `ReactRailsUJS`. + +## Mounting & Unmounting + +Usually, `react-rails` mounts & unmounts components automatically as described in [Event Handling](#event-handling) below. + +You can also mount & unmount components from `<%= react_component(...) %>` tags using UJS: + +```js +// Mount all components on the page: +ReactRailsUJS.mountComponents() +// Mount components within a selector: +ReactRailsUJS.mountComponents(".my-class") +// Mount components within a specific node: +ReactRailsUJS.mountComponents(specificDOMnode) + +// Unmounting works the same way: +ReactRailsUJS.unmountComponents() +ReactRailsUJS.unmountComponents(".my-class") +ReactRailsUJS.unmountComponents(specificDOMnode) +``` + +You can use this when the DOM is modified by AJAX calls or modal windows. + +## Event Handling + +`ReactRailsUJS` checks for various libraries to support their page change events: + +- `Turbolinks` +- `pjax` +- `jQuery` +- Native DOM events + +`ReactRailsUJS` will automatically mount components on `<%= react_component(...) %>` tags and unmount them when appropriate. + +If you need to re-detect events, you can call `detectEvents`: + +```js +// Remove previous event handlers and add new ones: +ReactRailsUJS.detectEvents() +``` + +For example, if `Turbolinks` is loaded _after_ `ReactRailsUJS`, you'll need to call this again. This function removes previous handlers before adding new ones, so it's safe to call as often as needed. + +If `Turbolinks` is `import`ed via Shakapacker (and thus not available globally), `ReactRailsUJS` will be unable to locate it. To fix this, you can temporarily add it to the global namespace: + +```js +// Order is particular. First start Turbolinks: +Turbolinks.start(); +// Add Turbolinks to the global namespace: +window.Turbolinks = Turbolinks; +// Remove previous event handlers and add new ones: +ReactRailsUJS.detectEvents(); +// (Optional) Clean up global namespace: +delete window.Turbolinks; +``` + +## `getConstructor` + +Components are loaded with `ReactRailsUJS.getConstructor(className)`. This function has two default implementations, depending on if you're using the asset pipeline or Shakapacker: + +- On the asset pipeline, it looks up `className` in the global namespace (`ReactUJS.constructorFromGlobal`). +- On Shakapacker, it `require`s files and accesses named exports, as described in [Use with Shakapacker](./get-started.md#use-with-shakapacker), falling back to the global namespace (`ReactUJS.constructorFromRequireContextWithGlobalFallback`). + +You can override this function to customize the mapping of name-to-constructor. [Server-side rendering](./server-side-rendering.md) also uses this function. + +For example, the fallback behavior of +`ReactUJS.constructorFromRequireContextWithGlobalFallback` can sometimes make +server-side rendering errors hard to debug as it will swallow the original error +(more info +[here](https://github.com/reactjs/react-rails/issues/264#issuecomment-552326663)). +`ReactUJS.constructorFromRequireContext` is provided for this reason. You can +use it like so: + +```js +// Replaces calls to `ReactUJS.useContext` +ReactUJS.getConstructor = ReactUJS.constructorFromRequireContext(require.context('components', true)); +``` + diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 000000000..fa1f79976 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,33 @@ +# Upgrading + + + + +- [2.7 to 3.0](#27-to-30) +- [2.3 to 2.4](#23-to-24) + + + + +## 2.7 to 3.0 +- Keep your `react_ujs` up to date: `yarn upgrade` +- **Drop support for Webpacker:** Before any ReactRails upgrade, make sure upgrading from Webpacker to Shakapacker 7. For more information check out Shakapacker +- **SSR:** ReactRails 3.x requires separate compilations for server & client bundles. See [Webpack config](https://github.com/reactjs/react-rails/tree/master/test/dummy/config/webpack) directory in the dummy app to addapt the new implementation. + +## 2.3 to 2.4 + +Keep your `react_ujs` up to date, `yarn upgrade` + +React-Rails 2.4.x uses React 16+ which no longer has React Addons. Therefore the pre-bundled version of react no longer has an addons version, if you need addons still, there is the 2.3.1+ version of the gem that still has addons. + +If you need to make changes in your components for the prebundled react, see the migration docs here: + +- https://reactjs.org/blog/2016/11/16/react-v15.4.0.html +- https://reactjs.org/blog/2017/04/07/react-v15.5.0.html +- https://reactjs.org/blog/2017/06/13/react-v15.6.0.html + + +For the vast majority of cases this will get you most of the migration: +- global find+replace `React.Prop` -> `Prop` +- add `import PropTypes from 'prop-types'` (Webpacker only) +- re-run `bundle exec rails webpacker:install:react` to update npm packages (Webpacker only) diff --git a/docs/view-helper.md b/docs/view-helper.md new file mode 100644 index 000000000..a61278112 --- /dev/null +++ b/docs/view-helper.md @@ -0,0 +1,49 @@ +# View Helper + + + + +- [Custom View Helper](#custom-view-helper) + + + + +`react-rails` includes a view helper and an [unobtrusive JavaScript driver](./ujs.md) which work together to put React components on the page. + +The view helper (`react_component`) puts a `div` on the page with the requested component class & props. For example: + +```erb +<%= react_component('HelloMessage', name: 'John') %> + +
+``` + +On page load, the [`react_ujs` driver](./ujs.md) will scan the page and mount components using `data-react-class` +and `data-react-props`. + +The view helper's signature is: + +```ruby +react_component(component_class_name, props={}, html_options={}) +``` + +- `component_class_name` is a string which identifies a component. See [getConstructor](./ujs.md#getconstructor) for details. +- `props` is either: + - an object that responds to `#to_json`; or + - an already-stringified JSON object (see [JBuilder note](./component-generator.md#use-with-jbuilder) below). +- `html_options` may include: + - `tag:` to use an element other than a `div` to embed `data-react-class` and `data-react-props`. + - `prerender: true` to render the component on the server. + - `camelize_props` to [transform a props hash](./component-generator.md#camelize-props) + - `**other` Any other arguments (eg `class:`, `id:`) are passed through to [`content_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag). + + +## Custom View Helper + +`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(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`. diff --git a/test/react/server_rendering/manifest_container_test.rb b/test/react/server_rendering/manifest_container_test.rb index 740050ef1..1d8833cdc 100644 --- a/test/react/server_rendering/manifest_container_test.rb +++ b/test/react/server_rendering/manifest_container_test.rb @@ -23,7 +23,7 @@ def teardown def test_find_asset_gets_asset_contents application_js_content = @manifest_container.find_asset("application.js") - assert(application_js_content.length > 50_000, "It's the compiled file") + assert_operator(application_js_content.length, :>, 50_000, "It's the compiled file") end end end diff --git a/test/react/server_rendering/yaml_manifest_container_test.rb b/test/react/server_rendering/yaml_manifest_container_test.rb index 56fdc9903..14ac45f9d 100644 --- a/test/react/server_rendering/yaml_manifest_container_test.rb +++ b/test/react/server_rendering/yaml_manifest_container_test.rb @@ -17,7 +17,7 @@ def teardown def test_find_asset_gets_asset_contents application_js_content = @manifest_container.find_asset("application.js") - assert(application_js_content.length > 50_000, "It's the compiled file") + assert_operator(application_js_content.length, :>, 50_000, "It's the compiled file") end end end