Note: This post was first published in my blog, The Great Code Adventure
Building Responsive Applications, Cleanly, is Hard
The first large (okay, more like medium) scale application I built in React was pretty straightforward. It was a simple CRUD application for managing your very own list of cats and their associated hobbies (so fun). Being so straightforward, it wasn't too difficult to keep my code clean and well organized. There weren't too many fancy features, so my system of container components that fetched data and fed it to presentational components just felt kind of natural.
My understanding of this pattern was kind of blown apart about a year ago when, as a teacher at the Flatiron School, we shepherded 26 students through a React project sprint in which they broke out into small groups to develop their own varied and complex React + Redux applications. That was where things got messy. Managing such a diverse group of projects was a great way to encounter all the bugs and all the tough design decisions, all at once.
As hectic as that could be, it really drove home the utility and elegance of the container pattern in React. Instead of allowing any and all components to fetch and manipulate data, which can make debugging pretty much suck, we want to implement a pattern that's in line with the Single Responsibility Principle, and that keeps our code DRY.
So, I thought I'd offer a deeper dive into the container pattern, and one example implementation. But before we jump into the code, let's talk about container and presentational components.
What's a Container Component?
When reading up on container components, I came across this phrase a lot:
"Container components are components that are aware of Redux"
–– the internet
So, what does that mean?
Well, a container component is a component that is responsible for retrieving data, and in order to get that data, the component needs to use Redux's connect
and mapStateToProps
functions.
A container component will grab data from state via mapStateToProps
. The component will then pass necessary portions of that data down to its children as props
.
A container component is also responsible for dispatching actions that make changes to application state.
Another phrase that I came across a lot was the distinction between "controller views" and "views". This analogy really made sense to me, coming from Rails. If React is a view-layer technology, some views nonetheless are responsible for retrieving data (controller views) and passing that data to other views in order to be displayed (presentational views).
What's a Presentational Component?
If a container component is a component that actually leverages Redux to get data, a presentational component simply receives that data from its parent container and displays it.
So, you might be wondering, if a presentational component just displays data, and the container component is the one that contains any action-firing functions, how can a user's interaction with a presentation component ultimately trigger an action?
This is where callback props come in.
Callback Functions as Props
In our upcoming example, we'll see how to define a function in a container component that dispatches an action. Such a function will be passed as prop to a child, presentational, component, and triggered via a callback, in response to a users' interaction.
Okay, now we're almost ready to dive in to the code.
Application Background
The code we'll be looking at is from an student attendance tracking application that allows students to log in and indicate that they have arrived that day. Instructors can log in and view the attendance records for their class via a color-coded calendar, clicking on a calendar day and a student name from a list of students to view the details of a student's attendance record.
We'll be taking a closer look at the instructor side of things, implementing the container pattern to build of the ability for an instructor to select a calendar day and a student to view that student's attendance record details for that day.
Something like this:
Let's get started!
Component Design
When building in React, I've found it really helpful to do lots and lots of wire framing. So, before we dive in to the code, let's talk about the overall structure of our components.
As we can see from the image above, we have a couple of distinct areas that will respond really well to componentization. The image can be broken down into three distinct parts.
- calendar
- student list
- attendance record show
So, we'll build a container components, ScheduleContainer
, that contains the child presentational components of calendar and attendance record show. We'll make a StudentsContainer
component that is rendered by ScheduleContainer
but that in turn renders a presentational component, StudentList
.
Something like this:
In order to display an attendance record detail, we need to know who the selected student is and what the selected day is. With this information, we can dip into the attendance records we have in the application's state, identify the correct attendance record, and pass it to the attendance record show component to be displayed.
Before we worry about selecting students and dynamically rendering the correct attendance record, we'll get all of our data displaying nicely. Then, we'll move on to using callback functions to select students from studentList
component to change the attendance record that ScheduleContainer
passes down to attendanceRecordShow
to display.
Step 1: connect
-ing our Container Components and Getting Data
First things first, we'll set up our top-most level container component, ScheduleContainer
, and give it access to the data it needs from state.
This post isn't concerned with the "back-end" of things, so we won't really be diving in to action creator functions or reducers. We'll assume that the data in state looks like this:
{
attendanceRecords: [
{id: 1, date: '10-7-2017', records: [
{id: 1, student_id: 7, arrived: true, arrivedAt:
'10am'},
{id: 2, student_id: 8, arrived: false, arrivedAt:
null}]},
{id: 2, date: '10-8-2017', records: [
{id: 3, student_id: 7, arrived: true, arrivedAt:
'10:20am'},
{id: 2, student_id: 8, arrived: true, arrivedAt:
'9:00am'},]},
],
students: [
{id: 7, firstName: "Sophie", lastName: "DeBenedetto"},
{id: 8, firstName: "Doctor", lastName: "Who"},
{id: 9, firstName: "Amy", lastName: "Pond"}
]
}
We can see that state
contains attendanceRecords
and students
and that attendance records are organized by date, with each attendance record object containing a property, records
, which lists the records for each student for that date.
Our ScheduleContainer
component is mainly concerned with getting the attendance records from state, and passing them to the calendar presentational component. For my calendar, I used the React DayPicker library.
import React from 'react';
import DayPicker, { DateUtils } from 'react-day-picker'
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as attendanceRecordActions from '../../actions/attendanceRecordActions';
class ScheduleContainer extends React.Component {
componentDidMount() {
if (this.props.attendanceRecords.length = = 0) {
this.props.actions.fetchAttendanceRecords();
}
}
render() {
return (
< DayPicker
locale='us'
selectedDays={day => {
DateUtils.isSameDay(new Date())
}} />
)
}
}
function mapStateToProps(state, ownProps) {
return {attendanceRecords: state.attendanceRecords}
}
function mapDispatchToProps(dispatch) {
return {actions: bindActionCreators(attendanceRecordActions, dispatch)
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleContainer);
So far our component is pretty simple. It manages the following:
- Use
mapStateToProps
to get the attendance records from state and make them available to our component as props. (The default value for this key ofstate
is an empty array, and it is set in the initial state of our application, not shown here.) - Use
mapDispatchToProps
to get theattendanceRecordActions
functions and make them available to our component underthis.props.actions
. - Use the lifecycle method,
componentDidMount
to check if there are in fact attendance records. If not, dispatch thefetchAttendanceRecords
action, which will make an API call, get the attendance records, populate them into application state and cause a re-render. - Then, render the
DayPicker
calendar component, highlighting the selected day via theselectedDays
prop.
Currently, we're not doing anything with the attendance records we grabbed from state. So, what do we need to do with them?
We need to:
- Identify the selected day and student and render that student's record for that day.
- Allow a user to click on a calendar day and change the selected day and attendance record to view.
Step 2: Passing Data Down to Presentational Components to Display
Our aim is to display the attendance record for a selected student and a selected day. Before we worry about how we will get that information, let's take build out a simple functional component to display it.
We'll build a component, AttendanceRecordShow
, that will be rendered by ScheduleContainer
. Eventually, ScheduleContainer
will pass the correct attendance record (based on selected student and day) down into this component.
// src/components/AttendanceRecordShow.js
import React from 'react'
import Moment from 'react-moment';
const AttendanceRecordShow = (props) => {
function studentInfo() {
if (props.student) {
return (
< p >
record for: {props.student.first_name}{props.student.last_name}
< /p>
}
}
function recordInfo() {
if (props.record) {
if (props.record.arrived) {
const date = new Date(props.record.arrived_at)
return < p>arrived at: {date.toDateString()}< /p>
} else {
return < p>absent or late</ p>
}
}
}
return (
< div className="col-sm-12 text-center">
{studentInfo()}
{recordInfo()}
< p>{props.day.toDateString()}< /p>
< /div>
)
}
export default AttendanceRecordShow
ScheduleContainer
will render the component like this:
// src/components/containers/ScheduleContainer.js
class ScheduleContainer extends React.Component {
...
render() {
return (
< DayPicker
locale='us'
selectedDays={day => {
DateUtils.isSameDay(new Date())
}} />
< AttendanceRecordShow
day={we need to give it a day!}
student={we need to give it a student!}
record={we need to give it a record!}/>
)
}
Our ScheduleContainer
container is in charge of fetching and manipulating data, and passing it down to child functional, or presentational, components to be displayed.
So, let's teach ScheduleContainer
how to identify and grab the attendance record for the selected student and day, and pass that down to the appropriate presentational components.
ScheduleContainer
will need to keep track of the selected student, day and attendance record, and the selected student and day will change based on the user's click of a certain calendar day or student from our student list. This will in turn change the attendance record that we want to display. So, ScheduleContainer
should keep track of this information as part of its own internal state.
We'll start by giving ScheduleContainer
a constructor function that sets some default values. We'll give the selectedDay
property a default value of today's date, the selectedStudent
property a default value of null
and the selectedRecord
a default value of null
.
// src/components/containers/ScheduleContainer.js
class ScheduleContainer extends React.Component {
constructor(props) {
super(props)
this.state = {selectedStudent: null, selectedRecord: null, selectedDay: new Date()}
}
...
render() {
return (
< DayPicker
locale='us'
selectedDays={day => {
DateUtils.isSameDay(new Date())
}} />
< AttendanceRecordShow
day={this.selectedDay}
student={this.selectedStudent}
record={this.selectedRecord}/>
)
}
We need to give the user the ability to change the selected day, i.e. to select a day. The DayPicker
component responds to a callback function, onClick
, that we can set to a custom function to set our selected day. This way, when a user clicks on a calendar day, we can dynamically update the ScheduleContainer
component's state's selectedDay
property, changing the value that we pass down into AttendanceRecordShow
.
Let's define a function, selectDay
, and tell it to fire as the onClick
function for the DayPicker
component. Our selectDay
function has two jobs:
- Set the
ScheduleContainer
component's state'sselectedDay
property to the day that the user clicks on via the calendar. - If there is already a student selected, selecting a day should change the state's
selectedRecord
property to the record of the selected student for that day.
selectDay(e, day) {
e.preventDefault();
if (this.state.selectedStudent) {
const recordsBySelectedDate = this.props.attendanceRecords.find(recordsByDate => {
const date = new Date(recordsByDate.date)
return date.toDateString() = = day.toDateString()
})
const record = recordsBySelectedDate.records.find(record => record.student_id = = this.state.selectedStudent.id)
this.setState({selectedRecord: record, selectedDay: day})
} else {
this.setState({selectedDay: day})
}
}
In the function above, we first check to see if there is a selectedStudent
, if so, we then grab the attendance records with the newly selected date, and then from that set of records, grab the record with the student_id
of the selected student's ID.
Next up, let's give our user the ability to select a student from our list of students.
Step 3: Props as Callback Functions: Sending Actions Up from Presentational to Container Components
We'll build a presentational component, StudentList
, that will render a list of students. A user should be able to click on any student in the list and view that student's attendance record for the selected day.
But, our StudentList
will need access to all of the students in order to display them. StudentList
shouldn't fetch any data itself, or be connected to the store in any way––remember, it's just a dumb presentational component. We do have one container component ScheduleContainer
, that is responsible for fetching data. But this container component is already fetching attendance record data. We don't want to crowd this one container component with lots and lots of data fetching responsibilities.
So, we'll build another container component and have ScheduleContainer
contain it. This illustrates an important aspect of our container pattern:
Containers can contain other containers!
So, we'll build another container component, StudentsContainer
, that will fetch the student data and pass it down to a presentational component, StudentList
as part of props
The StudentsContainer
Component
StudentsContainer
should follow a similar pattern to ScheduleContainer
––use mapStateToProps
to grab the students and use the componentDidMount
lifecycle method to fetch students from the API if none are populated into state.
Let's do it!
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as instructorActions from '../../actions/instructorActions';
import StudentList from '../studentList';
class StudentsContainer extends React.Component {
componentDidMount() {
if (this.props.students.length = = 0) {
this.props.actions.fetchStudents();
}
}
render() {
return (
< div className="col-lg-4">
< h2>Students< /h2>
< StudentList
students={this.props.students}/>
< /div>
)
}
}
function mapStateToProps(state) {
return {students: state.students}
}
function mapDispatchToProps(dispatch) {
return {actions: bindActionCreators(instructorActions, dispatch)}
}
export default connect(mapStateToProps, mapDispatchToProps)(StudentsContainer);
This component plucks the students from state and passes them to the presentational component, StudentList
.
Our StudentList
component looks something like this:
import React from 'react'
import {ListGroup, ListGroupItem} from 'react-bootstrap'
const StudentList = (props) => {
function studentListItems() {
return props.students.map((student, i) => {
return (
< ListGroupItem>
{student.first_name} {student.last_name}
< /ListGroupItem>
})
}
function studentListGroup() {
return (
< ListGroup>
{studentListItems()}
< /ListGroup>
)
}
return (
{studentListGroup()}
)
}
export default StudentList;
StudentList
iterates over the students stored in the students
prop passed down from StudentsContainer
, to collect and render a list group of student names.
The top-level container component, ScheduleContainer
will render StudentsContainer
like this:
// src/components/containers/ScheduleContainer.js
class ScheduleContainer extends React.Component {
constructor(props) {
super(props)
this.state = {selectedStudent: null, selectedRecord: null, selectedDay: new Date()}
}
...
render() {
return (
<StudentsContainer />
< DayPicker
locale='us'
selectedDays={day => {
DateUtils.isSameDay(new Date())
}} />
< AttendanceRecordShow
day={this.selectedDay}
student={this.selectedStudent}
record={this.selectedRecord}/>
)
}
Now that we have our student list up and running and displaying a lovely list of students, we need to allow our user to click on a student from that list, make that student the "selected student", and display that student's attendance record for the selected day.
Props as Callback Functions + The "Data Down Actions Up" Principle
Remember who's in charge of identifying the attendance record? It will have to be something that knows about the selected day and the selected student and has access to all the attendance records...
It's ScheduleContainer
! So, since it's StudentList
that will be in charge of rendering our list of students, we'll have to teach StudentList
how to send a message all the way back up to the top-level container, ScheduleContainer
, and tell it to update its selectedStudent
property in state whenever a user clicks on a student.
We'll define a function, selectStudent
, in ScheduleContainer
. This function will accept an argument of the ID of the student being selected, and update ScheduleContainer
's state's selectedStudent
accordingly.
It has a second responsibility as well. It must update the selectedRecord
property of the component's state in accordance with the newly selected student and current selected day.
Lastly, we'll have to pass this function down through StudentsContainer
, to StudentList
as a prop, and we'll need to bind
this
in the constructor function here in our top-level container in order for this to work.
// src/components/containers/ScheduleContainer.js
class ScheduleContainer extends React.Component {
constructor(props) {
super(props)
this.selectStudent = this.selectStudent.bind(this)
this.state = {selectedStudent: null, selectedRecord: null, selectedDay: new Date()}
}
...
selectStudent(studentId) {
const student = this.props.students.find(student => student.id = = studentId)
var that = this
const recordsBySelectedDate = this.props.attendanceRecords.find(recordsByDate => {
const date = new Date(recordsByDate.date)
return date.toDateString() == that.state.selectedDay.toDateString()
})
const record = recordsBySelectedDate.records.find(record => record.student_id studentId)
this.setState({selectedStudent: student, selectedRecord: record})
}
render() {
return (
< StudentsContainer
selectStudent={this.selectStudent}/>
< DayPicker
locale='us'
selectedDays={day => {
DateUtils.isSameDay(new Date())
}} />
< AttendanceRecordShow
day={this.selectedDay}
student={this.selectedStudent}
record={this.selectedRecord}/>
)
}
StudentsContainer
will in turn pass the selectStudent
function down to StudentList
:
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as instructorActions from '../../actions/instructorActions';
import StudentList from '../studentList';
class StudentsContainer extends React.Component {
componentDidMount() {
if (this.props.students.length == 0) {
this.props.actions.fetchStudents();
}
}
render() {
return (
<div className="col-lg-4">
<h2>Students</h2>
<StudentList
students={this.props.students}
selectStudent={this.props.selectStudent}/>
</div>
)
}
}
function mapStateToProps(state) {
return {students: state.students}
}
function mapDispatchToProps(dispatch) {
return {actions: bindActionCreators(instructorActions, dispatch)}
}
export default connect(mapStateToProps, mapDispatchToProps)(StudentsContainer);
And StudentList
will fire selectStudent
as on onClick
function for each student list item:
import React from 'react'
import {ListGroup, ListGroupItem} from 'react-bootstrap'
const StudentList = (props) => {
function triggerSelectStudent(e) {
e.preventDefault();
props.selectStudent(e.target.id)
}
function studentListItems() {
return props.students.map((student, i) => {
return (
< ListGroupItem onClick={triggerSelectStudent} id={student.id}>
{student.first_name} {student.last_name}
< /ListGroupItem>
)
})
}
function studentListGroup() {
return (
< ListGroup>
{studentListItems()}
< /ListGroup>
)
}
return (
{studentListGroup()}
)
}
export default StudentList;
Here, we define a function triggerSelectStudent
, that fires on the click of a student list item. The function grabs the ID of the student that was clicked on, and passes it to the invocation of the selectStudent
function, passed down to this component as a prop. This will travel all the way back up the component tree to ScheduleContainer
, invoking the selectStudent
function defined there. This, by the way, is a great example of the Data Down Actions Up flow that React is so good at.
That function will run, changing ScheduleContainer
's state to have a new selectedStudent
and a new selectedRecord
, which will trigger the component to re-render.
This will re-render the AttendanceRecordShow
component that ScheduleContainer
contains, rendering the newly selected attendance record for the user.
Conclusion
Phew! We did it! Okay, that was a lot. The code offered here is a very specific approach to building out a feature for this app, but it illustrates the larger container pattern in which:
- A top-level container renders the rest of the component tree
- That container holds child presentational components, as well as other containers which in turn hold presentational components
- Containers are responsible for getting data from state and updating internal state in response to user interaction
- Presentational components are responsible for receiving data from their parents to display and alerting their parents when a user-triggered change needs to be made via the DDAU pattern
As always, there is more than one way to approach a given feature, but the implementation shown here is in-line with the above principles. To check our the full code for this project, you can view this repo.
Happy coding!
Top comments (2)
The container pattern is a concept I have used repeatedly, still your article enunciates the benefits even better. Thanks for this.
Thanks for writing this amazing article! It's easy to follow and so descriptive, I'm already more comfortable with React and React-Redux.