The React + Redux Container Pattern

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.

About a month ago, though, 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 of state is an empty array, and it is set in the initial state of our application, not shown here.)
  • Use mapDispatchToProps to get the attendanceRecordActions functions and make them available to our component under this.props.actions.
  • Use the lifecycle method, componentDidMount to check if there are in fact attendance records. If not, dispatch the fetchAttendanceRecords 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 the selectedDays 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
      }
    }
  }
  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's selectedDay 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 (
      
      < 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!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus