1

I have been working on an SPA with React 18 and The Movie Database (TMDB) API.

I am currently working on a search functionality.

In the Searchbox.jsx component I have:

import { ReactComponent as Magnifier } from '../../icons/magnifier.svg';
import { useEffect, useState } from 'react';
import axios from 'axios';

function Searchbox({ movie }) {

 const API_URL = 'https://api.themoviedb.org/3';
 const [searchInput, setSearchInput] = useState('');
 const [results, setResults] = useState([]);
 const timeOutInterval = 1000;

const doMovieSearch = async (e) => {
setSearchInput(e.target.value);

if (searchInput.length >= 3) {
  const { data: { results } } = await axios.get(`${API_URL}/search/movie?query=${searchInput}`, {
    params: {
      api_key: process.env.REACT_APP_API_KEY
    }
  });

  setResults(results);
  console.log(results);
 }
}

  return (
    <form className="search_form w-100 mx-auto mt-2 mt-md-0">
      <div className="input-group">
        <input className="form-control search-box" type="text" value={searchInput} onKeyUp={debounceMovieSearch} placeholder="Search movies..." />
        <div className="input-group-append">
          <button className="btn" type="button">
            <Magnifier />
          </button>
        </div>
      </div>
    </form>
  );
}

export default Searchbox;

The problem

For a reason I have been unable to figure out, doing a search fails with this error:

Cannot read properties of undefined (reading 'target').

The error is at the line setSearchInput(e.target.value).

The line console.log(results) does not log anything.

Questions

  1. What am I doing wrong?
  2. What is the most reliable way to fix this issue?
Razvan Zamfir
  • 4,209
  • 6
  • 38
  • 252
  • You aren't passing the event to `doMovieSearch` it would need to be `const debounceMovieSearch = (event) => {setTimeout(doMovieSearch, timeOutInterval, event)}` (you can pass arguments to the callback of `setTimeout` after the delay, see: [setTimeout()](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout)) – pilchard Jul 17 '23 at 12:43
  • Also `console.log(results);` right after a setState call will never log the updated state value; it won't be available until the next render cycle. see: [The useState set method is not reflecting a change immediately](https://stackoverflow.com/questions/54069253/the-usestate-set-method-is-not-reflecting-a-change-immediately) – pilchard Jul 17 '23 at 12:47
  • And lastly, it's not really a debounce if you never clear the previous timeout calls. – pilchard Jul 17 '23 at 12:50

2 Answers2

1

To fix this issue, you can use a functional update with setSearchInput to ensure that you're always using the latest value of searchInput. Here's the modified code:

const doMovieSearch = async (e) => {
  const inputValue = e.target.value;
  setSearchInput(inputValue);

  if (inputValue.length >= 3) {
    const { data: { results } } = await axios.get(`${API_URL}/search/movie?query=${inputValue}`, {
      params: {
        api_key: process.env.REACT_APP_API_KEY
      }
    });

    setResults(results);
    console.log(results);
  }
}

const debounceMovieSearch = () => {
  setTimeout(() => {
    doMovieSearch({ target: { value: searchInput } }); // Pass a mock event object with the latest searchInput value
  }, timeOutInterval);
}
Hoang Long
  • 446
  • 4
  • 5
  • I have made the changes. I replaced _onKeyUp_ with _onChange_ event in order to fix the error: _Warning: You provided a `value` prop to a form field without an `onChange` handler_ There is no error, but `console.log(results)` does not show any results. – Razvan Zamfir Jul 17 '23 at 13:12
  • @RazvanZamfir did you read my comment? You can't `console.log` right after a setState call. see: [The useState set method is not reflecting a change immediately](https://stackoverflow.com/questions/54069253/the-usestate-set-method-is-not-reflecting-a-change-immediately) – pilchard Jul 17 '23 at 13:14
  • Where should I console.log? I need to see the results... – Razvan Zamfir Jul 17 '23 at 13:18
  • @RazvanZamfir logging is a side effect. For that use `useEffect(() => { console.log(results), [results])` – Mulan Jul 17 '23 at 13:19
1

You need to pass the event to debounceMovieSearch and pass it further to doMovieSearch:

import { ReactComponent as Magnifier } from '../../icons/magnifier.svg';
import { useState } from 'react';
import axios from 'axios';

function Searchbox({ movie }) {

  const API_URL = 'https://api.themoviedb.org/3';
  const [searchInput, setSearchInput] = useState('');
  const [results, setResults] = useState([]);
  const timeOutInterval = 1000;

  const doMovieSearch = async (e) => {
    setSearchInput(e.target.value);

    if (e.target.value.length >= 3) {
      const { data: { results } } = await axios.get(`${API_URL}/search/movie`, {
        params: {
          api_key: process.env.REACT_APP_API_KEY,
          query: e.target.value
        }
      });

      setResults(results);
    }
  }

  const debounceMovieSearch = (e) => {
    setTimeout(() => doMovieSearch(e), timeOutInterval);
  }

return (
    <form className="search_form w-100 mx-auto mt-2 mt-md-0">
      <div className="input-group">
      <input className="form-control search-box" type="text" defaultValue={searchInput} onKeyUp={debounceMovieSearch} placeholder="Search movies..." />
        <div className="input-group-append">
          <button className="btn" type="button">
            <Magnifier />
          </button>
        </div>
      </div>
    </form>
 );
}
export default Searchbox;

Additionally, I've made a small adjustment to the condition in doMovieSearch to use e.target.value.length instead of searchInput.length, as we want to check the length of the user's input, not the previous state of searchInput.

Razvan Zamfir
  • 4,209
  • 6
  • 38
  • 252