Skip to content

Commit

Permalink
Merge pull request #470 from shakacode/fix-doc-on-shared-store
Browse files Browse the repository at this point in the history
Fixes to avoid yak shave with redux_store
  • Loading branch information
justin808 authored Jul 11, 2016
2 parents d0f936a + a7f600a commit 4a3933d
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 14 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 `redux_store` below 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
7 changes: 6 additions & 1 deletion node_package/src/ReactOnRails.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ ctx.ReactOnRails = {
* Allows registration of store generators to be used by multiple react components on one Rails
* view. store generators are functions that take one arg, props, and return a store. Note that
* the setStore API is different in tha it's the actual store hydrated with props.
* @param stores (key is store name, value is the store generator)
* @param stores (keys are store names, values are the store generators)
*/
registerStore(stores) {
if (!stores) {
throw new Error(`Called ReactOnRails.registerStores with a null or undefined, rather than ` +
`an Object with keys being the store names and the values are the store generators.`);
}

StoreRegistry.register(stores);
},

Expand Down
31 changes: 21 additions & 10 deletions node_package/src/StoreRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const _stores = new Map();
export default {
/**
* Register a store generator, a function that takes props and returns a store.
* @param storeGenerators { name: component }
* @param storeGenerators { name1: storeGenerator1, name2: storeGenerator2 }
*/
register(storeGenerators) {
Object.keys(storeGenerators).forEach(name => {
Expand All @@ -16,6 +16,10 @@ export default {
}

const store = storeGenerators[name];
if (!store) {
throw new Error(`Called ReactOnRails.registerStores with a null or undefined as a value ` +
`for the store generator with key ${name}.`);
}

_storeGenerators.set(name, store);
});
Expand All @@ -31,15 +35,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
22 changes: 22 additions & 0 deletions node_package/tests/ReactOnRails.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,28 @@ test('serverRenderReactComponent throws error for invalid options', (assert) =>
);
});

test('registerStore throws if passed a falsey object (null, undefined, etc)', (assert) => {
assert.plan(3);

assert.throws(
() => ReactOnRails.registerStore(null),
/null or undefined/,
'registerStore should throw an error if a falsey value is passed (null)'
);

assert.throws(
() => ReactOnRails.registerStore(undefined),
/null or undefined/,
'registerStore should throw an error if a falsey value is passed (undefined)'
);

assert.throws(
() => ReactOnRails.registerStore(false),
/null or undefined/,
'registerStore should throw an error if a falsey value is passed (false)'
);
});

test('register store and getStoreGenerator allow registration', (assert) => {
assert.plan(2);
function reducer(state = {}, action) {
Expand Down
24 changes: 23 additions & 1 deletion node_package/tests/StoreRegistry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@ function storeGenerator2(props) {
return createStore(reducer, props);
};

test('StoreRegistry throws error for registering null or undefined store', (assert) => {
assert.plan(2);
StoreRegistry.stores().clear();
assert.throws(() => StoreRegistry.register({ storeGenerator: null }),
/Called ReactOnRails.registerStores with a null or undefined as a value/,
'Expected an exception for calling StoreRegistry.register with an invalid store generator.'
);
assert.throws(() => StoreRegistry.register({ storeGenerator: undefined }),
/Called ReactOnRails.registerStores with a null or undefined as a value/,
'Expected an exception for calling StoreRegistry.register with an invalid store generator.'
);
});

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 +62,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 +87,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 4a3933d

Please sign in to comment.