While Pages are technically Smart Components themselves, we make a distinction between the two concepts in the context of a React app to ensure simple structure and organization.
Pages are generally addressable via a URL and may read one or more params from the url or querystring. Pages are roughly similar to actions
in a MVC controller application and you may have pages like PersonShow
or PersonIndex
in an application.
It is the responsibility of the Page to bind to changes in the Flux Stores which are relevant to the page or its children. Pages will also be generally responsible for calling Flux Actions to make changes to data when children components request it.
A common pattern is to pass a function on the Page into a component as a prop
to enable the child component to make the callback when an action is taken in the interface. For clean separability, it is important that parent components (or pages, in this case) are unaware of the DOM structure generated by any children. Likewise, children components should not have knowledge of where their data comes from or how to persist it to a server, etc.
Consider the following example of a page and component:
export default class SomePage extends React.Component {
onItemNameChanged (id, name) {
this.getFlux().actions.itemActions.updateItemName({ id: id, name: name });
}
render () {
return (
<ul>
{ this.state.items.map( item =>
<ItemInlineEditor item={ item } key={ item.id } onNameChanged={ this.onItemNameChanged } />
) }
</ul>
);
}
};
export default class ItemInlineEditor extends React.Component {
saveNow () {
var itemId = this.props.item.id;
var newName = React.findDOMNode(this.refs.itemName).value;
this.props.onNameChanged(itemId, newName);
}
render () {
return (
<li>
<input ref='itemName' value={ this.props.item.name } onBlur={ this.saveNow } />
</li>
);
}
}
Above, the ItemInlineEditor
component is responsible for rendering the passed in item
binding to the appropriate onBlur
event internally on the component, finding the right DOM element from which to extract the new value, and then calling this.props.onNameChanged
with the id and new name value.
This component is very reusable and could be used anywhere in the application regardless of what item
is passed in.
The SomePage
page component has some state.items
array from which it renders multiple ItemInlineEditor
components, passing in the individual item
. It also passes down the page function onItemNameChanged
to handle exactly how to react to changes in the item name.
This is how we maintain complete logical separation of responsibilities between Pages and our reusable Components.
If we wanted to change the above SomePage
page to use a different type of item editor component, all we would need to do is create the new component using a button to save instead of the inline editor:
export default class ItemButtonEditor extends React.Component {
onSaveClick () {
var itemId = this.props.item.id;
var newName = React.findDOMNode(this.refs.itemName).value;
this.props.onNameChanged(itemId, newName);
}
render () {
return (
<li>
<input ref='itemName' value={ this.props.item.name } />
<button value='Save' onClick={ this.onSaveClick } />
</li>
);
}
}
And then update the render
method in SomePage
to use our new component.
<ItemButtonEditor item={ item } key={ item.id } onNameChanged={ this.onItemNameChanged } />
Because the other methods in SomePage
were never aware of the implementation details in the child component, we are all done.