0

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

1 Answer 1

2

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>
    );
  }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the answer. I tried your solution by putting the method directly to the context state. If I declare the state up top, I get undefined for handleSearch function. If I put it below the function, it seems to be working. However I'm still getting an infinite loop.
Can you replace your code in a question with new the code, so I can assess what it wrong

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.