Search code examples
reactjsasync-awaitreact-hooksgeneratoruse-effect

Asynchronous Generators in UseEffect - how to unsubscribe from updates


I've been playing with using async generators in Typescript React apps.

I created the following component:

import React, {useEffect, useState} from 'react'

const delay = (ms : number) => new Promise(resolve => setTimeout(resolve, ms))

const fetchData = async (count : number, url: string) => { 
    await delay(1000);
    const title : string = await fetch(`${url}/${count}`).then(response => response.json()).then(data => data.title)
    return title; 
}

const asyncGenerator = async function* () { 
    let count : number = 1;
    const url : string = 'http://jsonplaceholder.typicode.com/posts';
    while(true) { 
        yield fetchData(count, url)
        count++;
    }

}

const UseEffectComponent = () => { 
    const [titles, setTitles] = useState<string[]>([])

 
    useEffect(() => { 
        (async () => { 
            for await (const title of asyncGenerator()) { 
                setTitles(t => [...t, title])
            }
        })();
        
        
    }, [])

    return (
        <div>
            <div>
                {titles.map(t => <p>{t}</p>)}
            </div>
        </div>
    )
}

export default UseEffectComponent;

Essentially, I've created a stream of post titles, which are supplied by the asyncGenerator function, which will fetch data from the placeholder API every second by calling fetchData which uses delay to introduce an artificial 1000ms delay.

I'm curious what is the best way to read this data from the stream in the component itself. My current implementation using useEffect:

useEffect(() => { 
    (async () => { 
        for await (const title of asyncGenerator()) { 
            setTitles(t => [...t, title])
        }
    })();

has no cleanup code - meaning this stream will continue to supply data even when the component is unmounted. I'm not sure what the best way to fix this would be though. Can I exit this for of loop when the component unmounts? Or is there a better way to consume data from my stream?


Solution

  • You can add isMounted to check component is unmounted or not.

    useEffect(() => { 
      let isMounted = true;
      (async () => {
          for await (const title of asyncGenerator()) { 
            if(!isMounted){
              break;
            }
            setTitles(t => [...t, title])
          }
      })();  
    
      return () => {
        isMounted = false;
      };
    }, [])