use SWR data in useState Hook

Viewed 2819
  const fetcher = (url: string) => fetch(url).then((r) => r.json());

  const { data, error } = useSWR(
    "https://some.com/api",
    fetcher,
  );

is there any way to add data in a useState hook like this

  const fetcher = (url: string) => fetch(url).then((r) => r.json());

  const { data, error } = useSWR(
    "https://meme-api.herokuapp.com/gimme/5",
    fetcher,
  );

const [memes,setMemes]=useState(data);

cause I want to concat the data at some point for inifnite scrolling

2 Answers

Since https://meme-api.herokuapp.com/gimme/5 always returns new data for each call, useSWR isn't a good fit for this and, moreover, the fact it retrieves from cache and gives that to your code and then revalidates and (possibly) calls your code to update, without telling you whether it's the first result or an update, makes it very hard to do what you're describing.

Instead, I'd just use fetch directly and not try to do the SWR thing; see comments:

// Start with no memes
const [memes,setMemes] = useState([]);

// Use a ref to track an `AbortController` so we can:
// A) avoid overlapping fetches, and
// B) abort the current `fetch` operation (if any) on unmount
const fetchControllerRef = useRef(null);

// A function to fetch memes
const fetchMoreMemes = () => {
    if (!fetchControllerRef.current) {
        fetchControllerRef.current = new AbortController();
        fetch("https://meme-api.herokuapp.com/gimme/5", {signal: fetchControllerRef.current.signal})
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error ${response.status}`);
            }
            return response.json();
        })
        .then(newMemes => {
            setMemes(memes => memes.concat(newMemes.memes));
        })
        .catch(error => {
            // ...handle/report error...
        })
        .finally(() => {
            fetchControllerRef.current = null;
        });
    }
};

// Fetch the first batch of memes
useEffect(() => {
    fetchMoreMemes();
    return () => {
        // Cancel the current `fetch` (if any) when the component is unmounted
        fetchControllerRef.current?.abort();
    };
}, []);

When you want to fetch more memes, call fetchMoreMemes.

Live Example:

const {useState, useEffect, useRef} = React;

const Example = () => {
    // Start with no memes
    const [memes,setMemes] = useState([]);

    // Use a ref to track an `AbortController` so we can:
    // A) avoid overlapping fetches, and
    // B) abort the current `fetch` operation (if any) on unmount
    const fetchControllerRef = useRef(null);

    // A function to fetch memes
    const fetchMoreMemes = () => {
        if (!fetchControllerRef.current) {
            fetchControllerRef.current = new AbortController();
            fetch("https://meme-api.herokuapp.com/gimme/5", {signal: fetchControllerRef.current.signal})
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP error ${response.status}`);
                }
                return response.json();
            })
            .then(newMemes => {
                // I'm filtering out NSFW ones here on SO
                setMemes(memes => memes.concat(newMemes.memes.filter(({nsfw}) => !nsfw)));
            })
            .catch(error => {
                // ...handle/report error...
            })
            .finally(() => {
                fetchControllerRef.current = null;
            });
        }
    };

    // Fetch the first batch of memes
    useEffect(() => {
        fetchMoreMemes();
        return () => {
            // Cancel the current `fetch` (if any) when the component is unmounted
            fetchControllerRef.current && fetchControllerRef.current.abort();
        };
    }, []);
    
    const message = memes.length === 1 ? "1 meme:" : `${memes.length} memes:`;
    return <div>
        <div>{message} <input type="button" value="More" onClick={fetchMoreMemes}/></div>
        <ul>
            {/* `index` as key is ONLY valid because our array only grows */}
            {memes.map(({postLink}, index) => <li key={index}>{postLink}</li>)}
        </ul>
    </div>
};


ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

The fastest solution to transfer data from one variable to another is to use an useEffect hook. When data changes, update memes.

useEffect(() => { setMemes(data); }, [data])

Infinite scroling

A better solution would be to use SWR provided solutions for infinite scrolling. You have different options documented here.

Plain fetch

In this case, you can also consider using directly the fetch function and appending data to the memes list directly:

const [ memes, setMemes ] = useState([]);

async function fetchAnotherPage() {
    const data = (await fetch('https://meme-api.herokuapp.com/gimme/5')).json();
    setMemes(value => [...value, ...data.memes]);
}

useEffect(() => fetchAnotherPage(), []);
Related