Adding search to a React/Next.js blog

When I built my Next.js-based website, one of the things I really wanted to implement was the ability to search through posts. I recently came across Kent C. Dodds' match-sorter package, a Node.js package which allows for speedy filtering through text arrays. It immediately made me think of its usage as a search algorithm.

Here's the idea: On the client, we can provide the page with an array of post metadata: titles, excerpts and slugs. We add an event listener to the search input which runs matchSorter on the array with the content of the search input, and we display the resulting posts.

I had a look at Kent C. Dodds' website and it turns out that he uses match-sorter as well in his search algorithm!

The basic usage of match-sorter is really simple: You provide it an array and a string to match with, and it returns an ordered array based on what ranks the highest. This makes for a really simple implementation below, with react functions to manage state:

import { matchSorter, rankings } from 'match-sorter'
import {useState, useEffect} from 'react'
const search = ['Hi', 'Hello', 'Hi there']
export default function App(){
  const [input, setInput] = useState('')
  const [results, setResults] = useState([])
  useEffect(() => {
    var res = matchSorter(search, input)
    setResults(res)
  }, [input])
  return (
    <>
      <input type="text" value={input} onInput={e => setInput(e.target.value)}/>
      {results.map(i => <p key={i}>{i}</p>)}
    </>
  )
}

However, in the case of an article search system, we want to do more: we'd like to be able to search not only through an array of titles, but through other information, such as a short excerpt for example. This data will be fetched from the CMS and provided to the browser, resulting in an object like so:

const posts = [
  {title: 'How I built my website', excerpt: 'Trying to figure out how to build my website was a challenge for me. As a web designer, I wanted it to be fully customisable, but I also wanted ease of use to reduce friction and make it easy to maintain.', url: 'https://felixrunquist.com/posts/how-i-built-my-website'},
  {title: 'Implementing online payments with Stripe', excerpt: 'A tutorial on setting up a credit card payment form on your NextJS site. We will cover signing up and the Stripe SDK', url: 'https://felixrunquist.com/posts/online-payments-stripe'},
  {title: 'Next.js HTTP Authentication with JWT and cookies', excerpt: 'I was recently trying to figure out a way to implement simple HTTP authentication for a personal Next.js project. I augmented the basic HTTP auth with JWT and cookies', url: 'https://felixrunquist.com/posts/next-js-http-authentication-jwt-cookies'},
  {title: 'Creating and ordering a custom PCB', excerpt: 'As part of a home automation project, I wanted to house a thermometer, motion sensor and display in a small form factor. I decided to design and order my own PCB.', url: 'https://felixrunquist.com/posts/creating-and-ordering-custom-pcb'}
]

The smart thing about the matchSorter function is that it can sort through arrays of objects as well: You can tell which key you'd like it to sort through, and you can even provide specific criteria for each key: for example, you can choose to match exact case, or complete equality.

Let's see how one could implement this:

const posts = [
  {title: 'How I built my website', excerpt: 'Trying to figure out how to build my website was a challenge for me. As a web designer, I wanted it to be fully customisable, but I also wanted ease of use to reduce friction and make it easy to maintain.', url: 'https://felixrunquist.com/posts/how-i-built-my-website'},
  {title: 'Implementing online payments with Stripe', excerpt: 'A tutorial on setting up a credit card payment form on your NextJS site. We will cover signing up and the Stripe SDK', url: 'https://felixrunquist.com/posts/online-payments-stripe'},
  {title: 'Next.js HTTP Authentication with JWT and cookies', excerpt: 'I was recently trying to figure out a way to implement simple HTTP authentication for a personal Next.js project. I augmented the basic HTTP auth with JWT and cookies', url: 'https://felixrunquist.com/posts/next-js-http-authentication-jwt-cookies'},
  {title: 'Creating and ordering a custom PCB', excerpt: 'As part of a home automation project, I wanted to house a thermometer, motion sensor and display in a small form factor. I decided to design and order my own PCB.', url: 'https://felixrunquist.com/posts/creating-and-ordering-custom-pcb'}
]//Retrieved by the CMS

import { matchSorter, rankings } from 'match-sorter'
import {useState, useEffect} from 'react'
export default function App(){
  const [input, setInput] = useState('')
  const [results, setResults] = useState([])
  useEffect(() => {
    var res = matchSorter(posts, input, {keys: [{key: 'title'}, {key: 'excerpt'}]})
    setResults(res)
  }, [input])
  return (
    <>
      <input type="text" value={input} onInput={e => setInput(e.target.value)}/>
      {results.map(i => <a href={i.url} key={i.title}><h2>{i.title}</h2><p>{i.excerpt}</p></a>)}
    </>
  )
}

As the array to search through is being passed to the client, you probably don't want to provide too large objects which could add to loading times, and cause performance issues. I'd limit myself to one thousand items!

We've now implemented a basic search function, that has an input, a function to display results, and a function that updates results when the input is written to. Let's say the search page is at www.example.com/search. What if one was to reload the page, or if one wanted to share the url to the exact results? One could store the current search state in the URL, with the query parameter. For instance, searching "Recipes" would dynamically update the url so that it points at www.example.com/search?q=Recipes. Like that, refreshing or sharing the search results would bring you back to the exact same state.

This can be done in React using react-router-dom (it can also be done in Next.js using useRouter), a library that manages URLs and paths. First you define a retrieval function, checking on page load whether a query string has been provided or not. When the text input is written to, you can dynamically update the query parameter.

const posts = [
{title: 'How I built my website', excerpt: 'Trying to figure out how to build my website was a challenge for me. As a web designer, I wanted it to be fully customisable, but I also wanted ease of use to reduce friction and make it easy to maintain.', url: 'https://felixrunquist.com/posts/how-i-built-my-website'},
{title: 'Implementing online payments with Stripe', excerpt: 'A tutorial on setting up a credit card payment form on your NextJS site. We will cover signing up and the Stripe SDK', url: 'https://felixrunquist.com/posts/online-payments-stripe'},
{title: 'Next.js HTTP Authentication with JWT and cookies', excerpt: 'I was recently trying to figure out a way to implement simple HTTP authentication for a personal Next.js project. I augmented the basic HTTP auth with JWT and cookies', url: 'https://felixrunquist.com/posts/next-js-http-authentication-jwt-cookies'},
{title: 'Creating and ordering a custom PCB', excerpt: 'As part of a home automation project, I wanted to house a thermometer, motion sensor and display in a small form factor. I decided to design and order my own PCB.', url: 'https://felixrunquist.com/posts/creating-and-ordering-custom-pcb'}
]//Retrieved by the CMS

import { matchSorter, rankings } from 'match-sorter'
import {useState, useEffect} from 'react'
import { useSearchParams, BrowserRouter as Router} from "react-router-dom";
export default function App(){
  return (
    <Router>
      <Search></Search>
    </Router>
  )
}

function Search(){
  const [searchParams, setSearchParams] = useSearchParams();
  const [input, setInput] = useState('')
  const [results, setResults] = useState([])
  useEffect(() => {
    setInput(searchParams.get('search'))//Get search parameters on load
  },[])

  useEffect(() => {
    var res = matchSorter(posts, input, {keys: [{key: 'title'}, {key: 'excerpt'}]})
    setResults(res)
    if(input){//Update query parameters
      setSearchParams({ 'search': input });
    }
  }, [input])
  return (
    <>
      <input type="text" value={input} onInput={e => setInput(e.target.value)}/>
      {results.map(i => <a href={i.url} key={i.title}><h2>{i.title}</h2><p>{i.excerpt}</p></a>)}
    </>
  )
}

That's all there is to it! Luckily, the react-sorter package does most of the heavy lifting, we just did the content fetching and state implementation. Let me know if this was helpful by reaching out to me on Twitter!

External links:

Felix

Felix

Last edited:

F

More posts

Cover Image for Making the Internet More Human

Making the Internet More Human

Navigating the internet has become difficult with all the accessibility issues, pop-ups, cookie banners and advertisements. We’ll explore different ways to make the web a more welcoming place for humans.

Cover Image for Designing spring animations for the web

Designing spring animations for the web

Designing intuitive animations can be tricky. We’ll explore movement in the physical world and see how to replicate this digitally, as well as best practices for responsive animation.