ibragimov
ibragimov

Reputation: 121

React Hook useEffect has a missing dependency: 'context'. Either include it or remove the dependency array

My goal is to make an API request when user types something on input. I'm getting the data successfully. However the component is rerendering twice and giving me this warning. If I include 'context' I'm getting an infinite loop. Here's my code:

Component.js:


const SearchBox = () => {
  const [searchTerm, setSearchTerm] = useState("");
  const { handleSearch, searchResults } = useContext(MovieContext);

  console.log(searchResults);

  useEffect(() => {
    let timer;
    timer = setTimeout(() => {
      handleSearch(searchTerm);
    }, 500);

    return () => clearTimeout(timer);
  }, [searchTerm]);

  const renderResults = () => {
    if (searchResults.length > 0) {
      searchResults.map(result => {
        return (
          <div key={result.Title}>
            <img src={result.Poster} alt={result.Title} />;
          </div>
        );
      });
    }
    return;
  };

  return (
    <>
      <label>
        <b>Search</b>
      </label>
      <input
        className="input"
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
      />
      <div className="dropdown is-active">
        <div className="dropdown-menu">
          <div className="dropdown-content results">{renderResults()}</div>
        </div>
      </div>
    </>
  );
};

On top of this context.searchResults is undefined, although I set the initial value as an empty array. I wanted to know what causing this. What am I doing wrong? Here is my context code below:

Context.js:

const Context = React.createContext("");

export class MovieStore extends Component {
  constructor(props) {
    super(props);
    this.state = {
      searchResults: [],
      handleSearch: this.handleSearch
    };
  }

  handleSearch = async term => {
    try {
      if (term !== "") {
        const response = await axios.get("http://www.omdbapi.com/", {
          params: {
            apikey: apikey,
            s: term
          }
        });
        this.setState({ searchResults: response.data.Search });
      }
    } catch (error) {
      console.log(error);
    }
  };

  render() {
    return (
      <Context.Provider value={this.state}>
        {this.props.children}
      </Context.Provider>
    );
  }
}

Upvotes: 0

Views: 1847

Answers (1)

Max
Max

Reputation: 2036

Exactly the same thing about an infinite loop is mentioned in React docs here. So the cause of the infinite loop is that, in the context render function, you create new value every time render is called.

  render() {
    return (
      <Context.Provider
        // ! value object creates every time render is called - it's bad
        value={{ ...this.state, handleSearch: this.handleSearch }}
      >
        {this.props.children}
      </Context.Provider>
    );
  }

It causes every consumer to rerender when context state updates. So, if you put context in dependencies array of useEffect, eventually, it'll cause an infinite loop, because context value is always different. Here's what happens:

  1. Context makes a search query.

  2. Context state updates with the new data, which causes all consumers to rerender.

  3. In context consumer useEffect sees that context value has been updated and it calls setTimeout which will call for another search in context provider in 500ms.

  4. Consumer calls context to make another search query and we've got an infinite loop!

The solution is to keep context value of the same object, while only updating its properties. It can be done by putting all the necessary properties inside of context state. Like that:

export class MovieStore extends Component {
  handleSearch = async term => {
    try {
      if (term !== "") {
        const response = await axios.get("http://www.omdbapi.com/", {
          params: {
            apikey: "15bfc1e3",
            s: term
          }
        });
        this.setState({ searchResults: response.data.Search });
      }
    } catch (error) {
      console.log(error);
    }
  };

  state = {
    searchResults: [],
    handleSearch: this.handleSearch // <~ put method directly to the state
  };

  render() {
    return (
      <Context.Provider value={this.state}> // <~ Just returning state here
        {this.props.children}
      </Context.Provider>
    );
  }
}

Upvotes: 2

Related Questions