From 0cd0e823879d4788f5a81e8ccd19aae591cf6a73 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 10 Jul 2016 18:11:16 -1000 Subject: [PATCH] Fixes to avoid yak shave with redux_store 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. --- README.md | 8 ++++++-- node_package/src/StoreRegistry.js | 25 +++++++++++++++--------- node_package/tests/StoreRegistry.test.js | 11 ++++++++++- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8c6163ac55..3d1338a0e4 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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). diff --git a/node_package/src/StoreRegistry.js b/node_package/src/StoreRegistry.js index 7622296fce..fb8185f3c8 100644 --- a/node_package/src/StoreRegistry.js +++ b/node_package/src/StoreRegistry.js @@ -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}].`); } }, diff --git a/node_package/tests/StoreRegistry.test.js b/node_package/tests/StoreRegistry.test.js index f8a17e1dc7..c4b2686c82 100644 --- a/node_package/tests/StoreRegistry.test.js +++ b/node_package/tests/StoreRegistry.test.js @@ -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 }); @@ -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 ' + @@ -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.' ); }); -