react-extra-hooks

Hooks for data fetching and more

Usage no npm install needed!

<script type="module">
  import reactExtraHooks from 'https://cdn.skypack.dev/react-extra-hooks';
</script>

README

react-extra-hooks

yarn add react-extra-hooks

hooks

  • usePromise
  • useLazyPromise
  • useDebounce
  • useLocalStorage

Examples

usePromise

import { usePromise } from 'react-extra-hooks'
import React from 'react'

const sleep = (t) => new Promise((res) => setTimeout(res, t))

async function pp() {
    await sleep(400)
    return {
        x: 9,
    }
}

const App = () => {
    const { result, loading, error } = usePromise(pp)
    if (loading) {
        return <>loading</>
    }
    return <div>{result?.x}</div>
}

useLazyPromise

import { useLazyPromise } from 'react-extra-hooks'
import React from 'react'

const sleep = (t) => new Promise((res) => setTimeout(res, t))

async function effect() {
    await sleep(400)
    alert('executed')
    return {
        x: 9,
    }
}

const UseLazyPromiseExample = () => {
    const [execute, { result, loading, error }] = useLazyPromise(pp)
    if (loading) {
        return <>loading</>
    }
    if (result) {
        return <div>{result?.x}</div>
    }
    return (
        <div>
            <button onClick={execute}>execute promise</button>
        </div>
    )
}

useLazyPromise with in memory cache, supports the options {promiseId: string, cacheSize: numer, cacheExpirationSeconds: number}. Every useLazyPromise cached the result based on

  • the argument passed to the execute function
  • the promiseId option (defaults to the name of the promise)

The cache is invalidated when

  • the page is refreshed
  • the cacheExpirationSeconds (defaults to 120 secods) times out
  • the cache overflows his size (that defaults to 50 elements), the first cached value is discarded
import { useLazyPromise } from 'react-extra-hooks'
import React from 'react'

const sleep = (t) => new Promise((res) => setTimeout(res, t))

async function effect(n: string) {
    await sleep(1000)
    // throw Error('xxx')
    alert('executed with n=' + n)
    return {
        x: n,
    }
}

const UseLazyPromiseExample = () => {
    const [arg, set] = useState('')
    const [execute, { result, loading, error }] = useLazyPromise(effect, {
        cache: true,
        cacheExpirationSeconds: 300,
    })
    if (loading) {
        return <>loading</>
    }
    if (error) {
        return <div>{error.message}</div>
    }

    return (
        <div>
            <input onChange={(e) => set(e.target.value)} value={arg} />
            <button onClick={() => execute(arg)}>execute promise</button>
            <code>
                <div>{result?.x}</div>
            </code>
        </div>
    )
}

Example with delayed argument necessary for the promise

const { query } = useRouter() // null on first render
const { result, loading, error } = usePromise(query ? asyncFunction : null, {
    cache: true,
    args: [query],
})

Usage with pagination

import { useLazyPromise } from 'react-extra-hooks'
import React from 'react'

const sleep = (t) => new Promise((res) => setTimeout(res, t))

const PaginationExample = () => {
    const [page, setPage] = useState(0)

    async function loadData(page) {
        const items = await fetchSomething({ page })
        return [...result, ...items]
    }

    const { result = [], loading, error } = usePromise(loadData, {
        args: [page],
    })

    if (!result.length) {
        return <>loading</>
    }

    if (error) {
        return <div>{error.message}</div>
    }

    return (
        <div>
            <span>page: {page}</span>
            <code>
                <div>
                    {result.map((x) => (
                        <div>{x}</div>
                    ))}
                    <button
                        disabled={loading}
                        onClick={() => setPage((x) => x + 1)}
                    >
                        {loading ? 'loading' : 'more'}
                    </button>
                </div>
            </code>
        </div>
    )
}

async function fetchSomething({ page }) {
    await sleep(1000)
    return Array(10)
        .fill(page + 10)
        .map((x, i) => x + i)
        .map((x) => x.toString())
}

Pagination with cursor

const ItemsList = ({}) => {
    const [after, setAfter] = useState(undefined)

    const getUserItems = useCallback(
        async (after): Promise<GetUserItemsQuery['Items']> => {
            const uid = getUid()
            const res = await sdk.GetUserItems({
                first: 3,
                after: after,
            })
            const newItems = res?.Items || ({} as any)
            return {
                ...newItems,
                nodes: [...(Items?.nodes || []), ...(newItems?.nodes || [])],
            }
        },
        [after],
    )

    const { result: Items, loading, error } = usePromise(getUserItems, {
        cache: true,
        args: [after],
    })

    const hasItems = !loading && Items?.nodes?.length

    if (!Items && loading) {
        return (
            <Center>
                <Spinner />
            </Center>
        )
    }
    if (error) {
        return (
            <Center>
                <ErrorMessage msg={error?.message} />
            </Center>
        )
    }
    if (!hasItems) {
        return (
            <Col align='center' justify='center' opacity={0.8} h='100%'>
                <NextLink href='/new'>
                    <Button>Create your first Item</Button>
                </NextLink>
            </Col>
        )
    }
    return (
        <Stack spacing={6}>
            {Items?.nodes?.map((item, i) => (
                <ItemCard key={i} data={item} />
            ))}
            {hasItems && Items?.pageInfo?.hasNextPage && (
                <Button
                    isLoading={loading}
                    isDisabled={loading}
                    onClick={(e) => setAfter(Items?.pageInfo?.endCursor)}
                >
                    Load More
                </Button>
            )}
        </Stack>
    )
}

TODO add support for suspense