Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Helper function using hooks inside a functional component

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;
like image 529
LazioTibijczyk Avatar asked Oct 15 '25 20:10

LazioTibijczyk


1 Answers

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

like image 74
Zachary Haber Avatar answered Oct 17 '25 10:10

Zachary Haber