skip to Main Content

How do I deal with a streamed response ?

using the function approach of setState with functions doesn’t seem to work:
the hooks are not executed in the correct order and the response is unordered

    const response = await fetch('/dostuff', {
      method:'POST',
      body
    })

    const reader = response.body!.getReader();
    const chunks = [];
    
    let done, value;
    while (!done) {
      ({ value, done } = await reader.read());
      if (done) {
        return chunks;
      }
      const strval = new TextDecoder().decode(value)
      chunks.push(strval);
      await setResponseText((prev)=> {
        return prev + strval
      })

      // console.log("EEE ",strval);          
    }

2

Answers


  1. Chosen as BEST ANSWER

    In the end the issue was in two different points:

    • decoder in the while loop
    • react strict mode enabled and useEffect interaction.

    Decoder

    the decoder needed to be outside of the loop because the stream response could end in the middle of a multibyte character. creating a new decoder at each iteration does not allow to manage this case.

    Strict mode

    I honestly did not think that the useEffect hook was somehow involved in this error so I limited the code in the original question, my bad.

    What happens is that in development mode, react strict mode causes the render and mounting / unmounting of components twice.

    This creates a racing condition because the first render's streamed response was still active and receiving data when the second render's streamed response started.

    The state that contains the response text is therefore updated sometimes with a first stream data chunk, sometime with a second stream chunk.

    The solution was therefore to add an AbortController in the useEffect call.

    useEffect( ()=> {
      const controller = new AbortController();
      const run = async (controller:AbortController) => {   
    
        let response
        try {
    
          response = await fetch('/doStuff', {
            method:'POST',
            body
          })
          
        } catch (error) {
          if (error.name === "AbortError") {
            return;
          }
          ...
        }
    
        const reader = response.body!.getReader();
        const chunks = [];
        
        let done, value;
        const dec = new TextDecoder()
    
        while (!done) {
          ({ value, done } = await reader.read());
          if (done) {
            return chunks;
          }
          const strval = dec.decode(value, { stream: true })
          chunks.push(strval);
          text.current += strval
          setResponseText(text.current)      
        }
    
    
      }
      run(controller)
    
      return () => controller.abort();
      
    },[])
    

  2. The useEffect hook is used to initiate the fetching process, and the responseText state is updated once at the end of the loop. This should help avoid potential issues with asynchronous state updates. You can add any dependencies that should trigger the effect to run again when they change.

      useEffect(() => {
        const fetchData = async () => {
          const response = await fetch('/dostuff', {
            method: 'POST',
            body,
          });
    
          const reader = response.body.getReader();
          let chunks = '';
    
          while (true) {
            const { value, done } = await reader.read();
    
            if (done) {
              break;
            }
    
            const strVal = new TextDecoder().decode(value);
            chunks += strVal;
          }
    
          setResponseText(chunks);
        };
    
        fetchData();
      }, []);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search