1) mosh-react-tut-refactor-table-head
2) mosh-react-tut-extract-body
Refactoring Movies Table
So in movies.jsx, we see a lot of code in our render function. Let’s pull out table, as it makes up the bulk of our JSX.
In components, we create controlled MoviesTable.jsx. Basically, in order to extract code, we to first figure out what we want the refactoring to look like.
A closer look tells us that the table code involves three things:
1) the array that shows the data
2) the delete function
3) the sort function
4) Sort column that specifies the column text and sort order
All these handler objects and belong in movies.jsx. So as a controlled component, we should not have state, and simply show data via the props object.
Thus we need to make sure that in movies, we pass them in.
movies.jsx
1 2 3 4 5 6 |
<MoviesTable showingArr = {showingArr} handleDelete = {this.handleDelete} onSort = {this.onSort} sortColumn = {this.state.sortColumn} /> |
So in MoviesTable, we extract them from the prop object and refactor like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import React, { Component } from 'react'; // stateless functional component const MoviesTable = (props) => { const { showingArr, handleDelete, onSort } = props; const raiseSort = property => { // so basically whatever sortColumn data is passed down, we must // make sure it matches what we click. // if not, we just assign it. const sortColumn = { ...props.sortColumn}; if (sortColumn.property === property) { sortColumn.order = sortColumn.order === 'asc' ? 'desc' : 'asc'; } else { sortColumn.property = property; sortColumn.order = 'asc'; } onSort(sortColumn); } return (<table className="table"> <thead> <tr> <th onClick={ () => raiseSort('title') }>Title</th> <th onClick={ () => raiseSort('genre.name') }>Genre</th> <th onClick={ () => raiseSort('numberInStock') }>Stock</th> <th onClick={ () => raiseSort('dailyRentalRate') }>Rate</th> </tr> </thead> <tbody> {showingArr.map(movie => { return ( <tr key={movie._id}> <td>{movie.title}</td> <td>{movie.genre.name}</td> <td>{movie.numberInStock}</td> <td>{movie.dailyRentalRate}</td> <td><button onClick={() => handleDelete(movie)} className="btn btn-danger btn-sm">delete</button></td> </tr> ); })} </tbody> </table>); } export default MoviesTable; |
Extracting Table Header from Movies Table
Notice MovieTable’s render is huge. Let’s extract the table header out.
We want to create a table header component in the render where we do something like this:
1 2 3 4 5 |
<TableHeader columns={columns} sortColumn={sortColumn} onSort={onSort} /> |
We had a string of paths and names in literal strings. So we first create an array to depict this:
1 2 3 4 5 6 |
let columns = [ { path: 'title', label: 'Title'}, { path: 'genre.name', label: 'Genre'}, { path: 'numberInStock', label: 'Stock'}, { path: 'dailyRentalRate', label: 'Rate'}, ]; |
We already have sortColumn object and onSort function in props, so we can directly pass it into TableHeader component.
thus, when we write our TableHeader component, we get the columns as an array and display their text. When they get clicked, we raise the sort function, which takes the prop object, and use the sortColumn property. Finally, it uses the passed in prop’s onSort function.
In src, components, common, create TableHeader.jsx file:
TableHeader.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import React, { Component } from 'react'; const TableHeader = (props) => { let raiseSort = property => { const sortColumn = { ...props.sortColumn}; if (sortColumn.property === property) { sortColumn.order = sortColumn.order === 'asc' ? 'desc' : 'asc'; } else { sortColumn.property = property; sortColumn.order = 'asc'; } props.onSort(sortColumn); } return (<thead> <tr> {props.columns.map(column => ( <th key={column.path} onClick={() => raiseSort(column.path)}> {column.label} </th> ))} </tr> </thead>); } export default TableHeader; |
MoviesTable.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import React, { Component } from 'react'; import TableHeader from './common/TableHeader'; // stateless functional component const MoviesTable = (props) => { const { showingArr, handleDelete, onSort, sortColumn } = props; let columns = [ { path: 'title', label: 'Title'}, { path: 'genre.name', label: 'Genre'}, { path: 'numberInStock', label: 'Stock'}, { path: 'dailyRentalRate', label: 'Rate'}, ]; return (<table className="table"> <TableHeader columns={columns} sortColumn={sortColumn} onSort={onSort} /> <tbody> {showingArr.map(movie => { return ( <tr key={movie._id}> <td>{movie.title}</td> <td>{movie.genre.name}</td> <td>{movie.numberInStock}</td> <td>{movie.dailyRentalRate}</td> <td><button onClick={() => handleDelete(movie)} className="btn btn-danger btn-sm">delete</button></td> </tr> ); })} </tbody> </table>); } export default MoviesTable; |
Extracting Body
We also need to extract the boy from MoviesTable. The movies array is passed via the props data. We map through it and display tr for each movie.
For each tr (movie), we need to display the properties of the movie. In JS, we can do object[property] to access the value of the property in an object. However, if there are nested objects, we cannot go object[property.anotherProperty]. Therefore, “genre.name” won’t work. However, we can use lodash for this:
1 |
_.get(item, column.path) |
It takes the object ‘item’, then it gives the property path column.path. It will down into the object with the properties path and return you the value.
Notice that our output only has text data. Our ‘delete’ button is nowhere to be seen.
Hence, let’s add the delete button into our columns array.
MoviesTable.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let columns = [ { path: 'title', label: 'Title'}, { path: 'genre.name', label: 'Genre'}, { path: 'numberInStock', label: 'Stock'}, { path: 'dailyRentalRate', label: 'Rate'}, { key: "delete", content: movie => ( <button onClick={()=>handleDelete(movie)} className="btn btn-danger btn-sm" > Delete </button> ) } ]; |
So now when we render this cell, we have to check for column.content. If its exists, its a delete button and returns JSX elements. If it doesn’t, then property is path and returns text and we use lodash for it:
1 2 3 4 5 6 |
let renderCell = (item, column) => { if (column.content) { return column.content(item); // returns react element, which is a <button> } return _.get(item, column.path); } |
For each row of data, we have to go through each column’s path (title, genre.name, numberInStock…) in order to display data.
The renderCell function to takes the item, column and display it (text or button…etc) as table data.
data is array of movies
columns is our defined array of property names, labels, and key/content JSX.
Hence, as go through each row, we must extract data like so:
1 2 3 4 5 6 7 |
{data.map(item => { return <tr key={item._id}>{columns.map(column=> { return <td key={column.path || column.key}> {renderCell(item, column)} </td>; })}</tr>; })} |
TableBody.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import React, { Component } from 'react'; import _ from 'lodash'; const TableBody = (props) => { const { data, handleDelete, columns } = props; let renderCell = (item, column) => { if (column.content) { return column.content(item); // returns react element, which is a <button> } return _.get(item, column.path); } return ( <tbody> {data.map(item => { return <tr key={item._id}>{columns.map(column=> { return <td key={column.path || column.key}> {renderCell(item, column)} </td>; })}</tr>; })} </tbody> ); } export default TableBody; |
Let’s also update our handleDelete function in movies.jsx:
movies.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
handleDelete = movie => { const { currentPage } = this.state; const updatedMovies = this.state.movies.filter(m => m._id !== movie._id); this.setState({ movies: updatedMovies, }, () => { let showing = []; let cur = currentPage; do { showing = this.#paginateMovies(cur); if (showing.length === 0) { cur = cur - 1; } else { break; } } while ( cur !== 0); this.setState({ showingArr: showing }) }); }; |
MoviesTable.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import React, { Component } from 'react'; import TableHeader from './common/TableHeader'; import TableBody from './common/TableBody'; // stateless functional component const MoviesTable = (props) => { const { showingArr, handleDelete, onSort, sortColumn } = props; let columns = [ { path: 'title', label: 'Title'}, { path: 'genre.name', label: 'Genre'}, { path: 'numberInStock', label: 'Stock'}, { path: 'dailyRentalRate', label: 'Rate'}, { key: "delete", content: movie => ( <button onClick={()=>handleDelete(movie)} className="btn btn-danger btn-sm" > Delete </button> ) } ]; return ( <table className="table"> <TableHeader columns={columns} sortColumn={sortColumn} onSort={onSort} /> <TableBody columns={columns} data={showingArr} handleDelete={handleDelete} /> </table>); } export default MoviesTable; |