4

I am trying to use fetching helper function inside my functional component but for React complains:

src\components\Header.js

Line 19:30: React Hook "useFetch" is called in function "handleSearch" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter react-hooks/rules-of-hooks

I'm aware that functional components should start with a capital letter however, useFetch isn't a components, it's simply a helper function. What am I doing wrong? I know I can solve this by just calling my function UseEffect instead of useEffect but should I?

Here's my helper.js

import { useState, useEffect } from 'react';

export const GH_BASE_URL = 'https://api.github.com/';

export const useFetch = (url, options) => {
    const [response, setResponse] = useState(null);
    const [error, setError] = useState(null);
    const [isLoading, setIsLoading] = useState(false);
    
    useEffect(() => {
        const fetchData = async () => {
            setIsLoading(true);

            try {
                const res = await fetch(url, options);
                const json = await res.json();
                setResponse(json);
                setIsLoading(false);
            } catch (error) {
                setError(error);
            }
        };
        
        if(url) {
            fetchData();
        }
    }, []);
   
    return { response, error, isLoading };
};

and my Header.js component

import React, { useState } from 'react';
import { useFetch, GH_BASE_URL } from '../helpers';

const REPO_SEARCH_URL = `${GH_BASE_URL}/search/repositories?q=`;

function Header(props) {
    const [searchValue, setSearchValue] = useState('');

    function handleChange(event) {
        setSearchValue(event.target.value);
    }

    async function handleSearch(event) {
        event.preventDefault();

        const response = useFetch(`${REPO_SEARCH_URL}${searchValue}`);
    }
    
    return (
        <header>
            <form 
                onSubmit={handleSearch}
                className="container"
            >
                <input
                    value={searchValue}
                    onChange={handleChange}
                    className="search-input"
                    placeholder="Search a repository">
                </input>
            </form>
        </header>
    );
}

export default Header;
1
  • FYI, what you've created as useFetch is a perfectly natural pattern and would be called a custom hook. That documentation also describes why @Zachary's answer is correct. Commented Nov 5, 2020 at 3:23

1 Answer 1

4

You have to use the useFetch hook in the main render function. You can't use it inside of another function. You'll need to adjust your useFetch to work separately.

Here's an example of how to do that. In this case, I'm making the useFetch refetch when the url or options change

helper.js

import { useState, useEffect } from 'react';

export const GH_BASE_URL = 'https://api.github.com/';

export const useFetch = (url, options) => {
  const [response, setResponse] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    // Set up aborting
    const controller = new AbortController();
    const signal = controller.signal;
    const fetchData = async () => {
      setIsLoading(true);

      try {
        const res = await fetch(url, { ...options, signal });
        const json = await res.json();
        setResponse(json);
        setIsLoading(false);
      } catch (error) {
        // AbortError means that the fetch was cancelled, so no need to set error
        if (error.name !== 'AbortError') {
          setError(error);
        }
      }
    };

    if (url) {
      fetchData();
    }
    // Clear up the fetch by aborting the old one
    // That way there's no race condition issues here
    return () => {
      controller.abort();
    };
    // url and options need to be in the effect's dependency array
  }, [url, options]);

  return { response, error, isLoading };
};

Header.js

import React, { useState } from 'react';
import { useFetch, GH_BASE_URL } from '../helpers';

const REPO_SEARCH_URL = `${GH_BASE_URL}/search/repositories?q=`;

function Header(props) {
  const [searchValue, setSearchValue] = useState('');

  
  function handleChange(event) {
    this.setState({ searchValue: event.target.value });
  }

  // Instead of using the search directly, wait for submission to set it
  const [searchDebounce,setSearchDebounce] = useState('');
  
  async function handleSearch(event) {
    event.preventDefault();
    setSearchDebounce(searchValue);
  }
  // If you want to include the options, you should memoize it, otherwise the fetch will re-run on every render and it'll cause an infinite loop.
  // This will refetch everytime searchDebounce is changed
  const { response, error, isLoading } = useFetch(
    searchDebounce?`${REPO_SEARCH_URL}${searchDebounce}`:''
  );

  return (
    <header>
      <form onSubmit={handleSearch} className="container">
        <input
          value={searchValue}
          onChange={handleChange}
          className="search-input"
          placeholder="Search a repository"
        ></input>
      </form>
    </header>
  );
}

export default Header;

If you want to run a function whenever the response changes, you could use an effect:

  useEffect(() => {
    if (error || isLoading) {
      return;
    }
    // Destructure this to prevent a deps issue on the hooks eslint config
    const { responseChanged } = props;
    responseChanged(response);
  }, [response, isLoading, error, props.responseChanged]);

Sign up to request clarification or add additional context in comments.

2 Comments

That's brilliant! How can I use the response data to call a props method? I am passing a function down to Header to update repos state in App but response is null...
@LazioTibijczyk, I added a way to do that at the end of the post, as is, you should make sure props.responseChanged (or whatever you call it) will useCallback so this doesn't run more often than you want. Or put the props.responseChanged into a ref and use that in the effect instead

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.