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-use-async-result

v0.1.2

Published

`use-async-result` is a simple, comprehensive, and type-safe library for exposing a Promise's states (loading/error/done/empty) to a React component.

Downloads

434

Readme

use-async-result: Simple, Comprehensive, Type-Safe Promises in React Components

use-async-result is a simple, comprehensive, and type-safe library for exposing a Promise's states (loading/error/done/empty) to a React component.

Installation

Install with npm:

npm install --save-dev react-use-async-result

Or with yarn:

yarn add react-use-async-result

And import with:

import { useAsyncResult } from 'react-use-async-result'

Simple Example

The following is a simple, naïve, example of useAsyncResult; for a more complete, real-world, example, see Complete Example, below.

import { useAsyncResult } from 'react-use-async-result'

const UserProfile = (props: { userId: string }) => {
    const userReq = useAsyncResult<User>(async () => {
        const req = await fetch(`/users/${props.userId}`)
        return req.json()
    }, [props.userId])

    if (userReq.isError)
        return <div>Error: {'' + userReq.error}</div>

    if (!userReq.isDone)
        return <div>Loading...</div>

    const user = userReq.result
    return <div>Username: {user.username}</div>
}

Motivating Example

Without the use of libraries, it can be quite complex to correctly handle asynchronous operations in components. Take, for example, a component nearly every React developer has likely written at least once in their lives: a UserProfile component.

This is a conceptually simple component: accept a userId, load the profile, show the profile. A simple, happy-path, version of this component can be written in less than 10 lines:

const UserProfile = (props: { userId: string }) => {
    const [user, setUser] = useState<User | null>(null)
    useEffect(() => {
        fetch(`/users/${props.userId}`)
            .then(response => response.json())
            .then(user => setUser(user))
    }, [props.userId])
    return <div>Username: {user?.username}</div>
}

But the keen-eyed developer will notice a number of issues with this implementation:

  • No feedback or error messages are displayed if request fails
  • An "empty" username will be displayed while the request is loading (instead of something more user-friendly, like a "Loading" message)
  • There's the possibility of a race condition if the userId is changed quickly (for example, imagine a request for /users/1 is loading extremely slowly, and while it's loading the userId is changed to 2 and the request for /users/2 completes quickly; when the request for /users/1 finally completes, the profile of /user/1 will be displayed instead of /users/2).

These issues can, of course, be resolved:

const UserProfile = (props: { userId: string }) => {
    const [user, setUser] = useState<User | null>(null)
    const [userError, setUserError] = useState(string | null)(null)
    const [userLastReq, _] = useState({ id: "" })

    useEffect(() => {
        const reqUserId = props.userId
        userLastReq.id = reqUserId
        setUser(null)
        setUserError(null)
        fetch(`/users/${reqUserId}`)
            .then(response => response.json())
            .then(
                user => userLastReq.id == reqUserId && setUser(user),
                error => userLastReq.id == reqUserId && setUserError('' + error),
            )
    }, [props.userId])

    if (userError)
        return <div>Error: {userError}</div>

    if (!user)
        return <div>Loading...</div>

    return <div>Username: {user.username}</div>
}

But while this code is correct, it isn't great:

  • The signal-to-nose ratio is low. Of the 20 non-whitespace lines, only 5 are doing something specifically related to "showing a user profile"; the other 15 generic bookkeeping which could be exactly the same for any other component which "loads and displays a thing".
  • The logical complexity is discordant with the implementational complexity. Logically the component is extremely simple: load and display a user's profile; even new React developers could reasonably be expected to understand this logic. The implementation, on the other hand, is complex: it relies on a nuanced understanding of React, closures, and mutability; even experienced developers could be forgiven for missing some of this nuance.

use-async-result provides a simple, comprehensive, abstraction over asynchronous operations in React components; it makes correct promise handling straightforward, and incorrect or incomplete handling difficult.

For example, if the original component was written with useAsyncResult, it might look like this:

import { useAsyncResult } from 'react-use-async-result'

const UserProfile = (props: { userId: string }) => {
    const userReq = useAsyncResult<User>(() => {
        return fetch(`/users/${props.userId}`)
            .then(response => response.json())
    }, [props.userId])
    const user = userReq.result
    return <div>Username: {user.username}</div>
}

But this will fail to compile, because it assumes the result isDone, and doesn't handle the pending or error states:

const user = userReq.result
                     ^^^^^^
Property 'result' does not exist on type 'UseAsyncResult<User>'.
    Property 'result' does not exist on type 'UseAsyncResultPending<User>'.

Which can be addressed by introducing standardized "loading" and "error" components:

// yourapp/components/async-result.tsx
import { AsyncResultError, AsyncResultNotDone } from 'react-use-async-result'

const ResultLoading = () => {
    return <div>Loading...</div>
}

const ResultError = (props: { result: AsyncResultError }) => {
    return <div>Error: {'' + props.result.error}</div>
}

const ResultNotDone = (props: { result: AsyncResultNotDone }) => {
    const { result } = props
    if (result.isError)
        return <ResultError result={result} />
    return <ResultLoading />
}

And, in the UserProfile component, checking whether the result isDone before using it:

// yourapp/components/UserProfile.tsx
const UserProfile = (props: { userId: string }) => {
    // ... snip ...

    if (!userReq.isDone)
        return <ResultNotDone result={result} />

    const user = userReq.result
    return <div>Username: {user.username}</div>
}

Complete Example

This (more) complete examples shows how useAsyncResult can be used for both loading when a component is created, and handling asynchronous requests in response to user actions.

Notice specifically how the "Save User" button is disabled while the "save" request is pending, how "save" errors are handled, and how the "save" request is cleared when a new profile is loaded.

import { useAsyncResult } from 'react-use-async-result'

const EditUserProfile = (props: { userId: string }) => {
    const userGetRes = useAsyncResult(async () => {
        userSaveRes.clear()
        const response = await fetch(`/users/${props.userId}`)
        return await response.json()
    }, [props.userId])

    const userSaveRes = useAsyncResult()
    const saveUser = () => {
        userSaveRes.bind(fetch({
            method: 'POST',
            url: `users/${props.userId}`,
            data: {
                username: document.getElementById("username").value,
            },
        }))
    }

    if (!userGetRes.isDone)
        return <ResultNotDone result={userGetRes} />

    const user = userGetRes.result

    return <div>
        Username: <input id="username" defaultValue={user.username} /><br />
        <button disabled={userSaveRes.isPending} onClick={saveUser}>
            Save User
        </button>
        {userSaveRes.isError && <ResultError result={iserSaveRes} />}
    </div>
}

Chaining AsyncResults

Similar to how Promises can be chained by returning a new promise in the handler to an existing promise, useAsyncResult can "chain" AsyncResults.

For example, consider a UserProfile component which accepts userId={null} when no user has been selected: an AsyncResult.empty() can be used to signal that the user request is "empty", and will never either "load" or "error":

import { useAsyncResult } from 'react-use-async-result'

const UserProfile = (props: { userId: string | null}) => {
    const userReq = useAsyncResult<User>(() => {
        if (!props.userId)
            return AsyncResult.empty()

        // ... snip ...
    }, [props.userId])

    if (userReq.isEmpty)
        return <div>Select a user to view their profile.</div>

    // ... snip ...
}

Type Safety Examples

Below are some examples of the type safety features of useAsyncResult.

Results can only be used once the promise has resolved:

const res = useAsyncResult(...)

console.log(res.result) // error
// example.tsx:1:14 - error TS2339: Property 'result' does not exist on type 'UseAsyncResult<...>'.
//  Property 'result' does not exist on type 'UseAsyncResultPending<...>'.
//
//          result.result // error
//                 ~~~~~~

if (res.isDone)
    console.log(res.result) // ok

The result's type will be inferred from the promise's type when possible:

const res = useAsyncResult(() => userApi.getUser(42))
if (res.isDone)
    console.log(res.result.username) // ok

The AsyncResultNotDone convenience type can be used to assert that a result has not yet resolved:

const showPendingResult = (res: AsyncResultNotDone) => { ... }

const res = useAsyncResult(() => userApi.getUser(42))

showPendingResult(res) // error
// example.tsx:19 - error TS2345: Argument of type 'UseAsyncResult<number>' is not assignable to parameter of type 'AsyncResultNotDone<unknown>'.
//   Type 'UseAsyncResultDone<number>' is not assignable to type 'AsyncResultNotDone<unknown>'.
//
//     showPendingResult(res) // error
//                       ~~~

if (!res.isDone)
    showPendingResult(res) // ok