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:
- Match-sorter documentation: https://github.com/kentcdodds/match-sorter
- URL-based state: https://blog.logrocket.com/use-state-url-persist-state-usesearchparams/