How To Add Search Functionality to a NextJS Markdown Blog (Part 2)
Tags: react, typescript, nextjs, tailwindcss
Published: 2021-08-25 20:53:21

---
title: How To Add Search Functionality to a NextJS Markdown Blog (Part 2)
published: true
date: 2021-08-25 00:00:00 UTC
tags: react, typescript, nextjs, tailwindcss
description: Adding a search input field that renders a dynamic list of results as a user types
canonical_url: https://bionicjulia.com/blog/add-search-functionality-nextjs-markdown-blog-part-2
---

NOTE: If you haven't done so yet, you'll want to first read [How To Add Search Functionality to a NextJS Markdown Blog (Part 1)](https://bionicjulia.com/blog/add-search-functionality-nextjs-markdown-blog-part-1).

So, picking up where I left off in Part 1, the next step was to figure out how I wanted the UI to look like. I've decided for now, to keep things simple and add a Search component to my blog list screen. You'll probably have seen it on your way here (if you're on my site), but if not, check out [https://bionicjulia.com/blog](https://bionicjulia.com/blog).

To summarise, here are the main features for this `Search` component:

- An input field that allows a user to type in text.
- Upon clicking on the input field, the general list of blog posts links are hidden, so that...
- As the user types, a dynamic list of blog post results matching the search query is shown.
- When the user clicks on a result, they are taken to the blog post.
- When a user clicks outside of the input field, the general list of blog posts are shown again, and the input field search query is cleared.

Let's start with the `Search` component itself, before looking at how to integrate it into my blog posts page.

### Step 1: Create the basic HTML structure and styling

I use Tailwind to style my HTML and added some dark mode styling which you can choose to ignore below. The important things to note here are:

- The `useRef` hook which allows me to reference this entire component (more in the next step).
- The setting up of various states for `active`, `query` and `results`. `active` will basically track whether a user is "actively" in search mode. `results` is an array of blog posts that matches the search `query` a user types in.

```jsx
import { useCallback, useRef, useState } from 'react'
import CustomLink from '@/components/CustomLink'
import { CachedPost } from 'types'

export default function Search() {
  const searchRef = useRef(null) as React.MutableRefObject<HTMLInputElement | null>
  const [query, setQuery] = useState('')
  const [active, setActive] = useState(false)
  const [results, setResults] = useState<CachedPost[]>([])

  const searchEndpoint = (query: string) => `/api/search?q=${query}`

  return (
    <div className="relative" ref={searchRef}>
      <input
        className="border-normal-text focus:outline-none border border-solid
                    box-border w-full rounded-lg
                    text-normal-text text-sm p-2
                    dark:border-off-white dark:bg-background-dark-mode dark:text-off-white"
        placeholder="Search blog posts (this is a work in progress - pls report any bugs!)"
        type="text"
        value={query}
      />
      {active && results.length > 0 && (
        <ul
          className="list-none overflow-hidden mt-2 p-2 absolute top-full inset-x-0 min-h-100px
          bg-white dark:bg-background-dark-mode"
        >
          {results.map(({ frontMatter, slug }) => (
            <li className="bg-white text-normal-text mt-2 leading-4 dark:bg-background-dark-mode last:mb-4" key={slug}>
              <CustomLink href={`/blog/${slug}`} className="text-sm">
                {frontMatter.title}
              </CustomLink>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}
```

### Step 2: Define the input element behaviour

We now want to define how the input element behaves. To do this, we'll need to define what happens `onChange` and `onFocus`.

Looking at `onFocus`, this callback function is called whenever the input element is in focus i.e. when a user clicks on it. In this case, I want the `active` state to be true (i.e. show a list of results) and to add an `onClick` listener which will allow us to define the behaviour we want to happen when a user next clicks on something (whether it's a blog post result, or out of the input element).

Considering `onChange` now, this callback function is called whenever the value of the input element changes e.g. as a user types a search query. In this case, I want to grab the `event.target.value` and set this as my `query` state. I can then call my `searchEndpoint` which calls my api (set up in Part 1) with the `query`. If there are results, I set the `results` state (an array of blog posts). As `onChange` is called every time a user types or deletes a letter, the `results` array is continuously updated making it dynamic.

The final thing to do here is to define what happens `onClick`. What I'm saying here is that if the user clicks anywhere outside of the `Search` component, we should make the `active` state false because the user no longer wants to be in "search mode". To tidy things up, I also want to clear the search query and results array, whilst removing the `onClick` listener since it has now been fulfilled.

Note that I wrapped the `onClick` and `onChange` functions with the `useCallback` hook from React to try to prevent unnecessary re-renders.

```jsx
import { useCallback, useRef, useState } from 'react'
import CustomLink from '@/components/CustomLink'
import { CachedPost } from 'types'

export default function Search() {
  const searchRef = useRef(null) as React.MutableRefObject<HTMLInputElement | null>
  const [query, setQuery] = useState('')
  const [active, setActive] = useState(false)
  const [results, setResults] = useState<CachedPost[]>([])

  const searchEndpoint = (query: string) => `/api/search?q=${query}`

  const onChange = useCallback((event) => {
    const query = event.target.value
    setQuery(query)
    if (query.length) {
      fetch(searchEndpoint(query))
        .then((res) => res.json())
        .then((res) => {
          setResults(res.results)
        })
    } else {
      setResults([])
    }
  }, [])

  const onFocus = () => {
    setActive(true)
    window.addEventListener('click', onClick)
  }

  const onClick = useCallback((event) => {
    if (searchRef.current && !searchRef.current.contains(event.target)) {
      setActive(false)
      setQuery('')
      setResults([])
      window.removeEventListener('click', onClick)
    }
  }, [])

  return (
    <div className="relative" ref={searchRef}>
      <input
        className="border-normal-text focus:outline-none border border-solid
                    box-border w-full rounded-lg
                    text-normal-text text-sm p-2
                    dark:border-off-white dark:bg-background-dark-mode dark:text-off-white"
        onChange={onChange}
        onFocus={onFocus}
        placeholder="Search blog posts (this is a work in progress - pls report any bugs!)"
        type="text"
        value={query}
      />
      {active && results.length > 0 && (
        <ul
          className="list-none overflow-hidden mt-2 p-2 absolute top-full inset-x-0 min-h-100px
          bg-white dark:bg-background-dark-mode"
        >
          {results.map(({ frontMatter, slug }) => (
            <li className="bg-white text-normal-text mt-2 leading-4 dark:bg-background-dark-mode last:mb-4" key={slug}>
              <CustomLink href={`/blog/${slug}`} className="text-sm">
                {frontMatter.title}
              </CustomLink>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}
```

### Step 3: Incorporating Search component into its parent component

The final step is to incorporate our newly created `Search` component into the `Blog` page. As I mentioned above, I want to hide the general list of blog posts whenever "search mode" is activated, so to do this, I need my parent component to monitor some sort of `searchActive` state

I then linked this to an `onFocusHandler` prop on the `Search` component.

```jsx
export default function Blog({ posts, title, description }: BlogProps) {
  const [searchActive, setSearchActive] = useState<boolean>(false)
  const checkSearchStatus = (status: boolean) => {
    if (status) {
      setSearchActive(true)
    } else {
      setSearchActive(false)
    }
  }

  return (
    <SiteContainer title={title} description={description}>
      <div className="mb-6">
        <section>
          // ...blah blah
          <div className="mb-4">
            <Search onFocusHandler={(status) => checkSearchStatus(status)} />
          </div>
          {!searchActive && <BlogPosts posts={posts} />}
        </section>
      </div>
    </SiteContainer>
```

Going back to our `Search` component then, we need to amend it to allow the acceptance of this `onFocusHandler`. The way we communicate to the parent is through the `onChange` and `onClick` functions. If `onChange` is being called, it means that the user is very much in search mode, hence why we set `onFocusHandler(true)`. If a user clicks anywhere outside our `Search` component, they are no longer in search mode and we set `onFocusHandler(false)`.

```jsx
// imports...

interface SearchProps {
  onFocusHandler: (status: boolean) => void
}

export default function Search({ onFocusHandler }: SearchProps) {
  // ...

  const onChange = useCallback((event) => {
    onFocusHandler(true)
    const query = event.target.value
    setQuery(query)
    if (query.length) {
      fetch(searchEndpoint(query))
        .then((res) => res.json())
        .then((res) => {
          setResults(res.results)
        })
    } else {
      setResults([])
    }
  }, [])

  const onFocus = () => {
    setActive(true)
    window.addEventListener('click', onClick)
  }

  const onClick = useCallback((event) => {
    onFocusHandler(true)
    if (searchRef.current && !searchRef.current.contains(event.target)) {
      setActive(false)
      onFocusHandler(false)
      setQuery('')
      setResults([])
      window.removeEventListener('click', onClick)
    }
  }, [])

  return (
    // blah blah
  )
}
```

Go back to our parent then, you can see that we are effectively calling the `checkSearchStatus` function with either `true` or `false`, which then shows or hides our general list of blog posts.

```jsx
export default function Blog({ posts, title, description }: BlogProps) {
  const [searchActive, setSearchActive] = useState<boolean>(false)
  const checkSearchStatus = (status: boolean) => {
    if (status) {
      setSearchActive(true)
    } else {
      setSearchActive(false)
    }
  }

  return (
    <SiteContainer title={title} description={description}>
      <div className="mb-6">
        <section>
          // ...blah blah
          <div className="mb-4">
            <Search onFocusHandler={(status) => checkSearchStatus(status)} />
          </div>
          {!searchActive && <BlogPosts posts={posts} />}
        </section>
      </div>
    </SiteContainer>
```

And that's all there is to it! What do you all think? Any ways I could have simplified this or explained it better? Let me know on [Instagram](https://instagram.com/bionicjulia) or [Twitter](https://twitter.com/bionicjulia) @bionicjulia.