Most of what I do in my day job involves searching data. As I work for a data-focused company, my primary responsibility revolves around putting a nice face on the ugly body of data storage. Some folks can access the database directly to massage results into a form that they need, but we also have a lot of SQL-ignorant folks (who are, of course, prevented from even accessing the data stores directly) who need to be able to look up information.

Traditionally, searching meant taking a bunch of form data and passing it to a stored procedure. The proc would take the criteria, query the stores that need querying, and return a result set. This would get passed back to the data access layer of the app, finessed as necessary, and then pushed to the views of the site for display. This is about as straightforward as a data manipulation application can get, and it works very well…assuming that the here-and-now is the only place where that those specific data results are needed. Unfortunately, there’s always outliers which make a standard operating procedure the absolute wrong way to handle things, such as when a user wants to sort the result set, or page through records. These requests are the stuff of nightmares, but thanks to React hooks, I am now sleeping a bit easier.

What’s a Hook? Well, I’ll just refer you to another post on React hooks that I wrote a few days ago for that answer. Like any good paradigm, once I started writing custom hooks I started finding reasons to write more hooks. Because of the structure of the hooks I had been writing, I figured that performing data searches would be an ideal use for a few reasons:

  1. The structure inherently assumes that I want the content of the hook to be ever-present.
  2. I can pull out just the methods I need from the hook.
  3. I can cram anything relevant to the concept inside the hook and obfuscate it behind a single function call.
  4. I can expose State variables to the world and get real-time updates.

React Components

In React, the rule of thumb is that if there’s a part of a web page that has functionality that can be ripped out and set to operate on it’s own (or with limited interaction to or from other parts of the page) then it should probably be its own component.

Take a look at this screenshot of this site’s homepage. I’ve called out several elements in the structure of the page which could be their own components were this site built with React: social media icons, message rotators, logos and ads, navigation, search, hero blocks and post lists, and even a Twitter widget. Within many of these we would find other, smaller, more targeted components, like in the block for “Palpable Tension”, where the tags, the image, the title, the byline, and blurb would all be smaller components.

This is nice, but what about searching?

With components in mind, consider a page which has a search bar and results displayed beneath. You may be familiar with this concept…

If this were a React application, I would expect the search bar to be one component, and the results display to be another. Were the search and results in a single page, communication would be simple, as what sends the form data to the data store would be what accepts the data returned. With components, though, one bit of code sends the request to the data store, and a different component needs to know about the results. This is where things get squirrely because inter-component communication is not as logical in React as we’d like it to be.

Here’s what I mean. Even though we’ve broken down functionality into discreet, manageable components, we still need to group them together. To do that, we need a “parent” component, usually in the form of a large-scale “page” (this isn’t completely accurate, but will do for illustration purposes). Unfortunately, our “child” components cannot communicate with one another directly. They can communicate with the parent, but only if the parent passes a means of communication into the child component via properties (or “props” as they’re known). That makes the parent component the only one who can A) accept communication from children, and B) communicate with children.

Hooks, therefore, can help us get around this.

Context Matters

React features a construct called a context. A context is a two part structure.

First, we have the provider. A context provider is a wrapper (also known as a higher order component) which makes data available to any components included inside of it.

<MyContext.Provider value={someClassOrFunction}>
   <FirstChildComponent />
   <SecondChildComponent />
</MyContext.Provider>

The data that is made available is defined in the value property of the context tag. What gets assigned here is completely up to the developer, but the strength of the context comes into play when the value is assigned a class instance or a functional construct, such as this MySearchProvider functional wrapper that exposes several other functions:

const MySearchProvider = () =>{
    const [ lastSearchResults, setLastSearchResults ] = useState(null)

    function Search(formVals){ }

    function SortResults(field, direction) { }

    return {
        Search, 
        SortResults,
        lastSearchResults
    }
}

Assigning MySearchProvider to value in a context provider, we are making it available to children of the context component.

Why not just call MySearchProvider when we need it?

This is a good question, because essentially we are insinuating that MySearchProvider is a reusable code block that we can simply import and use when we need it. The key, however, is that when using a context, we are getting access to the actual instance assigned to the value of the context provider. Were we to import and call MySearchProvider.Search(CRITERIA) in components were we wanted to use it, it would work, but the results would only be internal to that instance. meaning that we could only ever get the search results within the component in which we defined the instance of the MySearchProvider. What we need is a way to search once and make the results available everywhere that renders between the MyContext.Provider tags.

Consuming the Context

Once the context provider has been created, we can access the context inside children components in one of two ways. First, we can wrap a child call in its own context tags:

<MyContext.Consumer>
    { val => { <ChildComponent contextVal = {val} /> }
</MyContext.Consumer>

The Context.Consumer will accept a function as its child component (as well as other child components). The value that we assigned in the Provider is passed into this function, which we can then pass into a child component via props. This is explicit, and is easy to see which components will be receiving the value object, but it’s also a pain in the ass to write and maintain, as we have to make provisions for accepting the value object into the child component via props.

The second way is to call useContext within a child component definition.

import React, { useState, useContext } from 'react';
import { MyContext } from '../Context/MyContext'

const MyChildComponent = (props) => {
   const [ formVals, setFormVals ] = useState(null);
   const mySearchProvider = useContext(MyContext);

   const handleFormSubmit = (e) =>{
      e.preventDefault();
      mySearchProcessor.Search(formVals);
   }

   return { //Add form here }
}
   

As you can hopefully see, we are using the useContext hook to almost literally snatch our value assignment object out of thin air. By importing MyContext from the original definition component and passing it to useContext, React knows to start looking up the component stack for the implementation of MyContext.Provider and return the object assigned to value. So long as this component is present as a child of the Provider component, we will get access to the functionality that MySearchProvider provides.

Wait, Where’s the Hook?

Technically, we’ve seen it, but exploded into its constituent parts. If you were to implement a real version of this pseudocode in a React project, it would work; whatever data the Search function retrieves would be stored in a state variable, which we could access from mySearchProvider.lastSearchResults. But we want to make this more manageable so we don’t have to deal with most of this over and over again.

Here’s a more complete pseudocode example of creating a hook.

import React, { useState, useContext, createContext } from 'react';

//Create our search context
const MySearchContext = React.createContext(null);

export function SearchProvider({children}) {
   const provider = useSearchProvider();
   return <MySearchContext.Provider value={provider}>{children}</MySearchContext>
}

export const useSearch = () =>{
   return useContext(MySearchContext);
}

const useSearchProvider = () =>{
   const [ lastSearchResults, setLastSearchResults ] = useState(null);
   //Other state buckets as needed

   function Search(formVals) {
      
      const doSearch = (resolve, reject) => {
         //Do whatever search you need. 
         //Put the results in the lastSearchResults state bucket
         //As this internal funciton is returned as a Promise, 
         //resolve() results and reject() errors
      }

      return new Promise(doSearch)
   }

   return {
      Search,
      lastSearchResults
   }
}

Massive props to useHooks.com for this structure, which I have been reusing now for several weeks.

What we’ve done is encapsulate everything we need to use in one simple file. Here’s how we use it.

//This is a parent component, maybe index.js or app.js. If possible keep the wrapper as low in the stack as
//you can to ensure we don't have in-memory stuff hanging around where it's not going to be used. 

//Import the wrapper from the hook file
import { SearchProvider } from '../Hooks/MySearchProvider.js';

//These are the children components: search form and search results.
import { MySearchForm, MySearchResults } from '../Search';


const App = () => {
   return (
      <SearchProvider>
         <MySearchForm />
         <MySearchResults />
      </SearchProvider>
}

Rather than wrapping our children components in the whole Context.Provider format, we only have to wrap children in the HOC component we created, which takes care of wrapping children with the Provider.

//This is the MySearchForm component (fragment)

//Import the hook itself
import { useSearch } from '../../Hooks/MySearchProvider.js';

const MySearchForm = (props) => {

   const { Search } = useSearch();

   const handleSearch = (e) =>{
      e.preventDefault();
      //To just execute the search:
      Search(formVals);

      //To use the search results in this component:
      Search(formVals)
      .then(results=>{
         //Do something local with results
      }
      .catch(err => console.log(err) }
   }

   return (
      //Form goes here
   )
}     

Inside the search component, we are importing the actual “hook”, useSearch. Via destructuring, we’re pulling out only the Search function from the hook since that’s the only thing we’re interested in at this point.

I have provided two ways to work with Search, assuming the function was defined as shown in the hook definition pseudocode above. The first will simply execute the search, storing the results in a State variable within the hook. The second will do the exact same thing, but will also give access to the results immediately. Because we returned a Promise from Search, we are able to take action once the function returns a value.

//This is the MyResults component (fragment)

//Import the hook itself
import { useSearch } from '../../Hooks/MySearchProvider.js';

const MySearchResults = (props) => {

   const { lastSearchResults } = useSearch();

   if (lastSearchResults) {
      //Return your results component or build output here
   else {
     //Return null because there are no lastSearchResults
   }
}

In the results component, we might be rendering a table with the results of the search, or we might be using MySearchResults as a factory for choosing subcomponents to render based on the presence or absence of results, returned HTTP status codes, or process flags (IDLE, ACTIVE, COMPLETE, ERROR, etc).

Note that we’re not using useEffect here, which can be used to gate execution and/or logic. This is because we want this component to re-render every time lastSearchResults changes. lastSearchResults will change, as we know, whenever the Search function of the hook is called (or if something else internal to the hook changes the State bucket…like a sort, page, or filter). If for some reason the search results re-renders outside of an update to lastSearchResults, it’ll still display the results that are stored in that state bucket.

Caveats

One thing that would need to be implemented inside the hook is a way to clear or reset the internal State buckets so that another search can be performed. When the context itself goes out of scope, the instance of the Provider will be reset naturally, but we certainly don’t want to see old results on a new search! We also need to make sure that all State buckets are reset, as a stray HTTP status code, say, could lead to false results.

Another thing to be aware of is that because this hook stores search results in a state bucket, the data becomes stale over time. If you have an application that must avoid concurrent record access, for example, then this will not be an optimal solution as different users whose search results may contain overlapping results will never be aware of changes made to the underlying live data store.

Good Housekeeping

Because this hook concept is designed to deal with search data, we can add a bunch of other internal functions. One might be to filter the result set by passing in criteria to a FilterResults function. Another would be to sort the results by column using a SortResults function. PageResults would also be a useful function if large datasets are expected.

Because of the way the hook is designed, implementing each of these would solve one particularly nasty problem: re-querying the database. In many of our older applications, we have one query which searches the database, and then another that re-queries the database on another page for a single record based on which record the user selected. We have also used procedures which will pass sorting values to the procedure in order to affect the order by clause in the original query. For queries which result in large datasets, this results in unacceptable refresh delays, which is why creating a search hook and storing results in a temporary state bucket is so attractive.

Sound off!

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