Skip to content

Commit

Permalink
perf(*): add warning when passing empty dependecies or primitive depe…
Browse files Browse the repository at this point in the history
…ndecies
  • Loading branch information
lintuming committed Sep 9, 2019
1 parent 8b0d00f commit da6f6aa
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 17 deletions.
Empty file removed CHANGELOG.md
Empty file.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ const useSomething=()=>{
}
```

## LICENSE
## License

- [MIT](./LICENSE)
4 changes: 3 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module.exports = {
clearMocks: true
clearMocks: true,
testMatch: ['<rootDir>/src/**/__tests__/*.test.{js,jsx,ts,tsx}'],
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/__tests__/*'],
};
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"version": "1.0.1",
"main": "lib/index.js",
"module": "esm/index.js",
"repository": "https://github.com/lintuming/react-shallow-hooks.git",
"repository": {
"type": "git",
"url": "https://github.com/lintuming/react-shallow-hooks.git"
},
"author": "lintuming <lntmg.lin@gmail.com>",
"license": "MIT",
"files": [
Expand Down
33 changes: 31 additions & 2 deletions src/__tests__/useShallowCallback.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { useShallowCallback } from '../';
import { renderHook, RenderHookResult } from '@testing-library/react-hooks';

import { getEmptyDepsMsg, getPrimitiveDepsMsg } from './utils';
describe('useShallowCallback', () => {
const originWarn = console.warn;
const OLD_ENV = process.env;

beforeEach(() => {
console.warn = jest.fn();
jest.resetModules(); // this is important - it clears the cache
process.env = { ...OLD_ENV };
delete process.env.NODE_ENV;
});

afterEach(() => {
console.warn = originWarn;
process.env = OLD_ENV;
});
it('should be defined', () => {
expect(useShallowCallback).toBeDefined();
});

const getHook = (deps?: any[]): [RenderHookResult<any[], (...args: any[]) => any>, jest.Mock] => {
const bodyFn = jest.fn();
return [
Expand All @@ -20,8 +35,22 @@ describe('useShallowCallback', () => {
bodyFn,
];
};

it('should warn when passing empty deps', () => {
getHook();
expect(console.warn).toHaveBeenCalledWith(getEmptyDepsMsg('useShallowCallback'));
getHook([1, 2, 3]);
expect(console.warn).toHaveBeenCalledWith(getPrimitiveDepsMsg('useShallowCallback'));
});
it('should not warn when passing empty deps on production mode', () => {
process.env.NODE_ENV = 'production';
getHook();
expect(console.warn).toHaveBeenCalledTimes(0);
getHook([1, 2, 3]);
expect(console.warn).toHaveBeenCalledTimes(0);
});
it('should return a value', () => {
const [hook, bodyFn] = getHook();
const [hook, bodyFn] = getHook([{}]);
expect(hook.result.current).toHaveBeenCalledTimes(1);
expect(bodyFn).toHaveBeenCalledTimes(1);
});
Expand Down
37 changes: 34 additions & 3 deletions src/__tests__/useShallowEffect.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { useShallowEffect, useShallowLayoutEffect } from '../';
import { renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { getEmptyDepsMsg, getPrimitiveDepsMsg } from './utils';

const testEffect = (effectName: string) => {
const isLayoutEffect = effectName === 'useShallowLayoutEffect';
describe(isLayoutEffect ? 'useShallowLayoutEffect' : 'useShallowEffect', () => {
const getHook = (deps: any[], mockFn?: jest.Mock): [jest.Mock, RenderHookResult<any[], void>, jest.Mock] => {
const originWarn = console.warn;
const OLD_ENV = process.env;

beforeEach(() => {
console.warn = jest.fn();
jest.resetModules(); // this is important - it clears the cache
process.env = { ...OLD_ENV };
delete process.env.NODE_ENV;
});

afterEach(() => {
console.warn = originWarn;
process.env = OLD_ENV;
});
const getHook = (deps?: any[], mockFn?: jest.Mock): [jest.Mock, RenderHookResult<any[], void>, jest.Mock] => {
const spy = mockFn || jest.fn();
const bodyFn = jest.fn();
const callEffect = isLayoutEffect ? useShallowLayoutEffect : useShallowEffect;
Expand All @@ -23,10 +38,26 @@ const testEffect = (effectName: string) => {
it('should be defined', () => {
expect(useShallowEffect).toBeDefined();
});

it('should warn when passing empty deps', () => {
getHook();
expect(console.warn).toHaveBeenCalledWith(
getEmptyDepsMsg(isLayoutEffect ? 'useShallowLayoutEffect' : 'useShallowEffect')
);
getHook([1, 2, 3]);
expect(console.warn).toHaveBeenCalledWith(
getPrimitiveDepsMsg(isLayoutEffect ? 'useShallowLayoutEffect' : 'useShallowEffect')
);
});
it('should not warn when passing empty deps on production mode', () => {
process.env.NODE_ENV = 'production';
getHook();
expect(console.warn).toHaveBeenCalledTimes(0);
getHook([1, 2, 3]);
expect(console.warn).toHaveBeenCalledTimes(0);
});
it('should call the effect callback on mount,call the clean up on unmount', () => {
const cleanUp = jest.fn();
const [effectCallback, hook] = getHook([], jest.fn(() => cleanUp));
const [effectCallback, hook] = getHook([{}], jest.fn(() => cleanUp));
expect(effectCallback).toHaveBeenCalledTimes(1);
expect(cleanUp).toHaveBeenCalledTimes(0);
hook.unmount();
Expand Down
33 changes: 32 additions & 1 deletion src/__tests__/useShallowImperativeHandle.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import { useShallowImperativeHandle } from '../';
import React, { RefObject } from 'react';
import { renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { getEmptyDepsMsg, getPrimitiveDepsMsg } from './utils';

describe('useShallowImperativeHandle', () => {
const getHook = <T>(deps?: any[], init?: () => T): [RefObject<T>, RenderHookResult<any[], any>, jest.Mock] => {
const originWarn = console.warn;
const OLD_ENV = process.env;

beforeEach(() => {
console.warn = jest.fn();
jest.resetModules(); // this is important - it clears the cache
process.env = { ...OLD_ENV };
delete process.env.NODE_ENV;
});

afterEach(() => {
console.warn = originWarn;
process.env = OLD_ENV;
});
const getHook = <T>(
deps?: any[],
init: () => T = () => ({} as T)
): [RefObject<T>, RenderHookResult<any[], any>, jest.Mock] => {
const ref = React.createRef<T>();
const bodyFn = jest.fn();
return [
Expand All @@ -21,6 +39,19 @@ describe('useShallowImperativeHandle', () => {
it('should be defined', () => {
expect(useShallowImperativeHandle).toBeDefined();
});
it('should warn when pass empty deps', () => {
getHook();
expect(console.warn).toHaveBeenCalledWith(getEmptyDepsMsg('useShallowImperativeHandle'));
getHook([1, 2, 3]);
expect(console.warn).toHaveBeenCalledWith(getPrimitiveDepsMsg('useShallowImperativeHandle'));
});
it('should not warn when passing empty deps on production mode', () => {
process.env.NODE_ENV = 'production';
getHook();
expect(console.warn).toHaveBeenCalledTimes(0);
getHook([1, 2, 3]);
expect(console.warn).toHaveBeenCalledTimes(0);
});
it('should customizes the instance value', () => {
let i = 0;
const [ref, hook, bodyFn] = getHook([], () => ({
Expand Down
32 changes: 30 additions & 2 deletions src/__tests__/useShallowMemo.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { useShallowMemo } from '../';
import { renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { getEmptyDepsMsg, getPrimitiveDepsMsg } from './utils';

describe('useShallowMemo', () => {
it('should be defined', () => {
expect(useShallowMemo).toBeDefined();
const originWarn = console.warn;
const OLD_ENV = process.env;

beforeEach(() => {
console.warn = jest.fn();
jest.resetModules(); // this is important - it clears the cache
process.env = { ...OLD_ENV };
delete process.env.NODE_ENV;
});

afterEach(() => {
console.warn = originWarn;
process.env = OLD_ENV;
});
const getHook = (deps?: any[]): [RenderHookResult<any[], number>, jest.Mock] => {
let i = 0;
Expand All @@ -21,6 +33,22 @@ describe('useShallowMemo', () => {
bodyFn,
];
};
it('should be defined', () => {
expect(useShallowMemo).toBeDefined();
});
it('should warn when pass empty deps', () => {
getHook();
expect(console.warn).toHaveBeenCalledWith(getEmptyDepsMsg('useShallowMemo'));
getHook([1, 2, 3]);
expect(console.warn).toHaveBeenCalledWith(getPrimitiveDepsMsg('useShallowMemo'));
});
it('should not warn when passing empty deps on production mode', () => {
process.env.NODE_ENV = 'production';
getHook();
expect(console.warn).toHaveBeenCalledTimes(0);
getHook([1, 2, 3]);
expect(console.warn).toHaveBeenCalledTimes(0);
});
it('should return a value', () => {
const [hook, bodyFn] = getHook();
expect(hook.result.current).toBe(0);
Expand Down
11 changes: 11 additions & 0 deletions src/__tests__/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function getEmptyDepsMsg(hookName) {
const originName = `use${hookName.slice(10)}`;

return `\`${hookName}\` should not be used with no dependencies. Use React.${originName} instead.`;
}

export function getPrimitiveDepsMsg(hookName) {
const originName = `use${hookName.slice(10)}`;

return `\`${hookName}\` should not be used with dependencies that are all primitive values. Use React.${originName} instead.`;
}
22 changes: 22 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
import React, { Ref, EffectCallback, DependencyList } from 'react';
import useShallowDeps from './useShallowDeps';

const isPrimitive = (val: any) => val !== Object(val);

const warningNotShallow = (lastestDeps, hookName: string) => {
if (process.env.NODE_ENV !== 'production') {
const originName = `use${hookName.slice(10)}`;
if (!lastestDeps || !lastestDeps.length) {
return console.warn(`\`${hookName}\` should not be used with no dependencies. Use React.${originName} instead.`);
}

if (lastestDeps.every(isPrimitive)) {
return console.warn(
`\`${hookName}\` should not be used with dependencies that are all primitive values. Use React.${originName} instead.`
);
}
}
};

const useShallowEffect = function(cb: EffectCallback, deps: DependencyList) {
warningNotShallow(deps, 'useShallowEffect');
return React.useEffect(cb, useShallowDeps(deps));
};
const useShallowLayoutEffect = function(cb: EffectCallback, deps: any[]) {
warningNotShallow(deps, 'useShallowLayoutEffect');
return React.useLayoutEffect(cb, useShallowDeps(deps));
};
const useShallowMemo = function<T>(cb: () => T, deps: any[]) {
warningNotShallow(deps, 'useShallowMemo');
return React.useMemo<T>(cb, useShallowDeps(deps));
};
const useShallowCallback = function(cb: (...args: any[]) => any, deps: any[]) {
warningNotShallow(deps, 'useShallowCallback');
return React.useCallback(cb, useShallowDeps(deps));
};
const useShallowImperativeHandle = function<T, R extends T>(ref: Ref<T> | undefined, cb: () => R, deps: any[]) {
warningNotShallow(deps, 'useShallowImperativeHandle');
return React.useImperativeHandle(ref, cb, useShallowDeps(deps));
};

Expand Down
13 changes: 7 additions & 6 deletions src/useShallowDeps.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffect, useRef, DependencyList } from 'react';
import { useRef, DependencyList } from 'react';
import shallowEquals from './shallowEqual';

export default function useShallowDeps(lastestDeps: DependencyList) {
const depsRef = useRef(lastestDeps);
const deps = shallowEquals(lastestDeps, depsRef.current) ? depsRef.current : lastestDeps;
useEffect(() => {
depsRef.current = deps;
}, [deps]);
return deps;

if (!shallowEquals(depsRef.current, lastestDeps)) {
depsRef.current = lastestDeps;
}

return depsRef.current;
}

0 comments on commit da6f6aa

Please sign in to comment.