Skip to content

Commit

Permalink
Fixes to avoid yak shave with redux_store
Browse files Browse the repository at this point in the history
We had a major yak shave when figuring out why setting up the store from
a content_for block caused majore issues.

The following doc changes and enhanced error messages will save others
from this pain.
  • Loading branch information
justin808 committed Jul 11, 2016
1 parent 87d2fc3 commit 0cd0e82
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 12 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ That will install the latest version and update your package.json.
## How it Works
The generator installs your webpack files in the `client` folder. Foreman uses webpack to compile your code and output the bundled results to `app/assets/webpack`, which are then loaded by sprockets. These generated bundle files have been added to your `.gitignore` for your convenience.
Inside your Rails views, you can now use the `react_component` helper method provided by React on Rails. You can pass props directly to the react component helper. You can also initialize a Redux store with view helper `redux_store` so that the store can be shared amongst multiple React components. Your best bet is to scan the code inside of the [/spec/dummy](spec/dummy) sample app.
Inside your Rails views, you can now use the `react_component` helper method provided by React on Rails. You can pass props directly to the react component helper. You can also initialize a Redux store with view or controller helper `redux_store` so that the store can be shared amongst multiple React components. See the docs for and scan the code inside of the [/spec/dummy](spec/dummy) sample app.
### Client-Side Rendering vs. Server-Side Rendering
In most cases, you should use the `prerender: false` (default behavior) with the provided helper method to render the React component from your Rails views. In some cases, such as when SEO is vital or many users will not have JavaScript enabled, you can enable server-rendering by passing `prerender: true` to your helper, or you can simply change the default in `config/initializers/react_on_rails`.
Expand Down Expand Up @@ -325,9 +325,13 @@ Include the module ReactOnRails::Controller in your controller, probably in Appl
2. In your component definition, you'll call `ReactOnRails.getStore('storeName')` to get the hydrated Redux store to attach to your components.
+ **props:** Named parameter `props`. ReactOnRails takes care of setting up the hydration of your store with props from the view.

For an example, see [spec/dummy/app/controllers/pages_controller.rb](spec/dummy/app/controllers/pages_controller.rb).
For an example, see [spec/dummy/app/controllers/pages_controller.rb](spec/dummy/app/controllers/pages_controller.rb). Note, this is preferable to using the equivalent view_helper `redux_store` in that you can be assured that the store is initialized before your components.

#### View Helper
`redux_store(store_name, props: {})`

Same API as the controller extension. **HOWEVER**, we recommend the controller extension instead because the Rails executes the template code in the controller action's view file (`erb`, `haml`, `slim`, etc.) before the layout. So long as you call `redux_store` at the beginning of your action's view file, this will work. However, it's an easy mistake to put this call in the wrong place. Calling `redux_store` in the controller action ensures proper load order, regardless of where you call this in the controller action. Note, you won't know of this subtle ordering issue until you server render and you find that your store is not hydrated properly.

`redux_store_hydration_data`

Place this view helper (no parameters) at the end of your shared layout. This tell ReactOnRails where to client render the redux store hydration data. Since we're going to be setting up the stores in the controllers, we need to know where on the view to put the client side rendering of this hydration data, which is a hidden div with a matching class that contains a data props. For an example, see [spec/dummy/app/views/layouts/application.html.erb](spec/dummy/app/views/layouts/application.html.erb).
Expand Down
25 changes: 16 additions & 9 deletions node_package/src/StoreRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,22 @@ export default {
getStore(name, throwIfMissing = true) {
if (_stores.has(name)) {
return _stores.get(name);
} else {
if (throwIfMissing) {
const storeKeys = Array.from(_stores.keys()).join(', ');
console.log('storeKeys', storeKeys);
throw new Error(`Could not find hydrated store with name '${name}'. ` +
`Hydrated store names include [${storeKeys}].`);
} else {
return;
}
}

const storeKeys = Array.from(_stores.keys()).join(', ');

if (storeKeys.length === 0) {
const msg = `There are no stores hydrated and you are requesting the store ` +
`${name}. This can happen if you are server rendering and you do not call ` +
`redux_store near the top of your controller action's view (not the layout) ` +
`and before any call to react_component.`;
throw new Error(msg);
}

if (throwIfMissing) {
console.log('storeKeys', storeKeys);
throw new Error(`Could not find hydrated store with name '${name}'. ` +
`Hydrated store names include [${storeKeys}].`);
}
},

Expand Down
11 changes: 10 additions & 1 deletion node_package/tests/StoreRegistry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ function storeGenerator2(props) {
return createStore(reducer, props);
};

test('StoreRegistry throws error for retrieving unregistered store', (assert) => {
assert.plan(1);
StoreRegistry.stores().clear();
assert.throws(() => StoreRegistry.getStore('foobar'),
/There are no stores hydrated and you are requesting the store/,
'Expected an exception for calling StoreRegistry.getStore with no registered stores.'
);
});

test('StoreRegistry registers and retrieves generator function stores', (assert) => {
assert.plan(2);
StoreRegistry.register({ storeGenerator, storeGenerator2 });
Expand All @@ -40,6 +49,7 @@ test('StoreRegistry returns undefined for retrieving unregistered store, ' +
'passing throwIfMissing = false',
(assert) => {
assert.plan(1);
StoreRegistry.setStore('foobarX', {});
const actual = StoreRegistry.getStore('foobar', false);
const expected = undefined;
assert.equals(actual, expected, 'StoreRegistry.get should return undefined for missing ' +
Expand All @@ -64,4 +74,3 @@ test('StoreRegistry throws error for retrieving unregistered hydrated store', (a
'Expected an exception for calling StoreRegistry.getStore with an invalid name.'
);
});

0 comments on commit 0cd0e82

Please sign in to comment.