Unverified Commit 2346b7a3 authored by Tasso Evangelista's avatar Tasso Evangelista Committed by GitHub
Browse files

refactor: Hooks in TypeScript (#240)

parent f024a6db
const path = require('path');
module.exports = {
extends: ['@rocket.chat/eslint-config'],
plugins: ['react'],
parser: 'babel-eslint',
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'@rocket.chat/eslint-config',
],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
warnOnUnsupportedTypeScriptVersion: false,
ecmaFeatures: {
experimentalObjectRestSpread: true,
legacyDecorators: true,
},
},
plugins: ['@typescript-eslint', 'react-hooks'],
rules: {
indent: ['error', 2],
'func-call-spacing': 'off',
'indent': 'off',
'import/order': ['error', {
'newlines-between': 'always',
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']]
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
}],
'no-useless-constructor': 'off',
'no-empty-function': 'off',
'no-spaced-func': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/func-call-spacing': ['error'],
'@typescript-eslint/indent': ['error', 2],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': ['warn', {
allowExpressions: true,
}],
'jsx-quotes': ['error', 'prefer-single'],
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/jsx-no-undef': 'error',
'react/jsx-fragments': ['error', 'syntax'],
},
env: {
browser: true,
commonjs: true,
es6: true,
node: true,
jest: true
},
settings: {
react: {
version: 'detect',
'import/resolver': {
node: {
extensions: [
'.js',
'.ts',
'.tsx'
],
},
},
},
'env': {
'jest': true,
},
};
[ignore]
[include]
[libs]
flow-typed
[lints]
[options]
esproposal.optional_chaining=enable
[strict]
import React, { useReducer, Component, createElement } from 'react';
import ReactDOM, { render, unmountComponentAtNode } from 'react-dom';
import { renderToString } from 'react-dom/server';
import { act } from 'react-dom/test-utils';
export const runHooks = (fn, mutations = []) => {
let returnedValue;
let forceUpdate;
function FunctionalComponent() {
[, forceUpdate] = useReducer((state) => !state, false);
returnedValue = fn();
return null;
}
let errorThrown;
class ComponentWithErrorBoundary extends Component {
state = { errored: false }
static getDerivedStateFromError = () => ({ errored: true })
componentDidCatch = (error) => {
errorThrown = error;
}
render = () => (this.state.errored ? null : createElement(FunctionalComponent))
}
const spy = jest.spyOn(console, 'error');
spy.mockImplementation(() => {});
const div = document.createElement('div');
render(createElement(ComponentWithErrorBoundary), div);
const values = [returnedValue];
for (const mutation of mutations) {
act(() => {
forceUpdate();
if (mutation === true) {
return;
}
mutation(returnedValue);
});
values.push(returnedValue);
}
unmountComponentAtNode(div);
if (errorThrown) {
throw errorThrown;
}
return values;
};
export const runHooksOnServer = (fn) => {
let returnedValue;
function FunctionalComponent() {
returnedValue = fn();
return null;
}
renderToString(<FunctionalComponent />);
return returnedValue;
};
......@@ -29,13 +29,13 @@ yarn test
- [useAutoFocus](#useautofocus)
- [Parameters](#parameters)
- [useDebouncedUpdates](#usedebouncedupdates)
- [useDebouncedCallback](#usedebouncedcallback)
- [Parameters](#parameters-1)
- [useDebouncedReducer](#usedebouncedreducer)
- [Parameters](#parameters-2)
- [useDebouncedState](#usedebouncedstate)
- [Parameters](#parameters-3)
- [useDebouncedCallback](#usedebouncedcallback)
- [useDebouncedUpdates](#usedebouncedupdates)
- [Parameters](#parameters-4)
- [useDebouncedValue](#usedebouncedvalue)
- [Parameters](#parameters-5)
......@@ -53,6 +53,7 @@ yarn test
- [Parameters](#parameters-11)
- [useToggle](#usetoggle)
- [Parameters](#parameters-12)
- [useUniqueId](#useuniqueid)
### useAutoFocus
......@@ -60,23 +61,22 @@ Hook to automatically request focus for an DOM element.
#### Parameters
- `isFocused` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** if true, the focus will be requested (optional, default `true`)
- `options` **FocusOptions** options of the focus request
- `isFocused` if true, the focus will be requested (optional, default `true`)
- `options` **Options?** options of the focus request
Returns **{current: [HTMLElement](https://developer.mozilla.org/docs/Web/HTML/Element)?}** the ref which holds the element
Returns **Ref&lt;{focus: function (options: Options): void}>** the ref which holds the element
### useDebouncedUpdates
### useDebouncedCallback
Hook to debounce the state updater function returned by hooks like `useState()` and `useReducer()`.
Hook to memoize a debounced version of a callback.
#### Parameters
- `pair` **\[any, function (): any]** the state and updater pair which will be debounced
- `pair.0` the state value
- `pair.1` the state updater function
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of milliseconds to delay the updater
- `callback` **function (...args: P): any** the callback to debounce
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** the number of milliseconds to delay
- `deps` **DependencyList?** the hook dependencies
Returns **any** a state value and debounced updater pair
Returns **any** a memoized and debounced callback
### useDebouncedReducer
......@@ -84,12 +84,13 @@ Hook to create a reduced state with a debounced `dispatch()` function.
#### Parameters
- `reducer` **function (any, any): any** the reducer function
- `initializerArg` **any** the initial state value or the argument passed to the initial state generator function
- `initializer` **function (any): any** the initial state generator function
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of milliseconds to delay the updater
- `reducer` **R** the reducer function
- `initialArg` **I** the initial state value or the argument passed to the
initial state generator function
- `init` **function (arg: I): ReducerState&lt;R>** the initial state generator function
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** the number of milliseconds to delay the updater
Returns **any** a state and debounced `dispatch()` function
Returns **\[ReducerState&lt;R>, any]** a state and debounced `dispatch()` function
### useDebouncedState
......@@ -97,22 +98,23 @@ Hook to create a state with a debounced setter function.
#### Parameters
- `initialValue` **(any | function (): any)** the initial state value or the initial state generator function
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of milliseconds to delay the updater
- `initialValue` **(S | function (): S)** the initial state value or the initial state generator function
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** the number of milliseconds to delay the updater
Returns **any** a state and debounced setter function
Returns **\[S, any]** a state and debounced setter function
### useDebouncedCallback
### useDebouncedUpdates
Hook to memoize a debounced version of a callback.
Hook to debounce the state dispatcher function returned by hooks like `useState()` and `useReducer()`.
#### Parameters
- `callback` **function (): any** the callback to debounce
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** the number of milliseconds to delay
- `deps` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)&lt;any>?** the hook dependencies
- `pair` **\[S, DispatchWithoutAction]** the state and dispatcher pair which will be debounced
- `pair.0` the state value
- `pair.1` the state dispatcher function
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** the number of milliseconds to delay the dispatcher
Returns **function (): any** a memoized and debounced callback
Returns **\[S, any]** a state value and debounced dispatcher pair
### useDebouncedValue
......@@ -120,10 +122,10 @@ Hook to keep a debounced reference of a value.
#### Parameters
- `value` **any** the value to be debounced
- `value` **V** the value to be debounced
- `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** the number of milliseconds to delay
Returns **any** a debounced value
Returns **V** a debounced value
### useLazyRef
......@@ -131,7 +133,7 @@ Hook equivalent to useRef, but with a lazy initialization for computed value.
#### Parameters
- `initializer` **function (): T** the function the computes the ref value
- `init` the function the computes the ref value
Returns **any** the ref
......@@ -141,7 +143,7 @@ Hook to listen to a media query.
#### Parameters
- `query` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** the CSS3 media query expression
- `query` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** the CSS3 media query expression
Returns **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** `true` if the media query matches; `false` is it does not match or the query is not defined
......@@ -152,9 +154,9 @@ while receiving a forwared ref.
#### Parameters
- `refs` **...[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)&lt;Ref&lt;any>>** the refs and callback refs that should be merged
- `refs` **...[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)&lt;Ref&lt;T>>** the refs and callback refs that should be merged
Returns **any** a merged callback ref
Returns **RefCallback&lt;T>** a merged callback ref
### useMutableCallback
......@@ -162,9 +164,9 @@ Hook to create a stable callback from a mutable one.
#### Parameters
- `fn` **function (): any** the mutable callback
- `fn` **function (...args: P): T** the mutable callback
Returns **any** a stable callback
Returns **function (...args: P): T** a stable callback
### useResizeObserver
......@@ -172,23 +174,23 @@ Hook to track dimension changes in a DOM element using the ResizeObserver API.
#### Parameters
- `options` **UseResizeObserverOptions** (optional, default `{}`)
- `options` **Options** (optional, default `{}`)
- `options.debounceDelay` the number of milliseconds to delay updates
Returns **UseResizeObserverReturn** a triple containing the ref and the size information
Returns **{ref: RefObject&lt;[Element](https://developer.mozilla.org/docs/Web/API/Element)>, contentBoxSize: ResizeObserverSize, borderBoxSize: ResizeObserverSize}** a triple containing the ref and the size information
### useSafely
Hook that wraps pairs of state and updater to provide a new updater which
can be safe and asynchronically called even after the component unmounted.
Hook that wraps pairs of state and dispatcher to provide a new dispatcher
which can be safe and asynchronically called even after the component unmounted.
#### Parameters
- `pair` **\[any, function (): any]** the state and updater pair which will be patched
- `pair` **\[S, (Dispatch&lt;A> | DispatchWithoutAction)]** the state and dispatcher pair which will be patched
- `pair.0` the state value
- `pair.1` the state updater function
- `pair.1` the state dispatcher function
Returns **any** a state value and safe updater pair
Returns **\[S, D]** a state value and safe dispatcher pair
### useToggle
......@@ -196,9 +198,15 @@ Hook to create a toggleable boolean state.
#### Parameters
- `initialValue` **(any | function (): any)** the initial value or the initial state generator function
- `initialValue` **([boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean) | function (): [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean))?** the initial value or the initial state generator function
Returns **\[[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean), D]** a state boolean value and a state toggler function
### useUniqueId
Hook to keep a unique ID string.
Returns **any** a state boolean value and a state toggler function
Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** the unique ID string
## Author
......
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-flow',
],
plugins: [
'@babel/plugin-proposal-class-properties',
],
};
// flow-typed signature: 4daa25492655417e7c0763d1d0b30fbb
// flow-typed version: c6154227d1/invariant_v2.x.x/flow_>=v0.104.x
declare module invariant {
declare module.exports: (condition: boolean, message: string) => void;
}
module.exports = {
preset: 'ts-jest',
errorOnDeprecated: true,
testMatch: [
'**/src/**/*.spec.[jt]s?(x)',
],
};
......@@ -21,40 +21,42 @@
"fuselage",
"rocketchat"
],
"source": "src/index.ts",
"main": "dist/index.js",
"module": "dist/index.module.js",
"unpkg": "dist/index.umd.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"start": "rollup -c -w",
"build": "rollup -c",
"start": "microbundle watch",
"build": "microbundle",
"test": "jest",
"lint": "eslint src tests",
"lint": "eslint --ext js,ts,tsx src",
"lint-staged": "lint-staged",
"docs": "documentation readme src/index.js --section='API Reference' --readme-file README.md"
"docs": "documentation readme 'src/{,**/!(__mocks__)/**/}!(*.spec).ts' --parse-extension=ts --resolve=node --section='API Reference' --readme-file README.md"
},
"devDependencies": {
"@babel/core": "^7.6.4",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.6",
"@babel/preset-flow": "^7.0.0",
"@rocket.chat/eslint-config": "^0.4.0",
"babel-eslint": "^10.0.3",
"documentation": "^12.1.2",
"@types/jest": "^25.2.3",
"@types/react-dom": "^16.9.8",
"@types/resize-observer-browser": "^0.1.3",
"@typescript-eslint/eslint-plugin": "^3.1.0",
"@typescript-eslint/parser": "^3.1.0",
"documentation": "^13.0.1",
"eslint": "^6.5.1",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-react": "^7.16.0",
"flow-bin": "^0.121.0",
"eslint-plugin-react-hooks": "^4.0.4",
"invariant": "^2.2.4",
"jest": "^25.1.0",
"jest": "^26.0.1",
"lint-staged": "^10.0.8",
"microbundle": "^0.12.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"rollup": "^2.1.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-peer-deps-external": "^2.2.2"
"ts-jest": "^26.1.0",
"typescript": "^3.9.5"
},
"peerDependencies": {
"invariant": "^2.2.4",
......
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import external from 'rollup-plugin-peer-deps-external';
import resolve from 'rollup-plugin-node-resolve';
import pkg from './package.json';
export default {
input: 'src/index.js',
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true,
},
],
plugins: [
external(),
babel(),
resolve(),
commonjs(),
],
};
import { CSSProperties } from 'react';
export const createBoxSizes = (style: CSSProperties): {
borderBoxSize: ResizeObserverSize;
contentBoxSize: ResizeObserverSize;
} => {
const getSizeInPixels = (value: string | number | undefined): number =>
(typeof value === 'string' && parseInt(value, 10))
|| (typeof value === 'number' && value)
|| 0;
const inlineSize = getSizeInPixels(style.inlineSize);
const borderInlineStartWidth = getSizeInPixels(style.borderInlineStartWidth);
const borderInlineEndWidth = getSizeInPixels(style.borderInlineEndWidth);
const paddingInlineStart = getSizeInPixels(style.paddingInlineStart);
const paddingInlineEnd = getSizeInPixels(style.paddingInlineEnd);
const blockSize = getSizeInPixels(style.blockSize);
const borderBlockStartWidth = getSizeInPixels(style.borderBlockStartWidth);
const borderBlockEndWidth = getSizeInPixels(style.borderBlockEndWidth);
const paddingBlockStart = getSizeInPixels(style.paddingBlockStart);
const paddingBlockEnd = getSizeInPixels(style.paddingBlockEnd);
const inlineExtra = borderInlineStartWidth + paddingInlineStart + paddingInlineEnd + borderInlineEndWidth;
const blockExtra = borderBlockStartWidth + paddingBlockStart + paddingBlockEnd + borderBlockEndWidth;
const borderBoxSize = Object.freeze({
inlineSize: inlineSize + (style.boxSizing === 'border-box' ? 0 : inlineExtra),
blockSize: blockSize + (style.boxSizing === 'border-box' ? 0 : blockExtra),
});
const contentBoxSize = Object.freeze({
inlineSize: inlineSize - (style.boxSizing === 'border-box' ? 0 : inlineExtra),
blockSize: blockSize - (style.boxSizing === 'border-box' ? 0 : blockExtra),
});
return {
borderBoxSize,
contentBoxSize,
};
};
export class ResizeObserverMock implements ResizeObserver {
callback: ResizeObserverCallback = () => undefined
contentRect: DOMRectReadOnly = {
bottom: undefined,
left: undefined,
right: undefined,
top: undefined,
x: undefined,
y: undefined,
width: undefined,
height: undefined,
toJSON: () => this.contentRect,
}
mutationObservers: Map<Element, MutationObserver> = new Map()
constructor(callback: ResizeObserverCallback) {
this.callback = callback;
}
disconnect = jest.fn((): void => {
this.callback = () => undefined;
})
observe = jest.fn((target: Element) => {
const mutationObserver = new MutationObserver((mutations: MutationRecord[]) => {
for (const mutation of mutations) {
if (!(mutation.target instanceof Element)) {
return;
}
const styles = getComputedStyle(mutation.target);
const {
borderBoxSize,
contentBoxSize,
} = createBoxSizes({
boxSizing: styles.boxSizing === 'border-box' ? 'border-box' : 'content-box',
inlineSize: styles.inlineSize,
borderInlineStartWidth: styles.borderInlineStartWidth ? parseInt(styles.borderInlineStartWidth, 10) : 0,
borderInlineEndWidth: styles.borderInlineEndWidth ? parseInt(styles.borderInlineEndWidth, 10) : 0,
paddingInlineStart: styles.paddingInlineStart ? parseInt(styles.paddingInlineStart, 10) : 0,
paddingInlineEnd: styles.paddingInlineEnd ? parseInt(styles.paddingInlineEnd, 10) : 0,
blockSize: styles.blockSize ? parseInt(styles.blockSize, 10) : 0,
borderBlockStartWidth: styles.borderBlockStartWidth ? parseInt(styles.borderBlockStartWidth, 10) : 0,
borderBlockEndWidth: styles.borderBlockEndWidth ? parseInt(styles.borderBlockEndWidth, 10) : 0,
paddingBlockStart: styles.paddingBlockStart ? parseInt(styles.paddingBlockStart, 10) : 0,
paddingBlockEnd: styles.paddingBlockEnd ? parseInt(styles.paddingBlockEnd, 10) : 0,
});
this.callback([{
target,
contentRect: this.contentRect,
borderBoxSize,
contentBoxSize,
}], this);
}
});
mutationObserver.observe(target, { attributes: true, attributeFilter: ['style'] });
this.mutationObservers.set(target, mutationObserver);
const t = target as HTMLElement;
t.style.inlineSize = `${ t.style.inlineSize };`;
})
unobserve = jest.fn((target: Element) => {
this.mutationObservers.get(target).disconnect();
this.mutationObservers.delete(target);
})
}
export default ResizeObserverMock;
import { act } from 'react-dom/test-utils';
export const mediaQueryLists = new Set<MediaQueryList>();
class MediaQueryListMock implements MediaQueryList {
_matches: boolean