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

Allowing a render function to return JSX and console.error a message. #1280

Merged
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
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ bracketSpacing: true
jsxBracketSameLine: false
parser: flow

extends:
- tslint-config-prettier

overrides:
- files: "*.@(css|scss)"
options:
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,17 @@ Another way is to use a separate webpack configuration file that can use a diffe

For details on techniques to use different code for client and server rendering, see: [How to use different versions of a file for client and server rendering](https://forum.shakacode.com/t/how-to-use-different-versions-of-a-file-for-client-and-server-rendering/1352). (_Requires creating a free account._)

## Specifying Your React Components: Direct or render functions
## Specifying Your React Components: Register directly or use render-functions

You have two ways to specify your React components. You can either register the React component (either function or class component) directly, or you can create a function that returns a React component, which we using the name of a "render function". Creating a function has the following benefits:
You have two ways to specify your React components. You can either register the React component (either function or class component) directly, or you can create a function that returns a React component, which we using the name of a "render-function". Creating a render-function allows:

1. You have access to the `railsContext`. See documentation for the railsContext in terms of why you might need it. You **need** a render function to access the `railsContext`.
2. You can use the passed-in props to initialize a redux store or set up react-router.
3. You can return different components depending on what's in the props.

Note, the return value of a **render function** should be JSX or an HTML string. Do not return a
function.
Note, the return value of a **render function** should be either a React Function or Class Component, or an object representing server rendering results.

Do not return a React Element (JSX).

ReactOnRails will automatically detect a registered render function by the fact that the function takes
more than 1 parameter. In other words, if you want the ability to provide a function that returns the
Expand All @@ -257,7 +258,7 @@ If you're not using this parameter, declare your function with the unused param:
```js
const MyComponentGenerator = (props, _railsContext) => {
if (props.print) {
// Wrap in a function so this is a React Component
// This is a React FunctionComponent because it is wrapped in a function.
return () => <H1>{JSON.stringify(props)}</H1>;
}
}
Expand Down
7 changes: 3 additions & 4 deletions docs/basics/render-functions-and-railscontext.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const MyAppComponent = (props, railsContext) => (

// the props get passed again, but we ignore since we use a closure
// or should we
(_props) =>
() =>
<div>
<p>props are: {JSON.stringify(props)}</p>
<p>railsContext is: {JSON.stringify(railsContext)}
Expand Down Expand Up @@ -140,13 +140,12 @@ export default (props, railsContext) => {

There's no reason that the railsContext would ever get passed to your React component unless the value is explicitly put into the props used for rendering. If you create a react component, rather than a render-function, for use by React on Rails, then you get whatever props are passed in from the view helper, which **does not include the Rails Context**. It's trivial to wrap your component in a "render-function" to return a new component that takes both:

POSSIBLE ENHANCEMENT: Would it be better to offer the ability to send props this way if a flag is passed in
the `react_component` helper? Probably not, since this is so easily done.

```js
import React from 'react';
import AppComponent from './AppComponent';
const AppComponentWithRailsContext = (props, railsContext) => (
// Create a React Function Component so you can
// use the React Hooks API in this React Function Component
() => <AppComponent {...{...props, railsContext}}/>
)
export default AppComponentWithRailsContext;
Expand Down
7 changes: 2 additions & 5 deletions lib/react_on_rails/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,8 @@ def self.server_bundle_js_file_path
# Either:
# 1. Using same bundle for both server and client, so server bundle will be hashed in manifest
# 2. Using a different bundle (different Webpack config), so file is not hashed, and
# bundle_js_path will throw.
# 3. Not using webpacker, and bundle_js_path always returns

# Note, server bundle should not be in the manifest
# If using webpacker gem per https://github.com/rails/webpacker/issues/571
# bundle_js_path will throw so the default path is used without a hash.
# 3. Not using webpacker, and this method returns the bundle_js_file_path
return @server_bundle_path if @server_bundle_path && !Rails.env.development?

bundle_name = ReactOnRails.configuration.server_bundle_js_file
Expand Down
4 changes: 4 additions & 0 deletions lib/react_on_rails/webpacker_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ def self.bundle_js_uri_from_webpacker(bundle_name)
# Next line will throw if the file or manifest does not exist
hashed_bundle_name = Webpacker.manifest.lookup!(bundle_name)

# If someday we add support for hashing the server-bundle and having that built
# by a webpack watch process and not served by the webpack-dev-server, then we
# need to add an extra config value "same_bundle_for_client_and_server" where a value of false
# would mean that the bundle is created by a separate webpack watch process.
if Webpacker.dev_server.running?
"#{Webpacker.dev_server.protocol}://#{Webpacker.dev_server.host_with_port}#{hashed_bundle_name}"
else
Expand Down
17 changes: 12 additions & 5 deletions node_package/src/createReactOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,20 @@ export default function createReactOutput({
// We just return at this point, because calling function knows how to handle this case and
// we can't call React.createElement with this type of Object.
return (renderFunctionResult as ServerRenderResult);
} // else we'll be calling React.createElement
// TODO: Can we detect if this is a React Element or a React Function Component?
// If already a ReactElement, then just return it.
// If a component, then wrap in an element
}

const reactComponent = renderFunctionResult as ReactComponent;
if (React.isValidElement(renderFunctionResult)) {
// If already a ReactElement, then just return it.
console.error(
`Warning: ReactOnRails: Your registered render-function (ReactOnRails.register) for ${name}
incorrectly returned a React Element (JSX). Instead, return a React Function Component by
wrapping your JSX in a function. ReactOnRails v13 will error on this, as React Hooks do not
work if you rerturn JSX.`);
return renderFunctionResult;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, this skips the maybe essential call of React.createElement(component, props).

This 100% breaks if hooks are used.

Should we just error out and force the change to wrap the result JSX in a function with zero params?

or should we make the upgrade easier for 99% of the cases that did not use Hooks.


// If a component, then wrap in an element
const reactComponent = renderFunctionResult as ReactComponent;
return React.createElement(reactComponent, props);
}
// else
Expand Down
4 changes: 2 additions & 2 deletions package-scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ scripts:
format:
default:
description: Format files using prettier.
script: concurrently --prefix "[{name}]" --names "js,json" -c "yellow,magenta,green" "nps format.js" "nps format.json"
script: concurrently --prefix "[{name}]" --names "ts,js,json" -c "yellow,magenta,green" "nps format.js" "nps format.json"
listDifferent:
description: Check that all files were formatted using prettier.
script: |
concurrently \
--prefix "[{name}]" \
--names "js,json" \
--names "ts,js,json" \
-c "yellow,magenta" \
"nps format.js.listDifferent" \
"nps format.json.listDifferent"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"redux": "^4.0.1",
"release-it": "^8.2.0",
"ts-jest": "^25.2.1",
"tslint-config-prettier": "^1.18.0",
"typescript": "^3.8.3",
"webpack": "^3.4.1",
"webpack-manifest-plugin": "^1.2.1"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<%= react_component("ContextFunctionReturnInvalidJSX", props: @app_props_server_render, prerender: false, trace: true, id: "HelloWorld-react-component-0") %>
<%= react_component("ContextFunctionReturnInvalidJSX", props: @app_props_hello, prerender: false, trace: true, id: "HelloWorld-react-component-0") %>
<hr/>

<h1>Example of error from failing to wrap the result in a function.</h1>
<h1>Example of Console.error from failing to wrap the result in a function.</h1>

See console log for expected warning and errors.
See console log for expected error.

Note, the component still rendered. In the next version, this should just error.
2 changes: 1 addition & 1 deletion spec/dummy/app/views/shared/_header.erb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
<%= link_to "Server Render With Timout", server_render_with_timeout_path %>
</li>
<li>
<%= link_to "Incorrectly returning JSX rather than function", context_function_return_jsx_path %>
<%= link_to "Incorrectly returning React Element (JSX) rather than React Function Component", context_function_return_jsx_path %>
</li>
<li>
<%= link_to "Incorrectly wrapping a pure component in a function", pure_component_wrapped_in_function_path %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import RailsContext from './RailsContext';

const ContextFunctionReturnInvalidJSX = (props, railsContext) => (
<>
<h3 className={css.brightColor}>Hello, {props.name}!</h3>
<h3 className={css.brightColor}>Hello, {props.helloWorldData.name}!</h3>
<p>Rails Context :</p>
<RailsContext {...{ railsContext }} />
</>
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/client/app/packs/clientRegistration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ ReactOnRails.register({
SetTimeoutLoggingApp,
HelloWorldHooks,
HelloWorldHooksContext,
ContextFunctionReturnJSX: ContextFunctionReturnInvalidJSX,
ContextFunctionReturnInvalidJSX,
PureComponentWrappedInFunction,
});

Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9111,6 +9111,11 @@ tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==

tslint-config-prettier@^1.18.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37"
integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==

tsutils@^3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
Expand Down