Although it’s now considered bad form to use the table tag in web development, sometimes it’s worth the public ridicule. Responsive layouts are all the rage these days, with users viewing sites on a 4K monitor as well as a relatively tiny cell phone screen, and tables are just too inflexible to make that transition. But sometimes the infrastructure needed to build a responsive layout to display tabular data is just too much, and a table is just enough.

Tables can either be populated from a static data source, or from data generated as a result of form input. As the former is kind of boring, we’ll be looking into the latter here. And to make things more complicated, we’re going to add in a component which allows for the sorting of data pulled from a public API, the Open Move Database (OMDb).

Tables at a Glance

I know this is a topic about a web development technology but if you happened to find this post and are interested in reading about development practices, but don’t have the web development background yourself, I wanted to quickly explain what a table is in HTML parlance.

<table>
   <thead>
      <tr>
         <th>Column Header 1</td>
         <th>Column Header 2</td>
         <th>Column Header 3</td>
      </tr>
   </thead>
   <tbody>
      <tr>
         <td>Data a1</td>
         <td>Data a1</td>
         <td>Data a3</td>
      </tr>
      <tr>
         <td>Data b1</td>
         <td>Data b2</td>
         <td>Data b3</td>
      </tr>
   </tbody>
</table>
<!-- TFOOT is optional -->

A table is exactly what you’d expect: a construct that organizes data across rows and columns. The main benefit of tables is that they maintain a consistent single-column width between rows so that everything aligns from top to bottom. Unfortunately, the table hasn’t evolved over time, so some of the mechanisms for styling tables rely on some very old-school and passé techniques.

The outer tag, table, contains at least one sub-section, although for our purposes we’ll be working with two. The required section is the tbody. This is where data will be repeated between tr tags (table rows) which are rendered top to bottom. Within a tr we find td tags (table…data?). td tags follow one after another and are rendered left to right.

The optional section that we need for this post is the thead section. This represents the top-most row of the table. It also contains tr elements, but rather than a td, a header uses th (table header) for the individual cells. The reason is because th cells have internal styling mandates which include centering the contents and bolding it if the contents of a th happen to be text or numbers. This visually offsets the header from the rest of the data if no further styling is applied.

A Search and Result React Implementation

Searching with React is more difficult than it should be. React’s mandate is that content should be as granular as possible, meaning that one component should only perform one unit of work. In a normal web page, we might code an entire page display in one file, top to bottom, sub out functionality to a Javascript file, and process heavy lifting via a back-end system. Here, we want to cleave as closely to React’s mandate as possible, so we’ll be splitting up the search form and the results.

As always, we are starting out with a new create-react-app which I will not explain because every other React post on the Internet does it and I’m already a long-winded blogger.

Search Form

First, our imports for a file MovieSearch/index.js

import React, { useState } from 'react';
import { MovieTitleSearch } from '../../API';

import { SearchResults } from './searchResults';

import { Container, Row, Col } from 'react-bootstrap';

We’ll need useState for several tracking options. MovieTitleSearch is just an Axios helper file which contacts the OMDb and returns the Promise for us to handle. SearchResults will be our results display, and the Bootstrap import is just a quick way to get our search form laid out on the screen for testing purposes.

const defaultSearchForm = {
    apikey: process.env.REACT_APP_OMDB_API,
    s:''
}

This is something I do regularly. When dealing with forms in React, we need to store their contents in state when they change. These form fields are called controlled elements. I always set up my controlled forms using an object which is stored in state, which is initialized with either blank or default values. Because we will be submitting our form to the OMDb API, we need to pass two bits of info: an API key obtained from the OMDb website, and a querystring parameter s which carries our search value. Note that I have put my API key into an .env file.

const MovieSearch = (props) =>{
    //Controlled components
    const [ formVals, setFormVals ] = useState(defaultSearchForm);
    //API status 
    const [ status, setStatus ] = useState("IDLE");
    //API results
    const [ resultVals, setResultVals ] = useState([]);

Starting out, we have our component name, MovieSearch which takes in props in the standard React fashion.

Next we have several state bucket setups. Here’s a quick description:

  • formVals holds our controlled form values. Note the assignment of the defaultSearchForm object to initialize the bucket.
  • status will hold our API action status. By default, it’s IDLE.
  • resultVals holds the data returned to us from the API call. It’s defaulted to an empty array.

Let’s look at the form output first.

return(
   <form onSubmit={handleOnSubmit}>
        <Container>
            <Row>
                <Col lg={3}>Movie Name</Col>
                <Col lg={7}><input type="text" name="s" onChange={handleOnChange} /></Col>
                <Col lg={2}><input type="submit" name="doSearch" value="Search" /></Col>
            </Row>
        </Container>
        <SearchResults dataStatus={status} data={resultVals} onSorted={handleOnSorted} />
    </form>
)

The form is sandwiched between two form tags for simplicity’s sake. The form definition is displayed using Bootstrap, and consists of just one input element, named “s” to match the property we have already defined in our defaultSearchFormObject, and a submit button. Below this, we have the SearchResults component, but we’ll ignore that for right now.

We have two methods we need to create: handleOnSubmit and handleOnChange.

//Form submission
const handleOnSubmit = (e) => {
	e.preventDefault();
	//We are actively searching
	setStatus("ACTIVE");
	MovieTitleSearch(formVals)
	.then(results=>{
		//results.data is ALWAYS what we want from an API call. This is where the payload will be.
		//Anything after that is custom. In this case, our data in the Search property.
		if (results.status === 200){
			//In the case of the OMDb, even an error is a 200 so we have to parse the preesnce of
			//data in the results.data.Error property and if preesnt, throw our error.
			//If empty, we have results and can update state.
			//Status parsing just in case.
			if (results.data.Error){
				setResultVals([]);            
				setStatus("NO_RESULTS");
			}else{
				setResultVals(results.data.Search);            
				setStatus("COMPLETE");
			}
		} else if (results.status === 204) {
			setResultVals([]); 
			setStatus("NO_RESULTS")
		}else if (results.status === 400){
			setResultVals([]); 
			setStatus("API_ERROR")
		}
	})
	.catch(err=>{console.log(err); setStatus("ERROR"); setResultVals([]); });
}

//Controlled component handling
const handleOnChange = (e) =>{
	const { name, value } = e.target;
	setFormVals(prev=>({
		...prev,
		[name]:value
	}));
}

handleOnSubmit sends our form values to the API. Because we are using the form element, we need to stop the form from doing what it would normally do, which is initiate a postback. If we do not include the e.preventDefault(), then the form will simply send the contents of our form back to itself, and we’ll see our data in the URL address bar of the browser.

Next, we begin the API call process. Because we want to know what the API is doing, we set the status state bucket to ACTIVE. We’ll remain in this state until one of a few conditions trigged by the next line, which is to call the API, passing in the contents of the formVals state bucket.

{
    "Search": [
        {
            "Title": "Batman Begins",
            "Year": "2005",
            "imdbID": "tt0372784",
            "Type": "movie",
            "Poster": "https://m.media-amazon.com/images/M/MV5BOTY4YjI2N2MtYmFlMC00ZjcyLTg3YjEtMDQyM2ZjYzQ5YWFkXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg"
        },
        {
            "Title": "Batman v Superman: Dawn of Justice",
            "Year": "2016",
            "imdbID": "tt2975590",
            "Type": "movie",
            "Poster": "https://m.media-amazon.com/images/M/MV5BYThjYzcyYzItNTVjNy00NDk0LTgwMWQtYjMwNmNlNWJhMzMyXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg"
        },
        {
            "Title": "Batman",
            "Year": "1989",
            "imdbID": "tt0096895",
            "Type": "movie",
            "Poster": "https://m.media-amazon.com/images/M/MV5BMTYwNjAyODIyMF5BMl5BanBnXkFtZTYwNDMwMDk2._V1_SX300.jpg"
        },
...
}

Here’s the OMDb return for a successful search of “Batman”. We get the title of the movie, the year, the IMDB ID, the type of media, and a link to the media’s poster image skimmed from Amazon. However, if we were to search for garbage, this is the return result:

{
    "Response": "False",
    "Error": "Movie not found!"
}

If we add a breakpoint in the browser’s developer tools after receiving a successful result from the API, we see this:

If we examine the breakpoint on an unsuccessful result, we see this:

In both cases, the results.status value is 200, which is a successful, non-erroring result. Because of this, the code is checking the value of results.data.Error and if it’s not null, then we set our status state bucket to NO_RESULTS and reset the resultVals to an empty array. If Error is null, however, we have data. We put results.data.Search into the state bucket, and set the status bucket to COMPLETE. I’ve also included additional code showing how to check the value of the HTTP status property, in case the API returns valid status codes. They basically perform the same tasks as explained above, just on the HTTP status values instead of arbitrary return properties.

The handleOnChange function is pretty much standard for dealing with controlled components. Basic components pass in an argument e, which we destructure into name and value. We then update our state bucket with these values using the spread operator on the existing values. This allows us to update the property of the state bucket defined by name and leave all other properties alone. Note that name is enclosed in brackets; this is so the parser understands that it should use the VALUE of name and not the actual text “name” when updating a property.

The last handler we have in this file is to handle the sorting.

const handleOnSorted = (sorted) =>{
    setResultVals( Object.keys(sorted).map((k) => sorted[k]))
}

This function will be passed to the SearchResults component. It’s also a bit of a kluge; for some reason, the sorted results are returned as an array, which we want, but when updating the state bucket with the value of sorted, the data placed into the bucket suddenly becomes a standard JS object. The use of Object.keys above forces our sorted data into an array again by building a new array in order to assign it to the state bucket.

Search Results

import { Loading } from '../Loading';
import { SortableHeader } from '../SortHeader';

const header = [
    {field:"Poster", label:"Poster", skipSort:true},
    {field:"Title", label:"Title"},
    {field:"Year", label:"Year"}
]

We import two custom components here: Loading, which displays a loading GIF, and SortableHeader, which will render our THEAD content, allow for interaction to sort by column, and returns the newly sorted dataset.

The header object is a custom construct that is used to define the content of SortableHeader. It is an array of objects, and each object should contain a value field, which is the name of the data element to sort on, and label, which is the value to display on the screen. skipSort is an optional boolean property which can be used to include a column header, but to skip the sorting interaction. In this case, we don’t want to sort on the poster image, so we skip that.

const SearchResults = (props) =>{

    const { dataStatus, data } = props;
    const { onSorted } = props;

We start by destructuring some props. I like to divide this process into variables and handlers, just to keep things logical. dataStatus is the status value from our API process (IDLE, COMPLETE, NO_CONTENT, ERROR) and data is the data returned to us from the API. onSorted is the handler that updates the state bucket where we saved the API results. Because we saved those results in the MovieSearch component, one level up, we have to pass this handler into the SearchResults component so the SortableHeader can send the sorted data to where it needs to be stored.

if (dataStatus === "IDLE"){
	return <div></div>
}else if (dataStatus === "ACTIVE"){
	return <Loading />
}else if (dataStatus === "NO_RESULTS"){
	return <div>No results</div>
}else{
	return(
		<table>
			<thead>
				<SortableHeader data={data} header={header} onSorted={onSorted} />
			</thead>
			<tbody>
				{data.map(itm=>{
					return (
						<tr key={itm.imdbID}>
							<td><img alt="poster" src={itm.Poster} style={{width:"100px", height:"auto"}}></img></td>
							<td>{`${itm.Title}`}</td>
							<td>{`${itm.Year}`}</td>
						</tr>
					)
				})}
			</tbody>
		</table>
	)
}

Here’s our output. We’re parsing the value of dataStatus to determine what to return. If we’re IDLE, we return an empty div container. If the status is ACTIVE, then we are waiting for results from the API and display the Loading component GIF. In this demo, the API returns relatively quickly, so the GIF is really just a flash, but it’s always good to include some kind of meter so the user knows the app hasn’t frozen. If we have NO_RESULTS, we need to let the user know; we should never display a blank screen or throw an ugly message when we have the ability to capture and shape the result.

Finally, if we tripped none of the preceding outputs, we render our results table. Our thead element contains the SortableHeader component. We’re passing in the API result data, the header object which contains the cell definitions, and the onSorted event handler passed in from MovieSearch. The tbody content is rendered using map. For each item in the data array collection, we return a new tr displaying td cells containing the poster image, the name of the movie, and the year it was released.

Sortable Header

 import React, { useState } from 'react';
 import { FaSort, FaSortUp, FaSortDown } from 'react-icons/fa';
 
 const interactable = {cursor:"pointer"}

We import React and useState, as well as some choice icons from the wonderful “react-icons” package. We are also defining a style constant interactable. This will be applied where we want to change the mouse pointer into the interactive “hand” to let the user know that the header elements can be clicked on.

 const SortableHeader = (props) => {
     //Records field being sorted, and current sort direction
     const [ sortField, setSortField ] = useState(null);
     const [ sortAsc, setSortAsc ] = useState(true);   //Using boolean allows for prev=>!prev
 
     //Destructuring props
     //Data: Raw data
     //Header: object aligning Field and Label for rendering THEAD TRs
     const { data, header } = props;
     //Callback that receives newly sorted data
     const { onSorted } = props;

Starting out, we set some state buckets:

  • sortField tracks the field that we are currently sorting by. This default to null.
  • sortAsc is a boolean used to track the sort direction. Ascending is default (true) and Descending is assigned as false.

We then destructure the properties passed in: API data, the header definition object, and the sort handler.

const handleClick = (col) => {
	if (sortField && sortField === col){
		setSortAsc(prev=>!prev);
	}else{
		setSortField(col);
		setSortAsc(true);
	}

	SortData(data, col, sortAsc)
		.then(sorted=>{ onSorted(sorted); })
		.catch(err=>{ console.log(err); })
 }

When a user clicks on a header, we need to know which column they want to sort on. If the value of col is equal to a value stored in the sortField state, then we need to flip the boolean stored in sortAsc. We assume that the user has already sorted on this column, so we need to sort in the opposite direction now. However, if we have no previously sorted column in state, or if the value of col does not equal the previous column, we reset the sorting direction to Ascending.

SortData is a function that we’ll get to. It takes the API data, the column, and the sort direction, rebuilds the data array, and returns it, sorted appropriately. This new data object is passed up the chain via the onSorted method that this component received.

return (
	<tr>
		{ header.map(itm=>{
			if (itm.skipSort){
				return <th key={itm.field}>{itm.label}</th>
			}else{
				return (<th key={itm.field} style={interactable} onClick={()=>handleClick(itm.field)}>
					{itm.label}
					<SortIcon isSorted={itm.field === sortField} sortAsc={sortAsc} field={itm.field} />
				</th>)
			}
		}) }
	</tr>
 )

The output of the component is fairly simple. Using map, we iterate through each item in the header definition object. If the header should skip sorting, we render the th using the label property. Otherwise, we set the style on the th to use the interactive cursor, add an onClick handler, and render the label property alongside a new sub-component which renders icons based on the sorting situation for this column.

Sort Icon

This is included in the SortableHeader file, but is outside of the component definition.

const SortIcon = ({isSorted, sortAsc, field}) =>{
     const sortStyle={marginLeft:"0px"}
     if (isSorted){
        return sortAsc ? <FaSortUp style={sortStyle} /> : <FaSortDown style={sortStyle} />
     }else{
         return <FaSort style={sortStyle} />
     }
 }

Basically, it returns a specific icon based on the case if the column is sorted, and if so, in which direction?

Sorting the Data

const SortData = (data, col, dir) =>{
     const doSort = (resolve, reject) =>{
         let sorted = data.sort(dynamicSort(col, dir));
         resolve(sorted);
     }
     return new Promise(doSort);
 }
 
 /* https://stackoverflow.com/questions/1129216/sort-array-of-objects-by-string-property-value */
 function dynamicSort(property, direction) {
     var sortOrder = 1;
     if(direction === false) {
         sortOrder = -1;
     }
     return function (a,b) {
         var result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0;
         return result * sortOrder;
     }
 }

Sorting the data is performed by a standard Array.prototype.sort function. The only difference is that we’re sorting objects by property values, so the dynamicSort function is included. I found this implementation after much searching on how to sort by a dynamic object, and the full post can be found at the URL included in the source snippet above.

Results

Starting out, this is our form. Because the status is IDLE, the space at the bottom is simply rendering an empty div tag. When we enter search criteria and click search, we get this:

This is terribly unsexy because we didn’t apply any style to the table, but we have our three columns “Poster”, “Title”, and “Year”. We display the image from Amazon and each API data value in the appropriate column. Note that the header is bolded (a feature of the th element), and our two sortable columns have icons appended while the skip-sort column “Poster” does not. If we opt to sort by “Title”, we get this:

And if we click the “Title” header again, we get this:

Wrapping it Up

This is a very simple form, result, and sort implementation. The biggest challenge here was dealing with the results while maintaining a separation of concern. In my mind, the form should be in control of the data since it’s the one collecting the info in order to do the search. As a result, that data needs to be passed into the table render component, which is its own thing; we should be able to create as many components as we want that can handle the data we expect, and swap them out as needed. For example, we could create another results display with the same signature that presents the movie results as tiles rather than in a table. We could then choose which component to use to display the results based on a toggle the user clicks to decide between table and tiles.

That means that handling the sort action should be in the table component, as it’s part of the table structure. However, this causes a dependency issue: since the data is stored at the form component level, we must have a function passed into the table component so that the SortableHeader component can send it’s work back to where the data is actually stored. In the end, this is a fair tradeoff, I decided, because everything is in the logical place, and if we decided not to use the sort functionality, we could erase that dependency.

There is also a little procedural bug in that the sort direction doesn’t quite work as reliably as it should. I just haven’t gotten around to sorting that out — pun intended.

Sound off!

This site uses Akismet to reduce spam. Learn how your comment data is processed.