React: Throttling API Requests πŸš€

Intro

We are working on a search bar where the user enters a search term and then we query the Wikipedia API and show the results on the screen. Currently the Wikipedia API is queried after every key press. Sometimes that is fine, or even desireable. But it’s good to know how to throttle requests so we have that in our toolkit. For example, sometimes querying an API is a service that you pay for (Like Algolia Search) and sometimes you have a limited number of requests allowed in a given time-frame.

Setting API request on a timer

There are many ways to solve this problem, but one way is to set a timer on the API request so that the request is only sent if the user has not input any new input changes for a set amount of time (to indicate that they have stopped typing). Let’s work on setting a timer for 500ms.

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)
    }

    // wait 500ms before executing search    setTimeout(() => {      // do not search if input is empty
      if (term) {
        search()
      }
    }, 500)  }, [term])

  const searchResultsMapped = results.map(result => {
    return (
      <div className="item" key={result.pageid}>
        <div className="right floated content">
          <a
            className="ui button"
            href={`https://en.wikipedia.org?curid=${result.pageid}`}
            target="_blank"
          >
            Read Article
          </a>
        </div>
        <div className="content">
          <div className="header">{result.title}</div>
          <span dangerouslySetInnerHTML={{ __html: result.snippet }}></span>
        </div>
      </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

This is great, however currently we are still executing a search for every keypress, it is just that they are all delayed by half a second. We need to adjust this so that every time a key gets pressed the timer gets reset.

How To Cancel a Timer

First we need to learn the method for cancelling a timer! The first thing that we need to understand is that every time you set a timer, the browser assigns that timer an id, which is a number. For example if we set a timer directly in the console we get the following.

timeout id # 963

Therefore if we set our timer to a variable, we can reference that variable later. Which is what we will need to do in order to cancel it.

clearTimeout()

clearTimeout() is the method that is used to cancel a current timer. It takes one argument, and that is the ID of the timer you wish to clear.

function myFunction() {
  let timer = setTimeout(function () {
    alert("Hello")
  }, 3000)
}

function myStopFunction() {
  clearTimeout(timer)
}

CleanUp Function

A cleanup function is a function that we set to do some type of housekeeping every time the useEffect function is called except the initial load. In this case we are going to be using it to cancel the timer. Let us take a look at how a cleanup function would look inside of the useEffect function.

useEffect(() => {
  console.log("Initial render AND Term Change")

  return () => {    console.log("CLEANUP: Run on next cycle");   }
    }, 500)
  }, [term])

So now we would simply need to put a clearTimeout function inside of our cleanup function.

Putting Them Together

Note in the first highlight that we have assigned the timer ID to a variable, and we reference that variable in the second highlight.

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);
    };

    // wait 500ms before executing search
    let timeoutID = setTimeout(() => {      // do not search if input is empty
      if (term) {
        search();
      }
    }, 500);
    
    // CLEANUP: clear current timer
    return () => {      clearTimeout(timeoutID);    };  }, [term]);

  const searchResultsMapped = results.map((result) => {
    return (
      <div className="item" key={result.pageid}>
        <div className="right floated content">
          <a
            className="ui button"
            href={`https://en.wikipedia.org?curid=${result.pageid}`}
            target="_blank"
          >
            Read Article
          </a>
        </div>
        <div className="content">
          <div className="header">{result.title}</div>
          <span dangerouslySetInnerHTML={{ __html: result.snippet }}></span>
        </div>
      </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;

And this is working perfectly. The search input will now wait for 500ms of inactivity before making the API request.

Searching On Initial Render

We do have one last small problem to solve. Because of our delay on running the search function, the initial API request for the default search term on page-load is unnecessarily delayed by 500ms. We want to make that initial request immediately.

One way that we could do this would be to just set up a count variable, where we add 1 to the count on every run through useEffect. And then run search() inside of a condition check if the count was 0.

Another method would be to use a simple boolean

// if there is a search term, but no search results
// aka this is probably the initial page load
if (term && !results.length){
  search()
} else {
  // everything we were doing before
}

Which would give us the following

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);
    };

    // run search immediately if this is initial page load    if (term && !results.length) {      search();    // else throttle search requests with timer      } else {      // wait 500ms before executing search      let timeoutID = setTimeout(() => {        // do not search if input is empty        if (term) {          search();        }      }, 500);
      // CLEANUP: clear current timer
      return () => {
        clearTimeout(timeoutID);
      };
    }
  }, [term]);

  const searchResultsMapped = results.map((result) => {
    return (
      <div className="item" key={result.pageid}>
        <div className="right floated content">
          <a
            className="ui button"
            href={`https://en.wikipedia.org?curid=${result.pageid}`}
            target="_blank"
          >
            Read Article
          </a>
        </div>
        <div className="content">
          <div className="header">{result.title}</div>
          <span dangerouslySetInnerHTML={{ __html: result.snippet }}></span>
        </div>
      </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;

Github Repo

Ncoughlin: React Widgets