diff --git a/.babelrc.js b/.babelrc.js index 9ef83f62..97bb886b 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -5,7 +5,7 @@ const loose = true module.exports = { presets: [['@babel/preset-env', { loose, modules: false }]], plugins: [ - ['@babel/proposal-object-rest-spread', { loose }], + ['@babel/plugin-transform-object-rest-spread', { loose }], cjs && ['@babel/transform-modules-commonjs', { loose }], ['@babel/transform-runtime', { useESModules: !cjs }] ].filter(Boolean), diff --git a/.prettierrc b/.prettierrc index f79acadc..bf7e27e7 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,5 +2,6 @@ "semi": false, "singleQuote": true, "trailingComma": "none", - "arrowParens": "avoid" + "arrowParens": "avoid", + "endOfLine": "auto" } diff --git a/jest.config.js b/jest.config.js index c7d3135c..c3fa6389 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,12 +6,14 @@ const commonOptions = { const projects = [ { displayName: 'js', - testMatch: ['**/?(*.)+(spec|test).js?(x)'] + testMatch: ['**/?(*.)+(spec|test).js?(x)'], + testEnvironment: 'jsdom' }, { displayName: 'ts', testMatch: ['**/?(*.)+(spec|test).ts?(x)'], - preset: 'ts-jest/presets/js-with-ts' + preset: 'ts-jest/presets/js-with-ts', + testEnvironment: 'jsdom' }, { displayName: 'ssr-js', diff --git a/package.json b/package.json index 9925f7b4..406bdcc0 100644 --- a/package.json +++ b/package.json @@ -29,53 +29,54 @@ "lint": "eslint . --ext .js,.jsx", "prepare": "npm run clean && npm run build && husky install", "release": "standard-version", - "pretest": "cp ./test/index.test.jsx ./test/index.test.tsx && cp ./test/index.test.ssr.jsx ./test/index.test.ssr.tsx", + "pretest": "shx cp ./test/index.test.jsx ./test/index.test.tsx && shx cp ./test/index.test.ssr.jsx ./test/index.test.ssr.tsx", "test": "tsd && jest --no-cache" }, "dependencies": { "@babel/runtime": "7.22.11", "dequal": "2.0.3", - "lru-cache": "^8.0.0" + "lru-cache": "^10.0.1" }, "peerDependencies": { - "axios": ">=0.24.0", + "axios": ">=1.0.0", "react": "^16.8.0-0 || ^17.0.0 || ^18.0.0" }, "devDependencies": { "@babel/cli": "7.22.10", "@babel/core": "7.22.11", "@babel/plugin-transform-runtime": "7.22.10", - "@babel/preset-env": "7.21.5", + "@babel/preset-env": "7.22.14", "@babel/preset-react": "7.22.5", "@commitlint/cli": "17.7.1", "@commitlint/config-conventional": "17.7.0", "@testing-library/react": "12.1.5", "@testing-library/react-hooks": "7.0.2", - "@types/jest": "29.5.3", + "@types/jest": "29.5.4", "@types/node": "20.5.7", "@types/react": "18.2.20", "@types/react-dom": "18.2.7", - "axios": "0.27.2", + "axios": "1.5.0", "cross-env": "7.0.3", "eslint": "8.48.0", "eslint-config-prettier": "9.0.0", "eslint-plugin-import": "2.28.1", - "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-prettier": "5.0.0", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "husky": "^8.0.1", - "jest": "26.6.3", + "jest": "29.6.4", + "jest-environment-jsdom": "^29.6.4", "lint-staged": "14.0.1", "npm-run-all": "4.1.5", - "prettier": "2.8.8", + "prettier": "3.0.3", "react": "17.0.2", "react-dom": "17.0.2", - "react-test-renderer": "17.0.2", "rimraf": "5.0.1", + "shx": "0.3.4", "standard-version": "9.5.0", - "ts-jest": "26.5.6", + "ts-jest": "29.1.1", "tsd": "^0.29.0", - "typescript": "4.9.5" + "typescript": "5.2.2" }, "lint-staged": { "{src,test}/**/*.{js?(x),md}": [ diff --git a/src/index.d.ts b/src/index.d.ts index 7c417419..365b73bf 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -6,7 +6,7 @@ import { AxiosInstance, AxiosResponse } from 'axios' -import LRUCache from 'lru-cache' +import { LRUCache } from 'lru-cache' export interface ResponseValues { data?: TResponse diff --git a/src/index.js b/src/index.js index 473ded87..b9cd2364 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import React from 'react' -import StaticAxios from 'axios' -import LRU from 'lru-cache' +import StaticAxios, { isCancel } from 'axios' +import { LRUCache } from 'lru-cache' import { dequal as deepEqual } from 'dequal/lite' const actions = { @@ -69,7 +69,7 @@ export function makeUseAxios(configureOptions) { const __ssrPromises = [] function resetConfigure() { - cache = new LRU({ max: 500 }) + cache = new LRUCache({ max: 500 }) axiosInstance = StaticAxios defaultOptions = DEFAULT_OPTIONS } @@ -189,7 +189,7 @@ export function makeUseAxios(configureOptions) { return response } catch (err) { - if (!StaticAxios.isCancel(err)) { + if (!isCancel(err)) { dispatch({ type: actions.REQUEST_END, payload: err, error: true }) } @@ -217,7 +217,7 @@ export function makeUseAxios(configureOptions) { useDeepCompareMemoize(_options) ) - const cancelSourceRef = React.useRef() + const abortControllerRef = React.useRef() const [state, dispatch] = React.useReducer( reducer, @@ -229,20 +229,20 @@ export function makeUseAxios(configureOptions) { } const cancelOutstandingRequest = React.useCallback(() => { - if (cancelSourceRef.current) { - cancelSourceRef.current.cancel() + if (abortControllerRef.current) { + abortControllerRef.current.abort() } }, []) - const withCancelToken = React.useCallback( + const withAbortSignal = React.useCallback( config => { if (options.autoCancel) { cancelOutstandingRequest() } - cancelSourceRef.current = StaticAxios.CancelToken.source() + abortControllerRef.current = new AbortController() - config.cancelToken = cancelSourceRef.current.token + config.signal = abortControllerRef.current.signal return config }, @@ -251,7 +251,7 @@ export function makeUseAxios(configureOptions) { React.useEffect(() => { if (!options.manual) { - request(withCancelToken(config), options, dispatch).catch(() => {}) + request(withAbortSignal(config), options, dispatch).catch(() => {}) } return () => { @@ -259,14 +259,14 @@ export function makeUseAxios(configureOptions) { cancelOutstandingRequest() } } - }, [config, options, withCancelToken, cancelOutstandingRequest]) + }, [config, options, withAbortSignal, cancelOutstandingRequest]) const refetch = React.useCallback( (configOverride, options) => { configOverride = configToObject(configOverride) return request( - withCancelToken({ + withAbortSignal({ ...config, ...(isReactEvent(configOverride) ? null : configOverride) }), @@ -274,7 +274,7 @@ export function makeUseAxios(configureOptions) { dispatch ) }, - [config, withCancelToken] + [config, withAbortSignal] ) return [state, refetch, cancelOutstandingRequest] diff --git a/test/index.test.jsx b/test/index.test.jsx index 9782fe38..9fae09f4 100644 --- a/test/index.test.jsx +++ b/test/index.test.jsx @@ -1,5 +1,5 @@ import React from 'react' -import axios from 'axios' +import axios, { CanceledError } from 'axios' import { render, fireEvent } from '@testing-library/react' import { renderHook, act } from '@testing-library/react-hooks' @@ -11,17 +11,15 @@ import defaultUseAxios, { serializeCache as defaultSerializeCache, makeUseAxios } from '../src' -import { mockCancelToken } from './testUtils' -import LRUCache from 'lru-cache' +import { LRUCache } from 'lru-cache' jest.mock('axios') -let cancel -let token let errors +let abortSpy beforeEach(() => { - ;({ cancel, token } = mockCancelToken(axios)) + abortSpy = jest.spyOn(AbortController.prototype, 'abort') }) beforeAll(() => { @@ -385,7 +383,7 @@ function standardTests( rerender() - expect(cancel).not.toHaveBeenCalled() + expect(abortSpy).not.toHaveBeenCalled() }) it('should skip default cancellation after unmount if options.autoCancel is set to false', async () => { @@ -399,17 +397,17 @@ function standardTests( unmount() - expect(cancel).not.toHaveBeenCalled() + expect(abortSpy).not.toHaveBeenCalled() }) - it('should provide the cancel token to axios', async () => { + it('should provide the abort signal to axios', async () => { axios.mockResolvedValueOnce({ data: 'whatever' }) const { waitForNextUpdate } = setup('') expect(axios).toHaveBeenCalledWith( expect.objectContaining({ - cancelToken: token + signal: expect.any(AbortSignal) }) ) @@ -425,7 +423,7 @@ function standardTests( unmount() - expect(cancel).toHaveBeenCalled() + expect(abortSpy).toHaveBeenCalled() }) it('should cancel the outstanding request when the cancel method is called', async () => { @@ -437,7 +435,7 @@ function standardTests( result.current[2]() - expect(cancel).toHaveBeenCalled() + expect(abortSpy).toHaveBeenCalled() }) it('should cancel the outstanding request when the component refetches due to a rerender', async () => { @@ -449,7 +447,7 @@ function standardTests( rerender({ config: 'new config', options: {} }) - expect(cancel).toHaveBeenCalled() + expect(abortSpy).toHaveBeenCalled() await waitForNextUpdate() }) @@ -463,7 +461,7 @@ function standardTests( rerender() - expect(cancel).not.toHaveBeenCalled() + expect(abortSpy).not.toHaveBeenCalled() }) it('should not cancel the outstanding request when the component rerenders with same object config', async () => { @@ -475,7 +473,7 @@ function standardTests( rerender() - expect(cancel).not.toHaveBeenCalled() + expect(abortSpy).not.toHaveBeenCalled() }) it('should not cancel the outstanding request when the component rerenders with equal string config', async () => { @@ -487,7 +485,7 @@ function standardTests( rerender({ config: 'initial config', options: {} }) - expect(cancel).not.toHaveBeenCalled() + expect(abortSpy).not.toHaveBeenCalled() }) it('should not cancel the outstanding request when the component rerenders with equal object config', async () => { @@ -499,7 +497,7 @@ function standardTests( rerender({ config: { some: 'config' }, options: {} }) - expect(cancel).not.toHaveBeenCalled() + expect(abortSpy).not.toHaveBeenCalled() }) it('should cancel the outstanding request when the cancel method is called after the component rerenders with same config', async () => { @@ -513,16 +511,13 @@ function standardTests( result.current[2]() - expect(cancel).toHaveBeenCalled() + expect(abortSpy).toHaveBeenCalled() }) it('should not dispatch an error when the request is canceled', async () => { - const cancellation = new Error('canceled') + const cancellation = new CanceledError('canceled') axios.mockRejectedValueOnce(cancellation) - axios.isCancel = jest - .fn() - .mockImplementationOnce(err => err === cancellation) const { result, waitFor } = setup('') @@ -530,14 +525,16 @@ function standardTests( // to wait for. yet, if we don't try to wait, we won't know if we're handling // the error properly because the return value will not have the error until a // state update happens. it would be great to have a better way to test this - await waitFor(() => { - expect(result.current[0].error).toBeNull() + await act(async () => { + await waitFor(() => { + expect(result.current[0].error).toBeNull() + }) }) }) }) describe('manual refetches', () => { - it('should provide the cancel token to axios', async () => { + it('should provide the abort signal to axios', async () => { const { result, waitForNextUpdate } = setup('', { manual: true }) axios.mockResolvedValueOnce({ data: 'whatever' }) @@ -550,7 +547,7 @@ function standardTests( expect(axios).toHaveBeenLastCalledWith( expect.objectContaining({ - cancelToken: token + signal: expect.any(AbortSignal) }) ) @@ -572,7 +569,7 @@ function standardTests( unmount() - expect(cancel).toHaveBeenCalled() + expect(abortSpy).toHaveBeenCalled() }) it('should cancel the outstanding manual refetch when the component refetches', async () => { @@ -588,7 +585,7 @@ function standardTests( rerender({ config: 'new config', options: {} }) - expect(cancel).toHaveBeenCalled() + expect(abortSpy).toHaveBeenCalled() await waitForNextUpdate() }) @@ -606,16 +603,13 @@ function standardTests( result.current[2]() - expect(cancel).toHaveBeenCalled() + expect(abortSpy).toHaveBeenCalled() }) it('should throw an error when the request is canceled', async () => { - const cancellation = new Error('canceled') + const cancellation = new CanceledError('canceled') axios.mockRejectedValueOnce(cancellation) - axios.isCancel = jest - .fn() - .mockImplementationOnce(err => err === cancellation) const { result } = renderHook(() => useAxios('', { manual: true })) diff --git a/test/index.test.ssr.jsx b/test/index.test.ssr.jsx index 396ff07a..f4e9007d 100644 --- a/test/index.test.ssr.jsx +++ b/test/index.test.ssr.jsx @@ -3,14 +3,12 @@ import React from 'react' import ReactDOM from 'react-dom/server' import { makeUseAxios } from '../src' -import { mockCancelToken } from './testUtils' jest.mock('axios') let useAxios beforeEach(() => { - mockCancelToken(axios) useAxios = makeUseAxios() }) diff --git a/test/testUtils.js b/test/testUtils.js deleted file mode 100644 index 74aad5be..00000000 --- a/test/testUtils.js +++ /dev/null @@ -1,20 +0,0 @@ -export function mockCancelToken(axios) { - const cancel = jest.fn() - const token = { - promise: Promise.resolve({ message: 'none' }), - reason: { message: 'none' }, - throwIfRequested() {} - } - - const CancelToken = Object.assign(jest.fn(), { - source: () => ({ - cancel, - token - }) - }) - - axios.isCancel = jest.fn() - axios.CancelToken = CancelToken - - return { cancel, token } -}