Correctly organizing our components makes our code efficient brings increased performance to our React/Redux apps and facilitates later maintenance. Learn here to restructure your code into container (accessing redux store) and presentational (rendering html) components. Jump in, it will be fun!
Architecting presentational and container components in React apps using Redux
1. Senacor Technologies AG 07.02.2019 1
Architecting
presentational and
container componens in
React apps using Redux
Alain Lompo
Senacor Technologies AG
#C’tWebdev alain.lompo@senacor.com @alainlompo
2. Senacor Technologies AG 07.02.2019 2
Software developer
at Senacor
Usually with JEE and Spring/Spring boot
in the backend…
… And depending on the project
Angular or React on the frontend
I have a huge interest in graphics
Programming and I love reading and fishing
3. Senacor Technologies AG 07.02.2019 3
Every React component interacts directly with the Redux store
The main App component subscribes to the store and
uses getState() to read the state
It passes down this state as props to its children
Children components dispatch actions directly to the store
Initial scenario
5. Senacor Technologies AG 07.02.2019 5
Splitting up the « FAT » component
Start by writing the presentational component first
Its sole purpose is rendering the HTML component
Make it expect an onClick prop
The presentational component leaves full liberty to the
container component to specify what behaviour takes
place when the click event occurs
6. Senacor Technologies AG 07.02.2019 6
A presentational component is a component that just renders the HTML
It should only contain presentational markup
In a Redux-powered app it should not interact with the Redux store
Container
Component
Presentational
Component
Redux
Store
Splitting into container and presentational components
7. Senacor Technologies AG 07.02.2019 7
Redux
Store
ProjectsPanels EventPanels ContactPanels
Panel Panel Panel
Splitting into container and presentational components
9. Senacor Technologies AG 07.02.2019 9
Classic component declaration vs stateless approach
class Panels extends React.Component {
….
}
const Panels = (props) => (
….
)
10. Senacor Technologies AG 07.02.2019 10
Advantages to building stateless components
It is advantageous to build presentational component
in a stateless fashion
Reinforces Single Responsibility Principle
The syntax is more concise
Use of stateless components is recommanded
Offers better performances
11. Senacor Technologies AG 07.02.2019 11
Building stateless presentational components
const Panels = (props) => (
// ….
)
14. Senacor Technologies AG 07.02.2019 14
Bridging between the store and the presentational component
15. Senacor Technologies AG 07.02.2019 15
Benefits
Complete decoupling of presentational view code from state
and its actions
All knowledge of Redux and our store is isolated to our app’s
containers components
This minimizes the switching costs in the future
If we want to use another state management paradigm,
it would be transparent to our presentational components
27. Senacor Technologies AG 07.02.2019 27
Generating containers with React-Redux
Container components rely on presentational component to render markups
They just contain glue code between presentational component and store
React-redux is a popular library that gives us a couple of conveniences
when writing apps that uses Redux store
28. Senacor Technologies AG 07.02.2019 28
Using the Provider component
connect() needs a canonical mechanism for containers to access
the Redux store.
For this purpose react-redux offers a Provider component.
When we use connect() to generate container components, those
container components will assume that the store is available to
them via a « context ».
30. Senacor Technologies AG 07.02.2019 30
Using connect() to generate ProjectPanels
ProjectPanels connects the presentational component Panels to
our Redux store. It does it by:
Subscribing to the store in componentDidMount()
Creating a panels variable based on the store’s project
property and using that for the prop panels on Panels.
Setting the onClick prop on Panels to a function that
dispatches an OPEN_PROJECT action
31. Senacor Technologies AG 07.02.2019 31
Bridging between the store and the presentational component
39. Senacor Technologies AG 07.02.2019 39
Benefits of isolating the reference to the store
Isolating the reference to store to just one location has two huge benefits
The less references we have to store the less work to do if we desire
to move from Redux to other state management paradigm
Another benefit is for testing.
Injection of a mock store and specify the return for each spec
Assert that a given method is called on the store by passing the store
as prop to the Provider and not referencing it directly
40. Senacor Technologies AG 07.02.2019 40
ProjectDisplay component in full
How do we get
projectid?
41. Senacor Technologies AG 07.02.2019 41
We want only one reference to the store
<Provider store={store}>
<App />
</Provider>
42. Senacor Technologies AG 07.02.2019 42
Connect()’s mergeProps parameter
Connect() allows a third function as parameter called mergeProps.
connect(
mapStateToProps(state, [ownProps]),
mapDispatchToProps(dispatch, [ownProps]),
mergeProps(stateProps, dispatchProps, [ownProps])
)
47. Senacor Technologies AG 07.02.2019 47
Resources
Images are from:
https://lovepik.com:
Lovepik_com-500449026-intelligence-research.jpg
Code snippets can be found here:
https://github.com/alainlompo/react-components
At the moment, this component both renders HTML (the text field input) and communicates with the store. It dispatches the OPEN_THREAD action whenever a tab is clicked.But what if we wanted to have another set of tabs in our app? This other set of tabs would probably have to dispatch another type of action. So we’d have to write an entirely different component even though the HTML it renders would be the same.What if we instead made a generic tabs component, say Tabs? This presentational component would not specify what happens when the user clicks a tab. Instead, we could wrap it in a container component wherever we want this particular markup in our app. That container component could then specify what action to take by dispatching to the store.We’ll call our container component ThreadTabs. It will do all of the communicating with the store and let Tabs handle the markup. In the future, if we wanted to use tabs elsewhere — say, in a “contacts” view that has a tab for each group of contacts — we could re-use our presentational component:
The presentational component accepts props from a container component. The container compo- nent specifies the data a presentational component should render. The container component also specifies behavior. If the presentational component has any interactivity — like a button — it calls a prop-function given to it by the container component. The container component is the one to dispatch an action to the Redux store:
React components declared in this manner are wrapped in React’s component API. This declaration gives the component all of the React-specific features that we’ve been using, like lifecycle hooks and state management.However, React also allows you to declare stateless functional components. Stateless functional components, like Panels, are just JavaScript functions that return markup. They are not special React objects.Because Panels does not need any of React’s component methods, it can be a stateless component.
React components declared in this manner are wrapped in React’s component API. This declaration gives the component all of the React-specific features that we’ve been using, like lifecycle hooks and state management.However, as we cover in the “Advanced Components” chapter, React also allows you to declare stateless functional components. Stateless functional components, like Tabs, are just JavaScript functions that return markup. They are not special React objects.Because Panels does not need any of React’s component methods, it can be a stateless component.
Since Panels is not a React component object, it does not have the special property this.props, instead the parent will pass props to stateless components as an argument. So the components props will always be accessed as just props instead of this.props
The map call for Panels is done inside the div tag in the function’s return value
It could also be put above the function’s return statement like done before in the render function of ThreadPanels, it’s a matter of style and preferences.
Here we are not using a React component but an ES6 class
The container component specifies the props and behaviour for the presentational component
The props are set to this.props.panels, specified by App.
We then set the prop onClick to a function that calls store.dispatch(). We are expecting Panels to pass the id of the clicked panel to this function
At this step the model is already testable
Notice that:
ProjectPanels sends actions to the store directly with the dispatch, yet at the moment it’s reading from the store indirectly through props. (via this.props.panels). App is the one reading from the store and this data trickles down to ProjectPanels. But if ProjectPanels is dispatching directly to the store, is this indirection for reading from the store necessary?
Therefore, instead we can have all our container components responsible for both sending actions to the store and reading from it.
To achieve this, let’s make the component subscribe directly to the store in componentDidMount, in a similar way as it would be done in App
Now, inside of our render method, we can read state.projects directly from the store with getState(). We will generate panels here using the same logic that we used in App.
Our Panels component is now completely presentational. It specifies no behaviour of its own and could be dropped-in anywhere in the app
The ProjectPanels component is a container component. It renders no markup. Instead it interfaces with the store and specifies which presentational component to render. The container component is the connector of the store to the presentational component.
Our presentational and container component in full:
Our Panels component is now completely presentational. It specifies no behaviour of its own and could be dropped-in anywhere in the app
The ProjectPanels component is a container component. It renders no markup. Instead it interfaces with the store and specifies which presentational component to render. The container component is the connector of the store to the presentational component.
Our presentational and container component in full:
Our Panels component is now completely presentational. It specifies no behaviour of its own and could be dropped-in anywhere in the app
The ProjectPanels component is a container component. It renders no markup. Instead it interfaces with the store and specifies which presentational component to render. The container component is the connector of the store to the presentational component.
Our presentational and container component in full:
Part of rendering the panel for a project implies rendering its events. We could have a separate container and presentational components for Project and Events.
In this setup the presentational component for a Project would render the container component for an event.
But because we don’t anticipate ever rendering a list of events outside of a project, it is reasonnable to just have the container component for the project also manage the presentational component for an event.
We can have one container component, ProjectDisplay. This container component will render the presentational component Project
For the list of events we can have Project render another presentational component EventList.
But what about the EventInput? Like our previous version of ProjectPanels, the component contains two responsibilities. The component renders markup, a single text field with a submit button. In addition it specifies the behaviour of what would happen when the form is submitted.
Instead we could have a more generic component. TextFieldSubmit only renders markup and allows its parent to specify what happens when the text field is submitted. ProjectDisplay, through Project, could control the behavior of this text field.
With this new design we would have one container component for a project up top. The presentational component Project would be a composite of two child presentational components, EventList and TextFieldSubmit
Our Panels component is now completely presentational. It specifies no behaviour of its own and could be dropped-in anywhere in the app
The ProjectPanels component is a container component. It renders no markup. Instead it interfaces with the store and specifies which presentational component to render. The container component is the connector of the store to the presentational component.
Our presentational and container component in full:
We have now two components related to displaying a Project. One is an EventList, which renders all of the events of a project. The other is TextFieldSubmit, a generic text field entry that we are going to use to submit new events of a project.
We are collecting these two presentational components under Project, another presentational component. The container component ProjectDisplay will render Project which in turn will render EventList and TextFieldSubmit.
We anticipate that ProjectDisplay will pass Project three props
project: the project itself
onEventClick: the event click handler
onEventSubmit: the text field submit handler
We will have Project pass along the appropriate props to each of its child presentational components:
ProjectDisplay is now our container component. Like with our two provious container components, it will subscribe to the store. The component will be responsible for both reading from the store and dispatching actions to it.
First, subscribe to the store in componentDidMount.
ProjectDisplay will read directly from the store to get the active project. The container component will then render Project, passing in the props project and onEventClick
Insid of render, we will use the same logic we had used before in App to grab the active project.
ProjectDisplay will read directly from the store to get the active project. The container component will then render Project, passing in the props project and onEventClick
Insid of render, we will use the same logic we had used before in App to grab the active project.
We return Project, passing it project as well as specifying its behavior for onEventClick and onEventSubmit.
This result is quite a constrast with the App with started with.
Due to a combination of our new container and presentational component paradigm and a Redux state manager, the top-level component for this app is just specifying what container components to include on the page. All of the responsibility for reading and writing to state has been pushed down to each one our container components
Because we are not dispatching actions directly from our leaf components, we have isolated all knowledge of the Redux store to our container components. We are fre to reuse our presentational components in other contexts within our app. What’s more, if we wanted to switch state management paradigms from Redux to something else we would only need to modify our container component.
Our container components all look pretty similar. They subscribe to the store and then map state and actions to props on a presentational component. In the next section, we will explore an option for reducing some of the ceremony around writing container components.
Our two container components (ProjectPanels and ProjectDisplay) have similar behavior
They subscribe to the store in componentDidMount
They might have some logic to massage data from state into a format fit as a prop for the presentational component (like panels and ProjectPanels)
They map actions on the presentational components (like click events) to functions that dispatch to the store.
Because container components rely on presentational components to render markup they just contain « glue » code between the store and the presentational components.
A popular library, react-redux, gives us a couple of conveniences when writing React apps that use a Redux store. The primary convenience is its connect() function.
The connect() function in react-redux generates container components. For each presentational components we can write functions that specify how state should map to props and how events should map to dispatches
Context is a React feature that you can use to make certain data available to all React components.
With props, data is explicitely passed down the component hierarchy. A parent must specify what data is available to its children.
Context allows you to make data available implicitly to all components in a tree. Any component can « opt-in » to receiving context and that
component’s parent does not need to do anything.
The React team actively discourage the use of context, except for special circumstances.
To allow our generated container components to access the store through context, we need to wrap App in Provider.
At the bottom of our App.js, we will declare a new component WrappedApp. WrappedAPp will return the App component wrapped in the Provider component.
We will export WrappedApp from the file.
Provider expects to receive the props store. The store is now available anywhere in our component hierarchy under the context variable store.
Another common practice is to import Provider into index.js and wrap App component in it there.
The ProjectPanels components connects the presentational component Panels with our Redux store. It does so by:
Subscribing to the store in componentDidMount()
Creating a panels variable based on the store’s project property and using that for the prop panels on Panels.
Setting the onClick prop on Panels to a function that dispatches an OPEN_PROJECT action
We can use connect() to generate this component
We need to pass two arguments to connect(). The first will be a function that maps the state to the props of Panels. The second will be a function that maps
Dispatch calls to the component’s props.
We will see how this works by implementing it
Our Panels component is now completely presentational. It specifies no behaviour of its own and could be dropped-in anywhere in the app
The ProjectPanels component is a container component. It renders no markup. Instead it interfaces with the store and specifies which presentational component to render. The container component is the connector of the store to the presentational component.
Our presentational and container component in full:
Until here we have performed our « mapping » between the state and the props for Panels inside of the render function for ProjectPanels by creating the panels variable.
We will write a function that connect() will use to perform the same operation. We will call this function mapStateToPanelsProps().
Whenever the state is changed, this function will be invoked to determine how to map the new state to the props for Panels.
Declare the function above ProjectPanels in App.js. The function expects to receive the state as an argument
* We can reuse the logic that we had in ProjectPanels to produce the variable panels, based on state.projects:
Our state-to-props mapping function needs to return an object. The properties on this object are the prop names for Panels. Because the prop is called panels and the variable we are setting it to is also panels, we can use the ES6 object shorthand:
return {
panels;
};
This function encapsulates the logic that was previously in ProjectPanels, describing how the state maps to the props panels for Panels. Now we have to do the same for the prop onClick, which maps to a dispatch call.
We will call this function mapDispatchToPanelsProps()
const mapDispatchToPanelsProps = (dispatch) => (
This function will be passed to connect(). It will be invoked on setup with dispatch passed as an argument.
Like with mapStateToPanelsProps(), we will return an object that maps the prop onClick to the function that will perform the dispatching.
This function is idntical to the one that ProjectPanels previously specified.
We now have a function that maps the store’s state to the props panels on Panels and another that maps the prop onClick to a function that dispatches OPEN_PROJECT.
We can now replace our ProjectPanels component by using connect(). We will remove the entire ProjectPanels component that we used before.
The first argument to connect() is the function that maps the state to props. The second argument is the function that maps props to dispatch functions. Connect() returns a function that we will immediately invoke with the presentational component we’d like to « connect » to the store with our container component.
We return Project, passing it project as well as specifying its behavior for onEventClick and onEventSubmit.
Now let’s generate the ProjectDisplay component with connect().
It specifies three props
project
onEventClick
onEventSubmit
State to props
We call this mapping function mapStateToProjectProps(). It will map on prop:
project: maps to the active project in state.
Dispatch to props
We call this mapping function mappDispatchToProjectProps(). It maps two props
onEventClick: maps to a function that dispatches DELETE_EVENT
onEventSubmit: maps to a function that dispatches ADD_EVENT
mapStateToProjectProps() accepts the argument state. We have it return an object that maps the project property to the active project in the state
mapDispatchToProjectProps:
* First we write a dispatch prop for onEventClick. The function accepts an id and dispatches a DELETE_EVENT action. Again, this logic matches that of ProjectDisplay
Next we define the dispatch function for onEventSubmit.
Inside of the old ProjectDisplay, this function dispatched ADD_EVENT action like this:
Store.dispatch({
type: ‘ADD_EVENT’,
text: text,
projectId: activeProjectId
})
connect() will not pass our dispatch-to-props function the state. How do we get the active project’s id, then?
We might be tempted to try something like this:
store.dispatch({
type: ‘ADD_EVENT’,
text: text
// just read ‘activeProjectId’ directly from the store
projectId: store.getState().activeProjectId
})
This could work fine, however it is preferable that the mapping function we pass to connect() do not access the store directly.
Why?
We are about to replace our declaration of ProjectDisplay with a container component generated by connect(). Powerfully after we’ve done so,
The only reference to the store from our React components will be right here:
<Provider store={store}>
<App />
</Provider>
We return Project, passing it project as well as specifying its behavior for onEventClick and onEventSubmit.
mapDispatchToProjectProps:
* First we write a dispatch prop for onEventClick. The function accepts an id and dispatches a DELETE_EVENT action. Again, this logic matches that of ProjectDisplay
In connect(), ownProps refers to the props set on the container component that we are generating. In this instance, these would be any props that
App sets on either of our container components, ProjectPanels or ProjectDisplay
Accepting and using ownProps is optional. Because App does not specify any props on our container components, none of our mapping functions use
This second argument
mergeProps two arguments stateProps and dispatchProps are the objects returned by mapStateToProps and mapDispatchToProps
We can pass connect() a third function mergeProjectProps(). This function will be invoked with two arguments.
The object we return in mapStateToProjectProps
The object we return in mapDispatchToProjectProps
Connect will use the object returned by mergeProjectProps() as the final object to determine the props for Project
In sequence, connect() will do the following:
Call mapStateToProjectProps() with state
Call mapDispatchToProjectProps() with dispatch
Call mergeProjectProps() with the results of the two previous map functions (stateProps and dispatchProps)
Use the object returned by mergeProjectProps to set the props on Project
Inside of mergeProjectProps() we need to access to two items to create our ADD_EVENT action dispatch function:
The id of the project
The dispatch function itself
We get the id of the project via stateProps, as that object has the full project object and project.
To access to dispatch, we can pass it along in mapDispatchToProjectProps.
As such, our mapDispatchToProjectProps() function will define two properties:
onEventClick
dispatch:
We copy both stateProps and dispatchProps over to our new object using the spread operator (…)