reactjsreact-hooks

UseMemo still re-rendering component


The following is a simplification of a component I've written. It has two tabs, and different content is displayed depending on which tab is selected (done by a button not included here). There are two child components, a SimpleContent object that just displays static content, and a LoadedContent component that has to go away, make a call from a server to fetch data, and then render itself. What I would like to do is only make that server call once, so I don't want to re-render LoadedContent each time I select that tab. I want it to render the LoadedContent component once, even when I click to tab 1 and then back to tab 2.

I thought by using UseMemo to wrap the LoadedContent component that would work, but it doesn't - each time I click into tab 2, it reloads the content. How do I fix this please?

import React, { useState, useMemo } from 'react';
import LoadedContent from './loadedContent';
import SimpleContent from './simpleContent';

const TabsPage = (props) => {

    const [tab, setTab] = useState("tab1");
    
    const tab2Display = useMemo(() => {
        return (
            <LoadedContent  />
        )
    }, [])

    return (
    
        <div>
            {tab === "tab1" ? 
                <SimpleContent />
            : 
                {tab2Display}
        </div>
    )
}

export default TabsPage;

Solution

  • To make sure that the LoadedContent component fetches data only once, you should move the loading logic into the parent component and store the loaded data in the state. Using useMemo alone won't prevent re-rendering because it recalculates on every render. Here's how you can implement this:

    Create a state to hold the loaded content data. Then use a useEffect call to fetch data when the component mounts, and pass the data into the LoadedContent component as a prop. Here's an updated version of your TabsPage component:

    import React, { useState, useEffect } from 'react';
    import LoadedContent from './loadedContent';
    import SimpleContent from './simpleContent';
    
    const TabsPage = (props) => {
        const [tab, setTab] = useState("tab1");
        const [loadedData, setLoadedData] = useState(null);
    
        useEffect(() => {
            const fetchData = async () => {
                try {
                    const response = await fetch('YOUR_API_ENDPOINT'); // Replace with your API endpoint
                    const data = await response.json();
                    setLoadedData(data);
                } catch (error) {
                    console.error('Error fetching data:', error);
                }
            };
    
            // Fetch data only once when the component mounts
            if (!loadedData) {
                fetchData();
            }
        }, [loadedData]);
    
        return (
            <div>
                <button onClick={() => setTab("tab1")}>Tab 1</button>
                <button onClick={() => setTab("tab2")}>Tab 2</button>
    
                {tab === "tab1" ? 
                    <SimpleContent />
                    : 
                    loadedData ? <LoadedContent data={loadedData} /> : <p>Loading...</p>
                }
            </div>
        );
    }
    
    export default TabsPage;
    

    An even better solution, in my opinion, but something that would take more refactoring, is to use react-query. They have a useQuery hook that can fetch data from an endpoint, and then caches the result, so you can be very explicit with when you want to refetch the data. Here are the docs if you're interested.

    In this case, it might look something like:

    const fetchData = async () => {
        try {
            const response = await fetch('YOUR_API_ENDPOINT'); // Replace with your API endpoint
            const data = await response.json();
            return data;
        } catch (error) {
            console.error('Error fetching data:', error);
        }
    };
    
    const { data } = useQuery({
        queryKey: ['content'],
        queryFn: fetchData,
        refetchOnMount: false // just to demonstrate
    })