React Hooks: Wikipedia search bar with useEffect hook

Intro

We are going to make a search bar that queries the Wikipedia API and shows the results on the screen.

We start of course with our main App component.

App.js
import React from "react"

import Search from "./components/Search"

export default () => {
  return (
    <div>
      <Search />
    </div>
  )
}

and our Search component

components/Search.js
import React, { useState } from "react"

const Search = () => {
  const [term, setTerm] = useState("")

  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label>Search Term</label>
          <input
            className="input"
            value={term}
            onChange={e => setTerm(e.target.value)}
          />
        </div>
      </div>
    </div>
  )
}

export default Search

The search component is very simple. We make an input and use the useState hook to track the value of the input.

The useEffect Hook

The useEffect hook allows functional components to use something like lifecycle methods. Note however that you will never use lifecycle methods like componentDidMount in a functional component.

We configure the useEffect hook to run some code automatically in one of three scenarios.

  1. When the component is rendered for the first time only
  2. When the component is rendered for the first time and whenever it re-renders
  3. When the component is rendered for the first time and whenever it re-renders and some piece of data has changed

The useEffect hook takes two arguments.

  1. A function
  2. Direction on when that function is executed (one of the 3 scenarios above)

The form of the direction is always going to be

  1. An empty array
  2. An array with one or more elements inside of it
  3. No array at all

So all in all that would look roughly like this

useEffect({function}, {array})

useEffect second argument samples

useEffect Sample

Here we can see an example of the useEffect hook in action. Because our little input here has an onChange listener, the component is going to re-render with every keypress into the input. Which means that our 2nd console log is going to fire with every keypress. By contrast our console log inside of the useEffect hook is only going to fire on the initial render.

components/Search.js
import React, { useState, useEffect } from "react"

const Search = () => {
  const [term, setTerm] = useState("")

  // first argument of useEffect is always a function
  // second argument controls when code is executed
  useEffect(() => {
    console.log("RUN ON INITIAL RENDER")  }, [])

  // every keypress re-renders component
  console.log("RUN ON EVERY RENDER")
  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label>Search Term</label>
          <input
            className="input"
            value={term}
            onChange={e => setTerm(e.target.value)}
          />
        </div>
      </div>
    </div>
  )
}

export default Search

log runs on every keypress

We could then modify the second argument of useEffect to remove the array, and the console log will then run for every re-render.

components/Search.js
import React, { useState, useEffect } from "react"

const Search = () => {
  const [term, setTerm] = useState("")

  // first argument of useEffect is always a function
  // second argument controls when code is executed
  useEffect(() => {
    console.log("RUN AT INITIAL RENDER AND EVERY RE-RENDER")
  })
  // every keypress re-renders component
  console.log("RUN ON EVERY RENDER")

  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label>Search Term</label>
          <input
            className="input"
            value={term}
            onChange={e => setTerm(e.target.value)}
          />
        </div>
      </div>
    </div>
  )
}

export default Search

both logs runs on every keypress

If we did a third example where we included an array with an argument in it (like term) we would see the exact same thing in this case, because the term is changing with every keypress.

Now that we understand how useEffect works, we can go ahead and make our API request inside of our function.

API Request to Wikipedia

As usual, when making API request in React we are going to use the Axios package, so get that installed. Then we need to figure out how we are going to configure the useEffect hook.

// first argument of useEffect is always a function
// second argument controls when code is executed
useEffect(() => {
  // search wikipedia api
}, [term])

This is a good start as we have specified WHEN we want the API request to happen (on all component renders and term changes). The API request itself is a bit tricky.

No Async Await in useEffect Hook

You would think that we could do the following

useEffect(async () => {
  await axios("api-url")
}, [term])

But we are not allowed to use Async directly as the first argument. This is just something you need to know, but you will get a console warning if you break it.

Ways To Get Around No Async

The best way is to just use a helper function inside of this function and use Async on that.

useEffect(() => {
  const search = async () => {
    await axios.get("api-url")
  }
  search()
}, [term])

This is the recommended method to get around this restriction.

And just for fun here is an alternate syntax on this.

useEffect(() => {
  ;(async () => {
    await axios.get("api-url")
  })()
}, [term])

Wait what is this syntax? Double parentheses ()()? This is a way to define an anonymous function and immediately call it, and it is valid syntax. Learn something new everyday.

Final Request Sample

Here is what we end up with for our Wikipedia request.

components/Search.js
import React, { useState, useEffect } from "react"
import axios from "axios"

const Search = () => {
  const [term, setTerm] = useState("")

  useEffect(() => {    const search = async () => {      await axios.get("https://en.wikipedia.org/w/api.php", {        params: {          action: "query",          list: "search",          origin: "*",          format: "json",          srsearch: term,        },      })    }    search()  }, [term])
  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label>Search Term</label>
          <input
            className="input"
            value={term}
            onChange={e => setTerm(e.target.value)}
          />
        </div>
      </div>
    </div>
  )
}

export default Search

And if we check out network tab we can see that our API requests are successful.

successful API request

Manipulating Search Results

Now that we are receiving data from the Wikipedia API we need to be able to manipulate it. We need to create a new state in the component and have that state represent the search results.

components/Search.js
import React, { useState, useEffect } from "react"
import axios from "axios"

const Search = () => {
  const [term, setTerm] = useState("")
  const [results, setResults] = useState([])
  useEffect(() => {
    const search = async () => {
      const { data } = await axios.get("https://en.wikipedia.org/w/api.php", {
        params: {
          action: "query",
          list: "search",
          origin: "*",
          format: "json",
          srsearch: term,
        },
      })
      setResults(data)    }

    if (term) {
      search()
    }
  }, [term])

  console.log(results)
  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label>Search Term</label>
          <input
            className="input"
            value={term}
            onChange={e => setTerm(e.target.value)}
          />
        </div>
      </div>
    </div>
  )
}

export default Search

And this does get us the request results in the console, however we have a small problem.

initial request is blank

Our initial request is a blank string, so we are requesting nothing, and Wikipedia is whining about that. There are a couple of things that we could do to fix this. The first is to just implement a quick check to make sure that term exists before we run search()

components/Search.js
import React, { useState, useEffect } from "react"
import axios from "axios"

const Search = () => {
  const [term, setTerm] = useState("")
  const [results, setResults] = useState([])

  useEffect(() => {
    const search = async () => {
      const { data } = await axios.get("https://en.wikipedia.org/w/api.php", {
        params: {
          action: "query",
          list: "search",
          origin: "*",
          format: "json",
          srsearch: term,
        },
      })
      setResults(data)
    }
    // quick check for term value so we don't request search of empty string    if (term) {      search()    }  }, [term])

  console.log(results)

  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label>Search Term</label>
          <input
            className="input"
            value={term}
            onChange={e => setTerm(e.target.value)}
          />
        </div>
      </div>
    </div>
  )
}

export default Search

or another solution would be provide a default search term, like React…

components/Search.js
import React, { useState, useEffect } from "react"
import axios from "axios"

const Search = () => {
  const [term, setTerm] = useState("React")  const [results, setResults] = useState([])

  useEffect(() => {
    const search = async () => {
      const { data } = await axios.get("https://en.wikipedia.org/w/api.php", {
        params: {
          action: "query",
          list: "search",
          origin: "*",
          format: "json",
          srsearch: term,
        },
      })
      setResults(data)
    }
    search()
  }, [term])

  console.log(results)

  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label>Search Term</label>
          <input
            className="input"
            value={term}
            onChange={e => setTerm(e.target.value)}
          />
        </div>
      </div>
    </div>
  )
}

export default Search

We will take the 2nd route so that our users have something to look at when the widget loads.

Mapping and Displaying Search Results

Now that we have the search results, we just need to map over them and display them.

components/Search.js
import React, { useState, useEffect } from "react"
import axios from "axios"

const Search = () => {
  const [term, setTerm] = useState("React")
  const [results, setResults] = useState([])

  useEffect(() => {
    const search = async () => {
      const { data } = await axios.get("https://en.wikipedia.org/w/api.php", {
        params: {
          action: "query",
          list: "search",
          origin: "*",
          format: "json",
          srsearch: term,
        },
      })
      setResults(data.query.search)
    }
    search()
  }, [term])

  const searchResultsMapped = results.map(result => {    return (      <div className="item" key={result.pageid}>        <div className="content">          <div className="header">{result.title}</div>        </div>        {result.snippet}      </div>    )  })
  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label>Search Term</label>
          <input
            className="input"
            value={term}
            onChange={e => setTerm(e.target.value)}
          />
        </div>
      </div>
      <div className="ui celled list">{searchResultsMapped}</div>    </div>
  )
}

export default Search

search results rendered

Which is great except that the content is returned in HTML format, so we need to change how we handle this data. If we don’t we could be vulnerable to an XSS attack Wikipedia:Cross-site Scripting.

We will cover that in our next post.

GitHub Repo

Ncoughlin: React Widgets