Skip to content

Commit

Permalink
fixes: v0.1.4 - Fixed useAsync, added useInterval, added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
svaraborut committed Oct 19, 2022
1 parent 5beff29 commit bd71cc1
Show file tree
Hide file tree
Showing 8 changed files with 2,299 additions and 130 deletions.
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ node_modules
src
test

jest.config.ts
package-lock.json
tsconfig.json
14 changes: 14 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {Config} from '@jest/types';

// Sync object
const config: Config.InitialOptions = {
preset: 'ts-jest',
verbose: true,
clearMocks: true,
testEnvironment: 'jsdom',
// transform: {
// [`^.+\\.tsx?$`]: `ts-jest`,
// },
}

export default config
2,177 changes: 2,057 additions & 120 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
"description": "Hook collection for React",
"author": "Borut Svara <borut@svara.io>",
"license": "ISC",
"version": "0.1.3",
"version": "0.1.4",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "tsc"
"test": "jest"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@testing-library/react-hooks": "^8.0.1",
"@types/jest": "^29.2.0",
"@types/react": "16.14.32",
"jest": "^29.2.1",
"jest-environment-jsdom": "^29.2.1",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typescript": "^4.8.4"
},
"directories": {
Expand Down
15 changes: 9 additions & 6 deletions src/async/useAsync.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useRef, useState } from 'react';
import { PromiseFn } from './types';
import { usePromise } from './usePromise';
import { useLatest } from '../generic/useLatest';

/**
* An asynchronous wrapping function, that enables use of async functions within
Expand All @@ -14,7 +15,7 @@ import { usePromise } from './usePromise';
export interface UseAsyncState<Res, Err> {
isLoading: boolean
isCompleted: boolean
isSucceeded: boolean
isSucceed: boolean
isFailed: boolean
result?: Res
error?: Err
Expand All @@ -29,31 +30,33 @@ export function useAsync<Res = void, Args extends any[] = [], Err = any>(
fn: PromiseFn<Args, Res>,
): UseAsyncReturn<Res, Args, Err> {

const lastFn = useLatest(fn)

// Unique call id to handle concurrency
const callIdRef = useRef(0)

// Inner state
const [state, setState] = useState<UseAsyncState<Res, Err>>({
isLoading: false,
isCompleted: false,
isSucceeded: false,
isSucceed: false,
isFailed: false,
});

const runAsync = useCallback<PromiseFn<Args, Res>>(async (...args: Args) => {
const callId = ++callIdRef.current

setState({ isLoading: true, isCompleted: false, isSucceeded: false, isFailed: false })
setState({ isLoading: true, isCompleted: false, isSucceed: false, isFailed: false })
try {
// Execute
const result = await fn(...args)
const result = await lastFn.current(...args)
if (callIdRef.current === callId) {
setState({ isLoading: false, isCompleted: true, isSucceeded: true, isFailed: false, result })
setState({ isLoading: false, isCompleted: true, isSucceed: true, isFailed: false, result })
}
return result
} catch (error) {
if (callIdRef.current === callId) {
setState({ isLoading: false, isCompleted: true, isSucceeded: false, isFailed: true, error })
setState({ isLoading: false, isCompleted: true, isSucceed: false, isFailed: true, error })
}
throw error
}
Expand Down
31 changes: 31 additions & 0 deletions src/time/useInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { CallFn } from '../async/types';
import { DependencyList, useEffect } from 'react';
import { useLatest } from '../generic/useLatest';

/**
* Simply set an interval by providing a callback and delay, the interval
* is automatically mounted and unmounted.
*
* (?) When delay changes the interval is updated, to disable it just
* provide null or undefined as delay.
*
* (?) Additionally a list of dependencies can be provided. The dependencies
* will cause the interval to be rescheduled (delay is implicitly a
* dependency)
*
* todo : add enable fn to return a tear down function
*/
export function useInterval(fn: CallFn, ms?: number | null | undefined, deps?: DependencyList) {

const lastFn = useLatest(fn)

useEffect(() => {

if (ms === undefined || ms === null) return undefined;

const interval = setInterval(() => lastFn.current(), ms || 0)
return () => clearInterval(interval)

}, [ms, ...(deps || [])])

}
180 changes: 180 additions & 0 deletions test/async/useAsync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { useAsync } from '../../src';

describe('useAsync', () => {

function factory(resolve: boolean = true, result: string = 'done', ms: number = 0) {
return () => {
return new Promise((res, rej) => {
const wait = setTimeout(() => {
clearTimeout(wait);
(resolve ? res : rej)(result)
}, ms)
})
}
}

function hook() {
return renderHook(
({ fn }) => useAsync(fn),
{ initialProps: { fn: factory(true) }}
)
}

it('should be defined', () => {
expect(useAsync).toBeDefined()
})

it('should be initialized', async () => {
const h = hook()
expect(h.result.current).toMatchObject({
isLoading: false,
isCompleted: false,
isSucceed: false,
isFailed: false,
// result: undefined,
// error: undefined,
})
expect(h.result.current).not.toHaveProperty('result')
expect(h.result.current).not.toHaveProperty('error')
})

describe('run()', () => {

it('should resolve', async () => {
const h = hook()

// const res = await h.result.current.runAsync()
act(() => {
h.result.current.run()
})
await h.waitForNextUpdate()

expect(h.result.current).toMatchObject({
isLoading: false,
isCompleted: true,
isSucceed: true,
isFailed: false,
result: 'done',
})
})

it('should fail', async () => {
const h = hook()
await h.rerender({ fn: factory(false) })

// let err;
// try {
// await h.result.current.runAsync()
// } catch (e) {
// err = e
// }

act(() => {
h.result.current.run()
})
await h.waitForNextUpdate()

expect(h.result.current).toMatchObject({
isLoading: false,
isCompleted: true,
isSucceed: false,
isFailed: true,
error: 'done',
})
})

})

// todo : how to wait for the promise ?
// describe('runAsync()', () => {
//
// it('should resolve', async () => {
// const h = hook()
//
// let res = 'xxx'
// await act(async () => {
// return h.result.current.runAsync()
// })
//
// await h.waitForNextUpdate()
//
// expect(h.result.current).toMatchObject({
// isLoading: false,
// isCompleted: true,
// isSucceed: true,
// isFailed: false,
// result: res,
// })
// })
//
// it('should fail', async () => {
// const h = hook()
// await h.rerender({ fn: factory(false) })
//
// // let err;
// // try {
// // await h.result.current.runAsync()
// // } catch (e) {
// // err = e
// // }
//
// h.result.current.run()
// await h.waitForNextUpdate()
//
// expect(h.result.current).toMatchObject({
// isLoading: false,
// isCompleted: true,
// isSucceed: false,
// isFailed: true,
// error: 'done',
// })
// })
//
// })

it('supports unstable function', async () => {
const h = hook()
await h.rerender({ fn: factory(true, 'v1') })
await h.rerender({ fn: factory(true, 'v2') })

act(() => {
h.result.current.run()
})
await h.waitForNextUpdate()

expect(h.result.current.result).toEqual('v2')
})

it('is loading', async () => {
jest.useFakeTimers()

const h = hook()
await h.rerender({ fn: factory(true, 'slow', 1000) })

act(() => {
h.result.current.run()
})

expect(h.result.current).toMatchObject({
isLoading: true,
isCompleted: false,
isSucceed: false,
isFailed: false,
})

act(() => {
jest.runAllTimers()
})

await h.waitForNextUpdate()
expect(h.result.current).toMatchObject({
isLoading: false,
isCompleted: true,
isSucceed: true,
isFailed: false,
result: 'slow',
})
})

})
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"outDir": "dist",
"module": "esnext",
"target": "es5",
"target": "es6",
"lib": ["es2017", "es7", "es6", "dom"],
"declaration": true,
"moduleResolution": "node",
Expand Down

0 comments on commit bd71cc1

Please sign in to comment.