Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

use-async-effect2

DigitalBrainJS194MIT0.12.2TypeScript support: included

Asynchronous versions of the useEffect anduseCallback hooks that able to cancel internal code by user requests or component unmounting

react, reactjs, hook, hooks, useEffect, useCallback, useAsyncEffect, use-async-effect, async, deepstate, setState, promise, c-promise, cpromise, cancelable, cancellable, p-cancelable, timeout, progress, cancel, abortable, abort, AbortController, AbortSignal, signal, await, wait, promises, generator, co, yield, reject, race, decorator, delay, break, suspending, bluebird, deferred, react, cancellation, aborting, close, closable, pause, task

readme

Build Status npm npm bundle size David Stars

useAsyncEffect2 :snowflake:

This library provides an async belt for the React components as:

  • useAsyncEffect - deeply cancellable asynchronous effects that can be cleared (canceled) on component unmounting, timeout, or by user request.
  • useAsyncCallback - cancellable async callbacks
  • useAsyncDeepState - to define a deep state, the actual values of which can be accessed from an async routine
  • useAsyncWatcher - to watch for state updates in a promise flow

The library is designed to make it as easy as possible to use complex and composite asynchronous routines in React components. It works on top of a custom cancellable promise, simplifying the solution to many common challenges with asynchronous tasks. Can be composed with cancellable version of Axios (cp-axios) and fetch API (cp-fetch) to get auto cancellable React async effects/callbacks with network requests.

Quick start

  1. You have to use the generator syntax instead of ECMA async functions, basically by replacing await with yield and async()=>{} or async function() with function*:

     // plain React effect using `useEffect` hook
     useEffect(()=>{
       const doSomething = async()=>{
         await somePromiseHandle;
         setStateVar('foo');
       };
       doSomething();
     }, [])
     // auto-cleanable React async effect using `useAsyncEffect` hook
     useAsyncEffect(function*(){
       yield somePromiseHandle;
       setStateVar('foo');
     }, [])
  2. It's recommended to use CPromise instead of the native Promise to make the promise chain deeply cancellable, at least if you're going to change the component state inside it.

     import { CPromise } from "c-promise2";
    
     const MyComponent= ()=>{
         const [text, setText]= useState('');
    
         useAsyncEffect(function*(){
           yield CPromise.delay(1000);
           setText('Hello!');
         });
     }
  3. Don't catch (or just rethrow caught) CanceledError errors with E_REASON_UNMOUNTED reason inside your code before making any stage change:

     import {
       useAsyncEffect,
       E_REASON_UNMOUNTED,
       CanceledError
     } from "use-async-effect2";
     import cpAxios from "cp-axios";
    
     const MyComponent= ()=>{
         const [text, setText]= useState('');
    
         useAsyncEffect(function*(){
           try{
               const json= (yield cpAxios('http://localhost/')).data;
               setText(`Data: ${JSON.stringify(json)}`);
           }catch(err){
               // just rethrow the CanceledError error if it has E_REASON_UNMOUNTED reason
               CanceledError.rethrow(err, E_REASON_UNMOUNTED);
               // otherwise work with it somehow
               setText(`Failed: ${err.toString}`);
           } 
         });
     }

Installation :hammer:

  • Install for node.js using npm/yarn:
$ npm install use-async-effect2
$ yarn add use-async-effect2

Why

Every asynchronous procedure in your component that changes its state must properly handle the unmount event and stop execution in some way before attempting to change the state of the unmounted component, otherwise you will get the well-known React leakage warning:

Warning: Can't perform a React state update on an unmounted component. 
This is an no-op, but it indicates a memory leak in your application. 
To fix, cancel all subscriptions and asynchronous task in "a useEffect cleanup function".

It uses c-promise2 to make it work. When used in conjunction with other libraries from CPromise ecosystem, such as cp-fetch and cp-axios, you get a powerful tool for building asynchronous logic of React components.

Examples

useAsyncEffect

A tiny useAsyncEffect demo with JSON fetching using internal states:

Live demo to play

function JSONViewer({ url, timeout }) {
  const [cancel, done, result, err] = useAsyncEffect(function* () {
      return (yield cpAxios(url).timeout(timeout)).data;
    }, { states: true });

  return (
    <div>
      {done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
      <button className="btn btn-warning" onClick={cancel} disabled={done}>
        Cancel async effect (abort request)
      </button>
    </div>
  );
}

Another demo

import React from "react";
import {useState} from "react";
import {useAsyncEffect} from "use-async-effect2";
import cpFetch from "cp-fetch";

function JSONViewer(props) {
    const [text, setText] = useState("");

    useAsyncEffect(function* () {
            setText("fetching..."); 
            const response = yield cpFetch(props.url); // will throw a CanceledError if component get unmounted
            const json = yield response.json();
            setText(`Success: ${JSON.stringify(json)}`);
    }, [props.url]);

    return <div>{text}</div>;
}

Notice: the related network request will be aborted, when unmounting.

An example with a timeout & error handling (Live demo):

import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED, CanceledError} from "use-async-effect2";
import cpFetch from "cp-fetch";

export default function TestComponent(props) {
  const [text, setText] = useState("");
  const [isPending, setIsPending] = useState(true);

  const cancel = useAsyncEffect(
    function* ({ onCancel }) {
      console.log("mount");

      this.timeout(props.timeout);

      onCancel(() => console.log("scope canceled"));

      try {
        setText("fetching...");
        const response = yield cpFetch(props.url);
        const json = yield response.json();
        setIsPending(false);
        setText(`Success: ${JSON.stringify(json)}`);
      } catch (err) {
        CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough for UNMOUNTED rejection
        setIsPending(false);
        setText(`Failed: ${err}`);
      }

      return () => {
        console.log("unmount");
      };
    },
    [props.url]
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>{text}</div>
      <button onClick={cancel} disabled={!isPending}>
        Cancel request
      </button>
    </div>
  );
}

useAsyncCallback

Here's a Demo App to play with asyncCallback and learn about its options.

Live search for character from the rickandmorty universe using rickandmortyapi.com:

Live demo

import React, { useState } from "react";
import {
  useAsyncCallback,
  E_REASON_UNMOUNTED,
  CanceledError
} from "use-async-effect2";
import { CPromise } from "c-promise2";
import cpAxios from "cp-axios";

export default function TestComponent(props) {
  const [text, setText] = useState("");

  const handleSearch = useAsyncCallback(
    function* (event) {
      const { value } = event.target;
      if (value.length < 3) return;
      yield CPromise.delay(1000);
      setText("searching...");
      try {
        const response = yield cpAxios(
          `https://rickandmortyapi.com/api/character/?name=${value}`
        ).timeout(props.timeout);
        setText(response.data?.results?.map(({ name }) => name).join(","));
      } catch (err) {
        CanceledError.rethrow(err, E_REASON_UNMOUNTED);
        setText(err.response?.status === 404 ? "Not found" : err.toString());
      }
    },
    { cancelPrevious: true }
  );

  return (
    <div className="component">
      <div className="caption">
        useAsyncCallback demo: Rickandmorty universe character search
      </div>
      Character name: <input onChange={handleSearch}></input>
      <div>{text}</div>
      <button className="btn btn-warning" onClick={handleSearch.cancel}>
        Cancel request
      </button>
    </div>
  );
}

This code handles the cancellation of the previous search sequence (including aborting the request) and canceling the sequence when the component is unmounted to avoid the React leak warning.

useAsyncCallback example: fetch with progress capturing & cancellation (Live demo):

import React, { useState } from "react";
import { useAsyncCallback, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CPromise, CanceledError } from "c-promise2";
import cpAxios from "cp-axios";
import { ProgressBar } from "react-bootstrap";

export default function TestComponent(props) {
  const [text, setText] = useState("");
  const [progress, setProgress] = useState(0);
  const [isFetching, setIsFetching] = useState(false);

  const fetchUrl = useAsyncCallback(
    function* (options) {
      try {
        setIsFetching(true);
        this.innerWeight(3); // for progress calculation
        this.progress(setProgress);
        setText("fetching...");
        const response = yield cpAxios(options).timeout(props.timeout);
        yield CPromise.delay(500); // just for fun
        yield CPromise.delay(500); // just for fun
        setText(JSON.stringify(response.data));
        setIsFetching(false);
      } catch (err) {
        CanceledError.rethrow(err, E_REASON_UNMOUNTED);
        setText(err.toString());
        setIsFetching(false);
      }
    },
    [props.url]
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>{isFetching ? <ProgressBar now={progress * 100} /> : text}</div>
      {!isFetching ? (
        <button
          className="btn btn-success"
          onClick={() => fetchUrl(props.url)}
          disabled={isFetching}
        >
          Fetch data
        </button>
      ) : (
        <button
          className="btn btn-warning"
          onClick={() => fetchUrl.cancel()}
          disabled={!isFetching}
        >
          Cancel request
        </button>
      )}
    </div>
  );
}

useAsyncDeepState

An enhancement of the useState hook for use inside async routines. It defines a deep state abd works very similar to the React setState class method. The hook returns a promise that will be fulfilled with an array of newState and oldState values after the state has changed.

export default function TestComponent(props) {

  const [state, setState] = useAsyncDeepState({
    foo: 123,
    bar: 456,
    counter: 0
  });

  return (
    <div className="component">
      <div className="caption">useAsyncDeepState demo:</div>
      <div>{state.counter}</div>
      <button onClick={async()=>{
        const newState= await setState((state)=> {
          return {counter: state.counter + 1}
        });

        console.log(`Updated: ${newState.counter}`);
      }}>Inc</button>
      <button onClick={()=>setState({
        counter: state.counter
      })}>Set the same state value</button>
    </div>
  );
}

useAsyncWatcher

This hook is a promisified abstraction on top of the useEffect hook. The hook returns the watcher function that resolves its promise when one of the watched dependencies have changed.

export default function TestComponent7(props) {
  const [value, setValue] = useState(0);

  const [fn, cancel, pending, done, result, err] = useAsyncCallback(function* () {
    console.log('inside callback the value is:', value);
    return (yield cpAxios(`https://rickandmortyapi.com/api/character/${value}`)).data;
  }, {states: true, deps: [value]})

  const callbackWatcher = useAsyncWatcher(fn);

  return (
    <div className="component">
      <div className="caption">useAsyncWatcher demo:</div>
      <div>{pending ? "loading..." : (done ? err ? err.toString() : JSON.stringify(result, null, 2) : "")}</div>
      <input value={value} type="number" onChange={async ({target}) => {
        setValue(target.value * 1);
        const [fn]= await callbackWatcher();
        await fn();
      }}/>
      {<button onClick={cancel} disabled={!pending}>Cancel async effect</button>}
    </div>
  );
}

To learn more about available features, see the c-promise2 documentation.

Wiki

See the Project Wiki to get the most exhaustive guide.

Playground

To get it, clone the repository and run npm run playground in the project directory or just use the codesandbox demo to play with the library online.

API

useAsyncEffect(generatorFn, deps?: []): (cancel():boolean)

useAsyncEffect(generatorFn, options?: object): (cancel():boolean)

A React hook based on useEffect, that resolves passed generator as asynchronous function. The asynchronous generator sequence and its promise of the result will be canceled if the effect cleanup process started before it completes. The generator can return a cleanup function similar to the useEffect hook.

  • generatorFn(scope: CPromise) : GeneratorFunction - generator to resolve as an async function. Generator context (this) refers to the CPromise instance.
  • deps?: any[] | UseAsyncEffectOptions - effect dependencies

UseAsyncEffectOptions:

  • options.deps?: any[] - effect dependencies
  • options.skipFirst?: boolean - skip first render
  • options.states: boolean= false - use states
  • options.once: boolean= false - run the effect only once (the effect's async routine should be fully completed)

Available states vars:

  • done: boolean - the function execution is completed (with success or failure)
  • result: any - refers to the resolved function result
  • error: object - refers to the error object. This var is always set when an error occurs.
  • canceled:boolean - is set to true if the function has been failed with a CanceledError.

All these vars defined on the returned cancelFn function and can be alternative reached through the iterator interface in the following order:

const [cancelFn, done, result, error, canceled]= useAsyncEffect(/*code*/);

useAsyncCallback(generatorFn, deps?: []): CPromiseAsyncFunction

useAsyncCallback(generatorFn, options?: object): CPromiseAsyncFunction

This hook makes an async callback that can be automatically canceled on unmount or by user request.

  • generatorFn([scope: CPromise], ...userArguments) : GeneratorFunction - generator to resolve as an async function. Generator context (this) and the first argument (if options.scopeArg is set) refer to the CPromise instance.
  • deps?: any[] | UseAsyncCallbackOptions - effect dependencies

    UseAsyncCallbackOptions:

  • deps: any[] - effect dependencies
  • combine:boolean - subscribe to the result of the async function already running with the same arguments instead of running a new one.
  • cancelPrevious:boolean - cancel the previous pending async function before running a new one.
  • threads: number=0 - set concurrency limit for simultaneous calls. 0 means unlimited.
  • queueSize: number=0 - set max queue size.
  • scopeArg: boolean=false - pass CPromise scope to the generator function as the first argument.
  • states: boolean=false - enable state changing. The function must be single threaded to use the states.

Available state vars:

  • pending: boolean - the function is in the pending state
  • done: boolean - the function execution completed (with success or failure)
  • result: any - refers to the resolved function result
  • error: object - refers to the error object. This var always set when an error occurs.
  • canceled:boolean - is set to true if the function has been failed with a CanceledError.

All these vars defined on the decorated function and can be alternative reached through the iterator interface in the following order:

const [decoratedFn, cancel, pending, done, result, error, canceled]= useAsyncCallback(/*code*/);

useAsyncDeepState([initialValue?: object]): ([value: any, accessor: function])

arguments

  • initialValue

    returns

    Iterable of:
  • value: object - current state value
  • accessor:(newValue)=>Promise<rawStateValue:any> - promisified setter function that can be used as a getter if called without arguments

useAsyncWatcher([...valuesToWatch]): watcherFn

arguments

  • ...valuesToWatch: any - any values to watch that will be passed to the internal effect hook

    returns

  • watcherFn: ([grabPrevValue= false]): Promise<[newValue, [prevValue]]> - if the hook is watching one value
  • watcherFn: ([grabPrevValue= false]): Promise<[...[newValue, [prevValue]]]> - if the hook is watching multiple values
  • c-promise2 - promise with cancellation, decorators, timeouts, progress capturing, pause and user signals support
  • cp-axios - a simple axios wrapper that provides an advanced cancellation api
  • cp-fetch - fetch with timeouts and request cancellation
  • cp-koa - koa with middlewares cancellation

License

The MIT License Copyright (c) 2021 Dmitriy Mozgovoy robotshara@gmail.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

changelog

Changelog

All notable changes to this project will be documented in this file. Dates are displayed in UTC.

Generated by auto-changelog.

v0.12.2

  • Expose cancel method for useAsyncEffect hook; 662dfdc

v0.12.1

24 September 2021

v0.12.0

19 September 2021

  • Refactored useDeepState hook; eb83466

v0.11.5

14 July 2021

  • Added symbol props support for state created by useAsyncDeepState hook; c71507d

v0.11.4

1 July 2021

  • Refactored & optimized useAsyncCallback hook; 20c2802

v0.11.3

27 May 2021

  • Fixed .npmignore due to missing module typings; 9d48443

v0.11.2

23 May 2021

  • Added the ability for useAsyncWatcher and useAsyncDeepState to unsubscribe from state updates; 0915b17

v0.11.1

22 May 2021

  • Fixed bug with affected initial state object of the useAsyncDeepState hook; 2fde232

v0.11.0

22 May 2021

  • Refactored useAsyncState to useAsyncDeepState; 11eb17f

v0.10.0

17 May 2021

  • Added useAsyncState and useAsyncWatcher hooks; 861b2a4
  • Updated typings; 45cb882

v0.9.4

15 May 2021

  • Refactored useAsyncEffect hook; b62ea48

v0.9.3

14 May 2021

  • Refactored the internal finalize logic of the useAsyncCallback hook; fe20f5f

v0.9.2

13 May 2021

  • Added catchErrors option for the useAsyncCallbackHook; 377bc1a
  • Refactored isGeneratorFn to avoid using function name; 390fdd6

v0.9.1

12 May 2021

v0.9.0

9 May 2021

  • Improved useAsyncCallback queue logic; aa6e76b

v0.8.0

7 May 2021

v0.7.1

5 May 2021

v0.7.0

3 May 2021

  • Added internal states support; 1b7a703

v0.6.0

27 April 2021

  • Updated c-promise2 to v0.12.1; 9a1863e

v0.5.0

13 April 2021

  • Added scopeArg option for useAsyncCallback hook; af1ff83
  • Updated c-promise2 to v0.11.2; a5624bd
  • Fixed build status badge; 190daa2
  • Fixed build status badge - use travis-ci.com instead .org; ed67075

v0.4.0

11 January 2021

  • Added queueSize option for useAsyncEffect; 0436e46

v0.3.0

7 January 2021

  • Fixed bug with useAsyncEffect user cancellation; 3ccd038

v0.2.1

6 January 2021

  • Fixed demo links in the README.md; 1cb5f11
  • Added typings config to the package.json; 481afc8

v0.2.0

6 January 2021

v0.1.2

5 January 2021

  • Added console.error for catch handler; df9b78f

v0.1.1

5 January 2021

  • Refactored package.json; 3a7253a
  • Added prepublishOnly & postversion scripts; 40ebb4d
  • Renamed package; a0d6894

v0.1.0

5 January 2021