Rendering Lists with React

  • PublishedMarch 24, 2020
  • Length3 Minutes
  • CategoryTutorials

How to build a simple, reusable, and design-agnostic React list rendering component to simplify working with collections of data.

Photo by Jason Leung on Unsplash

About the Author

Dan is a front end engineer working on design systems. Previous roles include: Huge, Cvent, Gifn, and PRPL. For more information, see here.

Questions & Comments

If you have feedback, view this post on GitHub and create an issue or open a pull request with edits. Thanks for reading!

Let's Get It

In this post, we'll be building a declarative React list rendering component inspired by Ant Design's List and React Native's FlatList to use in situations where you may regularly reach for array.map(). While JavaScript's array methods are amazing modern additions to the language—they improve readability and foster immutable data transformation—they're fairly limited when it comes to UI rendering, but we can use them as a foundation for something better!

Over the next few examples, we'll refactor an array.map() example to use a newly created ListView, then start to introduce functionality designed to cut boilerplate required to implement common patterns for organizing collections of data.

List and List.Item

Before we get started, we'll create a couple unstyled List and List.Item components to use in the rest of the examples. To follow along interactively, see this CodeSandbox Project and feel free to fork and customize/style the examples as we go then share them with me on Twitter

1import React from 'react'
2
3// The list container
4const List = ({ children }) => <ul>{children}</ul>
5
6// An individual list item with a controlled checkbox
7const Item = ({ children, ...props }) => (
8 <li>
9 <label>
10 <input type="checkbox" {...props} /> {children}
11 </label>
12 </li>
13)
14
15// Named export of List and static access of List.Item
16List.Item = Item
17export { List }

Hardcoded List Items

Now we have <List /> and <List.Item /> created, we can use them to create a basic to-do list.

1import React from 'react'
2import { List } from './list'
3
4export default function Demo() {
5 return (
6 <main>
7 <h2>Daily objectives</h2>
8 <List>
9 <List.Item checked>Make some coffee</List.Item>
10 <List.Item checked>Write blog post</List.Item>
11 <List.Item checked={false}>Profit</List.Item>
12 </List>
13 </main>
14 )
15}

Using Array.map()

As our list continues to grow, we'll update to use a simple data structure then start rendering a variable amount of items with array.map()—Don't forget the key prop!

1import React from 'react'
2import { List } from './list'
3
4/*
5items = [
6 { "id": 2, "title": "Make some coffee", "complete": true }
7 { "id": 1, "title": "Write blog post", "complete": true }
8 { "id": 3, "title": "Profit", "complete": false }
9]
10*/
11
12export default function Demo({ items = [] }) {
13 return (
14 <main>
15 <h2>Daily objectives</h2>
16 <List>
17 {items.map(item => (
18 <List.Item key={item.id} checked={item.complete}>
19 {item.title}
20 </List.Item>
21 ))}
22 </List>
23 </main>
24 )
25}

New Challenger Approaches

So far, so good. The example above is a valid and standard approach in most React codebases. Over time, there will be other features we want to add to this list, such as filtering, sorting, and pagination. As the project continues to grow, the boilerplate supporting these patterns will get fragmented and inconsistent, unless we have a way to centralize them.

Let's start our alternative approach by creating ListView to be functionally equivalent to array.map()

1// list-view.js
2import React from 'react'
3
4export const ListView = ({ items = [], render = () => {} }) => (
5 <>{items.map(render)}</>
6)

We can now refactor our application code to replace our List.Item rendering

1import React from 'react'
2import { ListView } from './list-view'
3import { List } from './list'
4
5export default function Demo({ items = [] }) {
6 return (
7 <main>
8 <h2>Daily objectives</h2>
9 <List>
10 <ListView
11 items={items}
12 render={item => (
13 <List.Item key={item.id} checked={item.complete}>
14 {item.title}
15 </List.Item>
16 )}
17 />
18 </List>
19 </main>
20 )
21}

Improving the API

With the foundation in place, let's add a few more props and, in doing so, add support for many more use-cases.

1import React from 'react'
2
3export const ListView = ({
4 items = [],
5 render = () => {},
6 as: Wrapper = React.Fragment,
7 container: Container = React.Fragment,
8 before = null,
9 after = null,
10}) => (
11 <Wrapper>
12 {before ? before() : null}
13 <Container>{items.map(render)}</Container>
14 {after ? after() : null}
15 </Wrapper>
16)

A quick note on what the newly added props do before we update the example code. In each case, when these optional props are omitted, they leave behind no trace of their existence rendering either null or a Fragment accordingly.

  • as: Render the whole thing in a wrapper element.
  • container: Render an element around the list body.
  • before: Render a block of content before the list body.
  • after: Render a block of content after the list body.
1import React from 'react'
2import { ListView } from './list-view'
3import { List } from './list'
4
5export default function Demo({ items = [] }) {
6 return (
7 <ListView
8 as="main"
9 items={items}
10 container={List}
11 before={() => (
12 <header>
13 <h1>Daily objectives</h1>
14 </header>
15 )}
16 render={(item, idx) => (
17 <List.Item key={item.id} checked={item.complete}>
18 {item.title}
19 </List.Item>
20 )}
21 after={() => (
22 <footer>Complete: {items.filter(item => item.complete).length}</footer>
23 )}
24 />
25 )
26}

Composition

At this point, the ListView itself is fully functional and gives us a nice declarative API to render the original example entirely. Are we done? Not nearly. Now we're ready to really get started! With a flexible foundation in place, we can now write a series of thin wrapper components to implement custom functionality or styles.

Introducing Pagination

As our list length grows, one of the first additions we'll want to make is the introduction of pagination. To support a traditional pagination pattern, we'll wrap the ListView with a specialized PaginatedListView.

1// paginated-list-view.js
2import React from 'react'
3import { ListView } from './list-view'
4
5// Pagination Hook
6export function usePagination(total, perPage, initial) {
7 const [current, setPage] = React.useState(initial)
8 const numPages =
9 total % perPage ? Math.floor(total / perPage) + 1 : total / perPage
10 const onPrevious = () => setPage(Math.max(1, current - 1))
11 const onNext = () => setPage(Math.min(current + 1, numPages))
12 return { current, onPrevious, onNext, numPages, setPage }
13}
14
15// Intercept list items, extract the items for or current page
16// then render a standard ListView
17export function PaginatedListView({
18 items,
19 perPage = -1,
20 currentPage = 1,
21 ...props
22}) {
23 const offset = (currentPage - 1) * perPage
24 const paginated = items.slice(offset, offset + perPage)
25 return <ListView {...props} items={paginated} />
26}

Sidenote: If you're new to React Hooks, this recently updated free course from Kent Dodds & Egghead The Beginner's Guide to React is an excellent introduction to the topic.

To implement this in our example code, we'll need to add the usePagination() Hook, update the component name from ListView to PaginatedListView, then add two new props: currentPage and perPage.

1import React from 'react'
2import { PaginatedListView, usePagination } from './paginated-list-view'
3import { List } from './list'
4
5export default function Demo({ items = [], perPage = 6 }) {
6 const { current, onPrevious, onNext, numPages } = usePagination(
7 items.length,
8 perPage,
9 1
10 )
11 return (
12 <PaginatedListView
13 currentPage={current}
14 perPage={perPage}
15 as="main"
16 items={items}
17 container={List}
18 before={() => (
19 <header>
20 <h1>Daily objectives</h1>
21 </header>
22 )}
23 render={(item, idx) => (
24 <List.Item key={item.id} checked={item.complete}>
25 {item.title}
26 </List.Item>
27 )}
28 after={() => (
29 <footer>
30 <div>
31 <button onClick={onPrevious} disabled={current <= 1}>
32 Previous
33 </button>
34 <button onClick={onNext} disabled={current >= numPages}>
35 Next
36 </button>
37 </div>
38 <div>Complete: {items.filter(item => item.complete).length}</div>
39 </footer>
40 )}
41 />
42 )
43}

Render a Table Instead?

We sure can. Let's swap out all the HTML in our example and semantically render a table without modifying anything below the surface and keep the pagination free.

1import React from 'react'
2import { PaginatedListView, usePagination } from './paginated-list-view'
3
4export default function Demo({ items = [], perPage = 6 }) {
5 const { current, onPrevious, onNext, numPages } = usePagination(
6 items.length,
7 perPage,
8 1
9 )
10 return (
11 <PaginatedListView
12 currentPage={current}
13 perPage={perPage}
14 as="table"
15 items={items}
16 container="tbody"
17 before={() => (
18 <thead>
19 <tr>
20 <th>Done</th>
21 <th>Title</th>
22 </tr>
23 </thead>
24 )}
25 render={item => (
26 <tr key={item.id}>
27 <td>
28 <input
29 type="checkbox"
30 onChange={() => {}}
31 checked={item.complete}
32 id={`item_${item.id}`}
33 />
34 </td>
35 <td>
36 <label htmlFor={`item_${item.id}`}>{item.title}</label>
37 </td>
38 </tr>
39 )}
40 after={() => (
41 <tfoot>
42 <tr>
43 <td>{items.filter(item => item.complete).length}</td>
44 <td>
45 <button onClick={onPrevious} disabled={current <= 1}>
46 Previous
47 </button>
48 <button onClick={onNext} disabled={current >= numPages}>
49 Next
50 </button>
51 </td>
52 </tr>
53 </tfoot>
54 )}
55 />
56 )
57}

Further Improvement

To expand upon the examples above, here's a few features that can be implemented in just few lines in new composing components without modifying the underlying ListView at all:

  • Add list sorting and filtering by completion status using header controls
  • Create an alternate pagination pattern which uses an IntersectionOberserver-wrapped footer to support infinite scrolling

Really, the possibilities are endless. Feel free to fork the CodeSandbox Project or give it a shot in your next side-project and let me know what you find!

I hope the examples above help to illustrate the value of a generic list rendering component and the power of React as a tool to standardize UI patterns irrespective of DOM elements rendered on screen.

For more information, be sure to check out Ant Design's List and React Native's FlatList excellent examples too.