Learn render props by building a Lunr powered reusable client-side search component.

Jonathan Bowers
Two Story Robot
Published in
10 min readOct 24, 2018

There are several components that give easy access to search powered by server side technology (like react-instantsearch for algolia, or reactivesearch for elasticsearch). These are awesome if you have a big set of data to search through, but sometimes you have a smaller set of data and you need to do that searching client side. Maybe you just don’t want to set up a server-side search solution or you need to perform the search operations offline.

Lunr is a great full-text search library that can run client-side. Just give it some documents, and search through it. For smaller sets of data, this is probably all you need. So let's create a component that gives us client-side search capabilities!

Note: This tutorial assumes that you are familiar with React (via Create React App) and ES6 syntax.

What we’ll cover:

  • Rendering astronauts in a simple list
  • Adding Lunr to make the list searchable
  • Abstracting document structure allowing documents to be passed in as props
  • Reusability through a children render prop function
  • Catching errors with an error boundary component
“photo of moon” by NASA on Unsplash

Getting Started with a Simple List

We’ll use create-react-app to get our react environment setup, so run

npm init react-app react-search-lunr
cd react-search-lunr
npm start

To start, we’ll make a simple component that lists some astronauts. Eventually, this will have some search capability, but we need to start somewhere. So, for now, just iterate over a list of astronauts and render them.

Grab a copy of the moonwalkers file:

cd src/
curl -O https://gist.githubusercontent.com/jonotron/d97dd22b2a64a1a672d938c870b42561/raw/2ce99e467d0455cd07ee3ab1e33586cf36e47288/moonwalkers.js

Now add a new component, src/react-search-lunr.js:

And change the contents of App.js that create-react-app made for us to this:

Our component just grabs moonwalkers, and iterates over them to display their name and body. Don't forget, you need to use key={i} when we map in JSX.

Go to your browser and you should see something like this:

Add Search with Lunr and an Input

So that’s great, but not super functional. Now we’ll add some simple Lunr search capabilities by adding an input element and using its value to power the results. You'll need to install the lunr package

npm install lunr

At the top of your component, import the lunr package

import lunr from 'lunr'

Lunr requires that you create an index first. You need to specify the unique identifier (called ref in Lunr lingo), all the fields you want to search on, and all the documents to search. The component constructor seems like a good place to do this.

In your component add a constructor and build the index.

This adds the name and body fields to our index and sets the id field as the ref field (or the unique ID for the document) and iterates over each moonwalker and adds it to the index. We then set up some state in our component that we are going to use just below. filter will contain the text that the user types in the input box, index is our Lunr index, and results will contain the results of a search in the index with the filter text.

Note: You don’t have to add all the fields in the documents to the index, only those that you want to search. If your documents have some extra metadata that doesn’t make sense to search, don’t add them with the field() function.

Now let’s add an input element so we can type a filter and search the index. Add a handler function and change your render() function to your component like this:

Now our render() function uses an input element so the user can type search filters. The input from the user is stored as state (this.state.filter).

What’s going on in the handleChange function?

Our input is a controlled component, so handleChange is updating our component's state with the filter text from the input and using that text to search the Lunr index.

When you search for something in Lunr, it does not return the matching document, just the unique ID (and some meta data) about the matching document. Remember, we had to add the id field as the ref in the Lunr index? When you .search() on a Lunr index, it returns an array of objects with ref as a field. In our case, this refers to the id field of the original moonwalker document.

The .map() call is taking each result from the .search(), destructuring out the ref field, and then adding an item field to the results. This item field is a reference to the original document (making it really easy to show the data from the original document in our render()). moonwalkers.find(m ⇒ m.id === ref) finds the original moonwalker.

This allows us to use {result.item.name} in the render() function.

Try searching for an astronaut. Type ‘alan’ into the input box and you will see results for ‘alan bean’, ‘edgar mitchel’ (because Alan Shepard is mentioned in his bio), and ‘alan shepard

“astronaut action figures” by BRUNO CERVERA on Unsplash

Passing Search Data Through Props

Great, we have a component that we can use to search for moonwalkers. But it’s not exactly reusable. It only searches moonwalkers that the component provides for us. We’d like to provide the moonwalkers to the component, declaratively. Let's pass moonwalkers in as a prop.

Update your App.js to look like this:

And remove the moonwalkers import from the component and change the constructor() and handleChange functions to use moonwalkers as a prop, like this:

That’s a bit more reusable, now our data can be passed in declaratively at runtime as a prop.

Allow Any Document Structure

Our component still has some assumptions about the data. It loads specific fields into the index. Let’s abstract that away and allow the fields to also be passed in via props.

Change the component’s constructor to set the ref via an id prop and iteratively add fields from a fields prop. We also add the documents to state as we want to ensure that index and documents are based on the same set of data:

If you’re adding propTypes, it should look like this now:

ReactSearchLunr.propTypes = {
documents: PropTypes.arrayOf(PropTypes.object),
id: PropTypes.string.isRequired,
fields: PropTypes.arrayOf(PropTypes.string).isRequired
}

Update the handleChange function to use the documents on state:

Now you can update App.js to pass in these props:

Note: We can’t use ref as our prop name because that's reserved by React for dom references, so we call it id.

Great! Now our Lunr index can be setup declaratively with the fields and ref (as id) passed in as props. However, our component is still tightly coupled to our moonwalkers document structure. The render function explicitly uses the name and body fields of the results. As a user of this component, I don't want to have to always render a h1 and a p element for each moonwalker. I want to do my own thing.

Composition Through Children as a Render Prop

What I want is to declaratively display results however I please. This is where render props are helpful. They let you pass in a function that returns JSX that your component will render for you.

Hopefully, this makes more sense as an example. Change the render() function of your component to look like this:

And update your propTypes:

ReactSearchLunr.propTypes = {
documents: PropTypes.arrayOf(PropTypes.object),
id: PropTypes.string.isRequired,
fields: PropTypes.arrayOf(PropTypes.string).isRequired,
children: PropTypes.func
}

So our render function used to just map over the results and return a h1 and a p tag. It's still mapping over the results but instead of returning JSX, it's calling the children prop as if it were a function, and passing to it each result.

children is just a prop, we can ask that it be anything we want with propTypes. Here, we've asked that it be a function, because we invoke it for every result in our render() function. As long as the function that gets passed into the children prop returns JSX, things will work just fine.

So now our App.js render() function can be updated like so:

We’ve declared the children of <ReactSearchLunr> to be a function (not some elements or other components like you might be used to). This function will be called by <ReactSearchLunr> with every result from our search. So that <p key={result.ref}> tag (and its contents) will be repeated wherever children is called in the <ReactSearchLunr> component.

This is great because as a user of the ReactSearchLunr component, I can get access to the results without having to monkey with the internals of the component. I just know that results will be provided to my function that I pass in as children to the component.

Now I’m free to display the results however I want or even use those results to look up data from other sources.

In this case we’ve changed the display to render the name inline with the body.

This is much more reusable, but what about the search input box? Maybe I don’t always want the input box above the results like this, or maybe I want to provide the search text some other way or from elsewhere in my application. Let’s provide the search string as a prop too.

Providing everything as a prop

Lets get rid of that input inside of our component and just pass filter in as a prop. Remove the handleChange function and add a getResults function and change the render() function like so:

You’ll want to update your propTypes to:

ReactSearchLunr.propTypes = {
documents: PropTypes.arrayOf(PropTypes.object),
id: PropTypes.string.isRequired,
fields: PropTypes.arrayOf(PropTypes.string).isRequired,
filter: PropTypes.string,
children: PropTypes.func
}

Now we can pass filter into our component as a prop. Our render() function will recompute the results anytime the filter changes (remember, React components are re-rendered whenever a prop changes) using the getResults() function.

Let’s update the App.js file to show the input box to the user:

Better Composition with Children Render Function

Ok, now lets modify it slightly so we can have even more control over the results. Let’s send the entire result set to the children render prop function instead of calling it with every result. Change the ReactSearchLunr component's render() function to this:

render() {
return this.props.children(this.state.results)
}

Now our component simply calls the children render prop with the results, and we can compositionally choose how to render the results.

Update App.js to pass in a render function that takes all the results:

Now whenever the results are updated, ReactSearchLunr will call our renderResults function with the results. From there we can do whatever we want (including filtering or sorting the results if we wanted).

Tip: I find too many nested functions as children a bit hard to read, so I’ve opted to just pass a function into the children of my ReactSearchLunr component. You could also pass it in directly as the children prop (<ReactSearchLunr children={renderResults} />)

Handling Errors

If you try using some of the more advanced features of Lunr (e.g. searching for name:alan) you will get a QueryParseError.

Lunr is throwing an error when it tries to search for the filter. Since we are not catching this error, react is unmounting the entire application. We want to catch this error and allow the user to continue typing. For this we need an error boundary which we can use to wrap our ReactSearchLunr instance.

In our App.js create a new class component called ErrorBoundary and wrap it around the ReactSearchLunr component.

This ErrorBoundary component will now catch any error thrown by its children. There is a slight issue with the error boundary in that it doesn't reset it's state to try rendering ReactSearchLunr when the filter changes. We could pass filter into the ErrorBoundary and have it derive state from the filter prop, but that's more complicated than we need. Here's a little React trick: you can change the key prop of any component to reset it. So we just set key={this.state.filter} on the ErrorBoundary which will reset the component whenever the filter changes. Easy peasy.

Now if you try typing name: React will still display the error in development mode but if you close the error overlay you can see that the underlying app still works.

“man holding flashlight standing on rock” by Warren Wong on Unsplash

Conclusion

Awesome! Now we have a nice component that can be re-used pretty much however you want.

Sure there are some performance improvements we could make, but we’ve covered a lot of things. Hopefully, this gives you a bit more understanding about render props and how they can help you build very reusable components. We’ve also dabbled a bit in error boundaries and how to prevent component errors from wrecking your day.

If you are just interested in using the ReactSearchLunr component, we’ve open sourced it here: https://github.com/TwoStoryRobot/react-search-lunr

We built this little component as a result of some work with a client. If you’re looking for a capable team of javascript folks, we’d love to chat: https://www.twostoryrobot.com

Please share this with your fellow React friends. Render props are not as scary as they might seem at first. Any 👏 are appreciated.

--

--