javascriptreactjsuse-effectusecallback

Data Fetching Using useEffect() And useCallback In React


Issue

I'm looking for the most optimal way to fetch data using useEffect() when the fetch function is used in more than one place.

Situation

Currently, I have a parent component (ItemContainer) and a child component (SearchBar). ItemContainer should fetch the all the possible list of items using getItemList() functions. I'm executing this function within the useEffect() during the first render, and also passing it down to SearchBar component, so that when a user submits a search term, it will update itemList state by triggering getItemList() in ItemContainer.

This actually works just as I expected. However, my issue is that

  1. I'm not really sure whether it is okay to define getItemList() outside the useEffect() in this kind of situation. From what I've been reading (blog posts, react official docs) it is generally recommended that data fetching function should be defined inside the useEffect(), although there could be some edge cases. I'm wondering if my case applies as this edge cases.
  2. Is it okay to leave the dependency array empty in useCallback? I tried filling it out using searchTerm, itemList, but none of them worked - and I'm quite confused why this is so.

I feel bad that I don't fully understand the code that I wrote. I would appreciate if any of you could enlighten me with what I'm missing here...

ItemContainer

    const ItemContainer = () => {
        const [itemList, setItemList] = useState([]);
    
        const getItemList = useCallback( async (searchTerm) => {
            const itemListRes = await Api.getItems(searchTerm);
            setItemList(itemListRes)
        }, []);
    
        useEffect(() => {
            getItemList();
        }, [getItemList]);
    
        return (
            <main>
                <SearchBar search={getItemList} />
                <ItemList itemList={itemList} />
            </main>
        )
    }

SearchBar

const SearchBar = ({ search })  => {
    const [searchTerm, setSearchTerm] = useState('');

    const handleSubmit = (e) => {
        e.preventDefault();
        search(searchTerm);
        setSearchTerm('');
    }

    const handleChange = (e) => {
        setSearchTerm(e.target.value)
    }

    return (
        <form onSubmit={handleSubmit}>
            <input 
                placeholder='Enter search term...'
                value={searchTerm}
                onChange={handleChange}
            />
            <button>Search</button>
        </form>
    )
}

Solution

  • Here are my answers.

    1. Yes, it is okay. What's inside useCallback is "frozen" respect to the many ItemConteiner function calls that may happen. Since the useCallback content accesses only setItemList, which is also a frozen handler, there'll be no problems.

    2. That's also correct, because an empty array means "dependent to nothing". In other words, the callback is created once and keeps frozen for all the life of the ItemContainer.

    Instead, this is something weird:

        useEffect(() => {
            getItemList();
        }, [getItemList]);
    

    It works, but it has a very little sense. The getItemList is created once only, so why make an useEffect depending to something never changes?

    Make it simpler, by running once only:

        useEffect(() => {
            getItemList();
        }, []);