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.
About the Author
Questions & Comments
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'23// The list container4const List = ({ children }) => <ul>{children}</ul>56// An individual list item with a controlled checkbox7const Item = ({ children, ...props }) => (8 <li>9 <label>10 <input type="checkbox" {...props} /> {children}11 </label>12 </li>13)1415// Named export of List and static access of List.Item16List.Item = Item17export { 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'34export 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'34/*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*/1112export 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.js2import React from 'react'34export 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'45export default function Demo({ items = [] }) {6 return (7 <main>8 <h2>Daily objectives</h2>9 <List>10 <ListView11 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'23export 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'45export default function Demo({ items = [] }) {6 return (7 <ListView8 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.js2import React from 'react'3import { ListView } from './list-view'45// Pagination Hook6export function usePagination(total, perPage, initial) {7 const [current, setPage] = React.useState(initial)8 const numPages =9 total % perPage ? Math.floor(total / perPage) + 1 : total / perPage10 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}1415// Intercept list items, extract the items for or current page16// then render a standard ListView17export function PaginatedListView({18 items,19 perPage = -1,20 currentPage = 1,21 ...props22}) {23 const offset = (currentPage - 1) * perPage24 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'45export default function Demo({ items = [], perPage = 6 }) {6 const { current, onPrevious, onNext, numPages } = usePagination(7 items.length,8 perPage,9 110 )11 return (12 <PaginatedListView13 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 Previous33 </button>34 <button onClick={onNext} disabled={current >= numPages}>35 Next36 </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'34export default function Demo({ items = [], perPage = 6 }) {5 const { current, onPrevious, onNext, numPages } = usePagination(6 items.length,7 perPage,8 19 )10 return (11 <PaginatedListView12 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 <input29 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 Previous47 </button>48 <button onClick={onNext} disabled={current >= numPages}>49 Next50 </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.