npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-async-effect-state

v0.3.0

Published

Encapsulate using states from async request in React.

Downloads

3

Readme

React Async Effect State

Encapsulate setting states from async request in React. Also, have some scope creep which includes debouncing logic and manual trigger.

Usually on a React component that need to get data from an async call (eg: API call), the call is requested in a useEffect block, which then set some state on various lifecycle of the call. Including some error handling, this would look like this:

const [requestState, setRequestState] = useState(["loading", null, null]);
const [status, response, error] = requestState;

useEffect(() => {
    fetch('http://example.com')
        .then((data) => {
            setRequestState(["done", data, null]);
        })
        .catch((error) => {
            setRequestState(["error", null, error]);
        })
}, [])

if (status === "loading") {
    return (<p>Loading data...</p>);
}
if (status === "error") {
    return (<p>An error occured {error.toString()}</p>);
}

return (<p>{response}</p>);

This library reduce this to:

import { useAsyncEffectState } from 'react-async-effect-state';

const [status, response, error] = useAsyncEffectState(
    () => fetch('http://example.com'), []);

if (status === AsyncState.LOADING) {
    return (<p>Loading data...</p>);
}
if (status === AsyncState.ERROR) {
    return (<p>An error occured {error.toString()}</p>);
}

return (<p>{response}</p>);

or if you prefer:

import { useAsyncEffectState, asyncUIBlock } from 'react-async-effect-state';

const responseAsync = useAsyncEffectState(
    () => fetch('http://example.com'), []);

return asyncUIBlock(responseAsync,
    (response) => (<p>{response}</p>),
    (error) => (<p>An error occured {error.toString()}</p>),
    () => (<p>Loading data...</p>)
);

Debounce

Occasionally, you'll encounter a situation where you need a search box and you don't want the call to be triggered on every key press. By default this library only queue one call, and only one call is running at a time. Therefore, new request for the key will be triggered only after previous call complete. But you can also specify a debounce duration so that a call will not start until after some delay and no other new call is queued (user stopped entering key). For example:

import { useAsyncEffectState } from 'react-async-effect-state';

const [query, setQuery] = useState('');
const [status, response, error] = useAsyncEffectState(
    () => fetch('http://example.com&q=' + query),
    [query],
    {
        debounceDelayMs: 300   
    });

if (status === AsyncState.LOADING) {
    return (<p>Loading data...</p>);
}
if (status === AsyncState.ERROR) {
    return (<p>An error occured {error.toString()}</p>);
}

return (<>
    <input value={query} onChange={(e) => setQuery(e.target.value)} />
    <p>{response}</p>
</>);

Manual trigger

If you need to trigger the call manually, you can use a different variant, useManualAsyncState which returns a trigger and reset method.

import { useAsyncEffectState } from 'react-async-effect-state';

const [asyncState, trigger, reset] = useManualAsyncstate(
    () => fetch('http://example.com'));
const [status, response, error] = asyncState;

if (status === AsyncState.PENDING) {
    return (<>
        <p>No call yet</p>
        <button onClick={trigger}>Start</button>
    </>);
}
if (status === AsyncState.LOADING) {
    return (<p>Loading data...</p>);
}
if (status === AsyncState.ERROR) {
    return (<p>An error occured {error.toString()}</p>);
}

return (<>
    <p>{response}</p>
    <button onClick={reset}>Reset to pending</button>
</>);

Usage

useAsyncEffectState<T>(closure: () => Promise<T>, dependencyList: DependencyList, options: Options) => AsyncEffectState<T>

Encapsulate setting states from async request. The third parameter is an option object that can alter some behaviour. Returns a tuple of type [status,response,error] which is the current state of the request.

export interface Options {
    /**
     * By default on subsequent async call, the state will switch back to loading state. Set to true
     * to disable this and skip directly to final state then the async call resolve.
     */
    noLoadingOnReload?: boolean;

    /**
     * By default on subsequent useEffect closure call, (it's dependency was updated so it was
     * called), when previous async call was not completed, new call will only gets executed after
     * previous async call completed. Any repeated calls will be removed, meaning only one final
     * call will get executed. This is done to reduce the number of async call, which usually invoke
     * some APIs on response to some user input.
     */
    disableRequestDedup?: boolean;

    /**
     * By default the async state is updated only if there are no additional pending call. Set to true
     * to change that.
     */
    updateStateOnAllCall?: boolean;

    /**
     * Delay execution of async call by this amount. If another call was pending before the async
     * call, the async call is not run in favour of later queued call.
     */
    debounceDelayMs?: number;

    /**
     * By default, debounce start on additional call when a current call is running. This is to improve
     * user feedback in case only one request is required. Set this to true to delay even the first
     * call.
     */
    debounceOnInitialCall?: boolean;

    /**
     * By default, initially the state is AsyncState.LOADING. This is because for most use case,
     * data is loaded at the start. But when using `useManualAsyncState`, a separate state for before
     * the trigger is called is probably desired, in which case, this flag is turned on by default.
     */
    initiallyPending?: boolean;
    
}

asyncUIBlock<T>(AsyncEffectState<T>, onResolve: (T) => React.ReactNode, onReject: (Error) => React.ReactNode, onLoading?: () => React.ReactNode, onPending?: () => React.ReactNode) => React.ReactNode

A small syntactical sugar that runs one of the three closure and returns its response depending on the current request state. On loading and on pending is optional and will return undefined if not specified.

useManualAsyncState<T>(closure: () => Promise<T>, options: Options) => [AsyncEffectState<T>, () => () => void, () => void]

Behave the same as useAsyncEffectState, but the async call must be triggered manually via the second return value. Useful when the async call needs to be triggered by a button, for example:

import { useManualAsyncState, asyncUIBlock } from 'react-async-effect-state';

const [responseAsync, trigger, reset] = useManualAsyncState(
    () => fetch('http://example.com'), []);

return asyncUIBlock(responseAsync,
    (response) => (<p>{response} <button onClick={reset}>Reset</button></p>),
    (error) => (<p>An error occured {error.toString()} <button onClick={trigger}>Retry</button></p>),
    () => (<p>Loading data...</p>),
    () => (<p>No call yet... <button onClick={trigger}>Actually start loading</button></p>),
);

The trigger function returns another function that can be used to cancel state change when the call is complete. This is useful in a useEffect call.

The third return value is a reset function for changing the state back to pending.

Note that, it is your responsibility to prevent trigger from being called more than once if that is your intention.

Also, with debounceOnInitialCall off, usually async call will be called immediately, so if you change some state, and immediately call trigger, then the async call closure will not get the updated state.

map<T,U>(mapper: (T) => U, input: AsyncEffectState<T>) => AsyncEfectState<U>

Simple synchronous mapper for an AsyncEffectState which only map the result when the state is resolved. Useful for transforming the data without using the async function passed in the useAsyncEffectState which depending on youar use case will probably require another http call.

flatMap<T,U>(mapper: (T) => AsyncEffectState<U>, input: AsyncEffectState<T>) => AsyncEfectState<U>

Map the input state if resolved through a mapper. The mapper should itself returns an AsyncEffectState<U>. Note that the mapper runs conditionally, meaning it can't have React's useState or any other use* calls including useAsyncEffectState which uses useState and useEffect internally. It can however, return another AsyncEffectState<U> from it's closure.

combine<T1, T2, U>(combiner: (T1, T2) => U, input1: AsyncEffectState<T1>, input2: AsyncEFfectState<T2>) => AsyncEffectState<U>

Synchronously combine two AsyncEffectState into one. Only runs if both input is resolved. Otherwise, it will return the first errored input, followed by the first loading input.

License

MIT © Muhammad Amirul Ashraf