@arthurgeron/eslint-plugin-react-usememo
Advanced tools
Comparing version 1.0.1 to 1.1.0--beta0
@@ -45,2 +45,3 @@ 'use strict'; | ||
MemoStatus[MemoStatus["UnmemoizedOther"] = 7] = "UnmemoizedOther"; | ||
MemoStatus[MemoStatus["UnsafeLet"] = 8] = "UnsafeLet"; | ||
})(MemoStatus || (MemoStatus = {})); | ||
@@ -78,7 +79,3 @@ function isComponentName(name) { | ||
if (node.parent.kind === "let") { | ||
context.report({ node: node, messageId: "usememo-const" }); | ||
if (!node.init) { | ||
// Rely on usememo-const reported error to fail this identifier | ||
return MemoStatus.Memoized; | ||
} | ||
return MemoStatus.UnsafeLet; | ||
} | ||
@@ -182,11 +179,29 @@ return getExpressionMemoStatus(context, node.init); | ||
/*! ***************************************************************************** | ||
Copyright (c) Microsoft Corporation. | ||
Permission to use, copy, modify, and/or distribute this software for any | ||
purpose with or without fee is hereby granted. | ||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | ||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | ||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | ||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | ||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR | ||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | ||
PERFORMANCE OF THIS SOFTWARE. | ||
***************************************************************************** */ | ||
var __assign = function() { | ||
__assign = Object.assign || function __assign(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var _a, _b, _c, _d; | ||
var ValidExpressions = { | ||
'ArrowFunctionExpression': true, | ||
'ObjectExpression': true, | ||
'ArrayExpression': true, | ||
'LogicalExpression': true, | ||
'Identifier': true, | ||
'JSXEmptyExpression': false | ||
}; | ||
var jsxEmptyExpressionClassData = (_a = {}, | ||
@@ -199,2 +214,3 @@ _a[MemoStatus.UnmemoizedObject.toString()] = "object-class-memo-props", | ||
_a[MemoStatus.UnmemoizedOther.toString()] = "unknown-class-memo-props", | ||
_a[MemoStatus.UnsafeLet.toString()] = "usememo-const", | ||
_a); | ||
@@ -209,2 +225,3 @@ var jsxEmptyExpressionData = (_b = {}, | ||
_b[MemoStatus.UnmemoizedJSX.toString()] = "jsx-usememo-props", | ||
_b[MemoStatus.UnsafeLet.toString()] = "usememo-const", | ||
_b); | ||
@@ -219,2 +236,3 @@ var hookReturnExpressionData = (_c = {}, | ||
_c[MemoStatus.UnmemoizedJSX.toString()] = "jsx-usememo-hook", | ||
_c[MemoStatus.UnsafeLet.toString()] = "usememo-const", | ||
_c); | ||
@@ -229,3 +247,25 @@ var callExpressionData = (_d = {}, | ||
_d[MemoStatus.UnmemoizedJSX.toString()] = "jsx-usememo-deps", | ||
_d[MemoStatus.UnsafeLet.toString()] = "usememo-const", | ||
_d); | ||
var defaultReactHookNames = { | ||
"useContext": true, | ||
"useState": true, | ||
"useReducer": true, | ||
"useRef": true, | ||
"useLayoutEffect": true, | ||
"useEffect": true, | ||
"useImperativeHandle": true, | ||
"useCallback": true, | ||
"useMemo": true, | ||
"useDebugValue": true, | ||
"useDeferredValue": true, | ||
"useTransition": true, | ||
"useId": true, | ||
"useInsertionEffect": true, | ||
"useSyncExternalStore": true, | ||
"useQuery": true, | ||
"useMutation": true, | ||
"useQueryClient": true, | ||
"useInfiniteQuery": true | ||
}; | ||
@@ -256,3 +296,3 @@ var MessagesRequireUseMemo = { | ||
"unknown-usememo-deps": "Unknown value may need to be wrapped in useMemo() when used as a hook dependency", | ||
"usememo-const": "useMemo/useCallback return value should be assigned to a const to prevent reassignment" | ||
"usememo-const": "useMemo/useCallback return value should be assigned to a `const` to prevent reassignment" | ||
}; | ||
@@ -266,5 +306,11 @@ var MessagesRequireUseMemoChildren = { | ||
"unknown-usememo-children": "Unknown value may need to be wrapped in React.useMemo() when used as children", | ||
"usememo-const": "useMemo/useCallback return value should be assigned to a const to prevent reassignment" | ||
"usememo-const": "useMemo/useCallback return value should be assigned to a `const` to prevent reassignment" | ||
}; | ||
function shouldIgnoreNode(node, ignoredNames) { | ||
var _a, _b; | ||
return !!ignoredNames[node === null || node === void 0 ? void 0 : node.name] | ||
|| !!ignoredNames[node.callee.name] | ||
|| !!ignoredNames[(_b = (_a = node === null || node === void 0 ? void 0 : node.callee) === null || _a === void 0 ? void 0 : _a.property) === null || _b === void 0 ? void 0 : _b.name]; | ||
} | ||
function checkForErrors(data, expressionType, context, node, report) { | ||
@@ -306,3 +352,3 @@ var _a, _b; | ||
type: "object", | ||
properties: { strict: { type: "boolean" }, checkHookReturnObject: { type: "boolean" } }, | ||
properties: { strict: { type: "boolean" }, checkHookReturnObject: { type: "boolean" }, checkHookCalls: { type: "boolean" }, ignoredHookCallsNames: { type: "object" } }, | ||
additionalProperties: false | ||
@@ -348,7 +394,10 @@ }, | ||
ReturnStatement: function (node) { | ||
var _a, _b; | ||
if (node.parent.parent.type === 'FunctionDeclaration' && getIsHook(node.parent.parent.id) && node.argument) { | ||
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; | ||
var functionDeclarationNode = ((_b = (_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent) === null || _b === void 0 ? void 0 : _b.type) === 'FunctionDeclaration' && ((_d = (_c = node === null || node === void 0 ? void 0 : node.parent) === null || _c === void 0 ? void 0 : _c.parent) === null || _d === void 0 ? void 0 : _d.id); | ||
var anonFuncVariableDeclarationNode = ((_f = (_e = node.parent) === null || _e === void 0 ? void 0 : _e.parent) === null || _f === void 0 ? void 0 : _f.type) === 'ArrowFunctionExpression' && ((_j = (_h = (_g = node === null || node === void 0 ? void 0 : node.parent) === null || _g === void 0 ? void 0 : _g.parent) === null || _h === void 0 ? void 0 : _h.parent) === null || _j === void 0 ? void 0 : _j.type) === 'VariableDeclarator' && ((_m = (_l = (_k = node === null || node === void 0 ? void 0 : node.parent) === null || _k === void 0 ? void 0 : _k.parent) === null || _l === void 0 ? void 0 : _l.parent) === null || _m === void 0 ? void 0 : _m.id); | ||
var validNode = functionDeclarationNode || anonFuncVariableDeclarationNode; | ||
if (validNode && getIsHook(validNode) && node.argument) { | ||
if (node.argument.type === 'ObjectExpression') { | ||
if ((_b = (_a = context.options) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.checkHookReturnObject) { | ||
context.report({ node: node, messageId: "object-usememo-hook" }); | ||
if ((_p = (_o = context.options) === null || _o === void 0 ? void 0 : _o[0]) === null || _p === void 0 ? void 0 : _p.checkHookReturnObject) { | ||
report(node, "object-usememo-hook"); | ||
return; | ||
@@ -366,12 +415,14 @@ } | ||
CallExpression: function (node) { | ||
var _a, _b, _c, _d, _e; | ||
var callee = node.callee; | ||
if (!getIsHook(callee)) | ||
var ignoredNames = __assign(__assign({}, defaultReactHookNames), ((_c = (_b = (_a = context.options) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.ignoredHookCallsNames) !== null && _c !== void 0 ? _c : {})); | ||
if (((_e = (_d = context.options) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.checkHookCalls) === false | ||
|| !getIsHook(callee)) { | ||
return; | ||
var _a = node.arguments, dependencies = _a[1]; | ||
if (dependencies !== undefined && | ||
dependencies.type === "ArrayExpression") { | ||
for (var _i = 0, _b = dependencies.elements; _i < _b.length; _i++) { | ||
var dep = _b[_i]; | ||
if (dep !== null && ValidExpressions[dep.type]) { | ||
checkForErrors(callExpressionData, getExpressionMemoStatus(context, dep), context, node, report); | ||
} | ||
if (!shouldIgnoreNode(node, ignoredNames)) { | ||
for (var _i = 0, _f = node.arguments; _i < _f.length; _i++) { | ||
var argument = _f[_i]; | ||
if (argument.type !== 'SpreadElement') { | ||
checkForErrors(callExpressionData, getExpressionMemoStatus(context, argument), context, node, report); | ||
} | ||
@@ -378,0 +429,0 @@ } |
{ | ||
"name": "@arthurgeron/eslint-plugin-react-usememo", | ||
"version": "1.0.1", | ||
"version": "1.1.0--beta0", | ||
"description": "", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
325
README.md
@@ -42,37 +42,42 @@ # eslint-plugin-react-usememo | ||
- `{strict: true}`: Fails even in cases where it is difficult to determine if the value in question is a primitive (string or number) or a complex value (object, array, etc.); | ||
- `{checkHookReturnObject: true}`: Will require Object Expressions passed in return statements (e.g. `return {someFunc}`) to also be memoised (e.g. `return useMemo(() => ({someFunc}), [someFunc])`); **Disabled** by default. | ||
- `{checkHookReturnObject: true}`: Will require Object Expressions passed in return statements (e.g. `return {someFunc}`) to also be memoised (e.g. `return useMemo(() => ({someFunc}), [someFunc])`); **Disabled** by default; | ||
- `{checkHookCalls: true}`: Will require objects/data passed to a non-native/Custom hook to be memoized (e.g. in this code `const data = useParse(unparsedData)` it will check `unparsedData` for memo status), this check can be ignored by a hook/name basis, check next item for details; **Enabled** by default; | ||
- `{ignoredHookCallsNames: Record<string, boolean>}`: You can add specific hooks names here, individually disabling or enabling them to be checked when used, e.g. if you have a custom hook named `useX` and it should be called with unmemoized parameters you can set `{ ignoredHookCallsNames: { useX: false } }` and then have somethning like `const data = {}; const x = useX(data);` not generate any errors. | ||
### Function Components | ||
#### **Incorrect** | ||
```JavaScript | ||
function Component() { | ||
function Component() { | ||
const [data, setData] = useState([]); | ||
// This will be redeclared each render | ||
function renderItem({ item }) { | ||
return (<Text>item.name</Text>); | ||
const [data, setData] = useState([]); | ||
// This will be redeclared each render | ||
function renderItem({ item }) { | ||
return (<Text>item.name</Text>); | ||
} | ||
// Data isn't redeclared each ender but `[]` is | ||
return (<FlatList renderItem={renderItem} data={data ?? []} />); | ||
} | ||
// Data isn't redeclared each ender but `[]` is | ||
return (<FlatList renderItem={renderItem} data={data ?? []} />); | ||
} | ||
``` | ||
#### **Correct** | ||
```JavaScript | ||
// Has no dynamic dependencies therefore should be static, will be declared only once. | ||
function renderItem({ item }) { | ||
return <Text>item.name</Text>; | ||
} | ||
// Has no dynamic dependencies therefore should be static, will be declared only once. | ||
function renderItem({ item }) { | ||
return <Text>item.name</Text>; | ||
} | ||
const EMPTY_ARRAY = []; | ||
const EMPTY_ARRAY = []; | ||
function Component() { | ||
function Component() { | ||
const [data, setData] = useState(EMPTY_ARRAY); | ||
// Will only render again if data changes | ||
return (<FlatList renderItem={renderItem} data={data ?? EMPTY_ARRAY} />); | ||
} | ||
const [data, setData] = useState(EMPTY_ARRAY); | ||
// Will only render again if data changes | ||
return (<FlatList renderItem={renderItem} data={data ?? EMPTY_ARRAY} />); | ||
} | ||
``` | ||
@@ -82,34 +87,34 @@ ### Class Components | ||
```JavaScript | ||
class Component() { | ||
class Component() { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
data: undefined, | ||
propDrivenData: props., | ||
}; | ||
} | ||
// This will NOT be redeclared each render | ||
getItemName(item) { | ||
return item.name; | ||
} | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
data: undefined, | ||
propDrivenData: props., | ||
}; | ||
} | ||
// This will NOT be redeclared each render | ||
getItemName(item) { | ||
return item.name; | ||
} | ||
render() { | ||
// This function will be redeclared each render | ||
function renderItem({ item }) { | ||
return (<Text>{this.getItemName(item)}</Text>); | ||
render() { | ||
// This function will be redeclared each render | ||
function renderItem({ item }) { | ||
return (<Text>{this.getItemName(item)}</Text>); | ||
} | ||
// Data isn't redeclared each ender but [] is | ||
// Extradata has a exponential complexity (will iterate the entire array for each render, could render once or several times in a second) | ||
// Outcome will be that any new render on this component will cause the entire FlatList to render again, including children components, even if the data hasn't changed. | ||
return (<FlatList | ||
renderItem={renderItem} | ||
data={data ?? []} | ||
extraData={dataArray.filter(id => !!id)} | ||
/>); | ||
} | ||
// Data isn't redeclared each ender but [] is | ||
// Extradata has a exponential complexity (will iterate the entire array for each render, could render once or several times in a second) | ||
// Outcome will be that any new render on this component will cause the entire FlatList to render again, including children components, even if the data hasn't changed. | ||
return (<FlatList | ||
renderItem={renderItem} | ||
data={data ?? []} | ||
extraData={dataArray.filter(id => !!id)} | ||
/>); | ||
} | ||
} | ||
``` | ||
@@ -121,53 +126,53 @@ In the previous example there are two issues, a function and a object that will be dynamically redeclared each time the component renders, which will cause FlatList to keep re-rendering even when the input data hasn't changed. | ||
// Static therefore is only declared once | ||
const EMPTY_ARRAY = []; | ||
// Static therefore is only declared once | ||
const EMPTY_ARRAY = []; | ||
class Component() { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
data: undefined, | ||
propDrivenData: props.dataArray.filter(id => !!id), | ||
}; | ||
} | ||
// Properly regenerate state driven data only when props change instead of during each render | ||
static getDerivedStateFromProps(props) { | ||
if (props.propDrivenData !== this.props.propDrivenData) { | ||
return { | ||
class Component() { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
data: undefined, | ||
propDrivenData: props.dataArray.filter(id => !!id), | ||
}; | ||
} | ||
return null; | ||
} | ||
// Properly regenerate state driven data only when props change instead of during each render | ||
static getDerivedStateFromProps(props) { | ||
if (props.propDrivenData !== this.props.propDrivenData) { | ||
return { | ||
propDrivenData: props.dataArray.filter(id => !!id), | ||
}; | ||
} | ||
return null; | ||
} | ||
// Will be declared only once. | ||
getItemName({item}) { | ||
const { data } = this.state; | ||
const dataLength = data ? data.length : 0; | ||
return (<Text>{item.name} {dataLength}</Text>); | ||
} | ||
// Will be declared only once. | ||
getItemName({item}) { | ||
const { data } = this.state; | ||
const dataLength = data ? data.length : 0; | ||
return (<Text>{item.name} {dataLength}</Text>); | ||
} | ||
render() { | ||
const { data } = this.state; | ||
// Will only cause a new render if data changes | ||
return (<FlatList renderItem={this.renderItem} data={data ?? EMPTY_ARRAY} />); | ||
render() { | ||
const { data } = this.state; | ||
// Will only cause a new render if data changes | ||
return (<FlatList renderItem={this.renderItem} data={data ?? EMPTY_ARRAY} />); | ||
} | ||
} | ||
} | ||
``` | ||
#### **Correct** | ||
```JavaScript | ||
const EMPTY_ARRAY = []; | ||
const EMPTY_ARRAY = []; | ||
function Component() { | ||
const [data, setData] = useState(EMPTY_ARRAY); | ||
const [isEditing, setIsEditing] = useState(false); | ||
function Component() { | ||
const [data, setData] = useState(EMPTY_ARRAY); | ||
const [isEditing, setIsEditing] = useState(false); | ||
// Has dynamic dependencies but will only be re-declared when isEditing or the input data changes | ||
const renderItem = useCallback(({ item }) => { | ||
return (<Text>{isEditing ? 'item.name' : 'Editing'}</Text>); | ||
}, [isEditing]); | ||
// Has dynamic dependencies but will only be re-declared when isEditing or the input data changes | ||
const renderItem = useCallback(({ item }) => { | ||
return (<Text>{isEditing ? 'item.name' : 'Editing'}</Text>); | ||
}, [isEditing]); | ||
return (<FlatList renderItem={renderItem} data={data ?? EMPTY_ARRAY} />); | ||
} | ||
return (<FlatList renderItem={renderItem} data={data ?? EMPTY_ARRAY} />); | ||
} | ||
``` | ||
@@ -179,24 +184,37 @@ ### Hooks | ||
```JavaScript | ||
function useData() { | ||
function useData() { | ||
let otherData = {}; | ||
function getData() { | ||
// Doing something | ||
let otherData = {}; | ||
function getData() {} | ||
return { getData, otherData}; | ||
} | ||
return { getData, otherData}; | ||
} | ||
``` | ||
#### **Incorrect** (checkHookReturnObject: true) | ||
```JavaScript | ||
function getData() { | ||
// Doing something | ||
function getData() {} | ||
function useData() { | ||
return { getData }; | ||
} | ||
function useData() { | ||
return { getData }; | ||
} | ||
``` | ||
#### **Incorrect** Calling a custom hook | ||
```JavaScript | ||
function doSomething() {} | ||
function useStateManagement(someFlag) { | ||
useEffect(() => { | ||
doSomething(someFlag); | ||
}, [someFlag]); | ||
} | ||
function useData() { | ||
const someFlag = {}; | ||
// Some flag is memoized, so not unwanted side effects are generated in the other hook | ||
const data = useStateManagement(someFlag); | ||
} | ||
``` | ||
#### **Correct** | ||
@@ -206,7 +224,7 @@ ```JavaScript | ||
function useData() { | ||
const getData = useCallback(() => { // Or declare statically | ||
}, []); | ||
return {getData, otherData}; | ||
} | ||
function useData() { | ||
const getData = useCallback(() => { // Or declare statically | ||
}, []); | ||
return {getData, otherData}; | ||
} | ||
``` | ||
@@ -219,9 +237,52 @@ | ||
} | ||
function useData() { | ||
return useMemo(() => ({ getData }), []); | ||
} | ||
function useData() { | ||
return useMemo(() => ({ getData }), []); | ||
} | ||
``` | ||
#### **Correct** Passing a dependency to a hook | ||
```JavaScript | ||
function getData() {} | ||
function useData() { | ||
return useMemo(() => ({ getData }), []); | ||
} | ||
``` | ||
#### **Correct** Calling a custom hook | ||
```JavaScript | ||
function doSomething() {} | ||
function useStateManagement(someFlag) { | ||
useEffect(() => { | ||
doSomething(someFlag); | ||
}, [someFlag]); | ||
} | ||
function useData() { | ||
const someFlag = useMemo(() => ({}), []); | ||
// Some flag is memoized, so not unwanted side effects are generated in the other hook | ||
const data = useStateManagement(someFlag); | ||
} | ||
``` | ||
#### **"Correct"** Calling a custom hook (checkHookCalls: true) or (ignoredHookCallsNames: { useStateManagement: false }) | ||
```JavaScript | ||
function doSomething() { | ||
} | ||
function useStateManagement(someFlag) { | ||
useEffect(() => { | ||
doSomething(someFlag); | ||
}, [someFlag]); | ||
} | ||
function useData() { | ||
const someFlag = useMemo(() => ({}), []); | ||
// Some flag is regenerated each render, hence useEffect in "useStateManagement" will execute a new cycle each render | ||
// Either way this will not generate a error/warning because of the options passed | ||
const data = useStateManagement(someFlag); | ||
} | ||
``` | ||
## `require-memo` | ||
@@ -234,5 +295,5 @@ | ||
```JavaScript | ||
export default function Component() { | ||
return (<Text>This is a component</Text>); | ||
} | ||
export default function Component() { | ||
return (<Text>This is a component</Text>); | ||
} | ||
``` | ||
@@ -242,5 +303,5 @@ | ||
```JavaScript | ||
export default memo(function Component() { | ||
return (<Text>This is a component</Text>); | ||
}); | ||
export default memo(function Component() { | ||
return (<Text>This is a component</Text>); | ||
}); | ||
``` | ||
@@ -258,10 +319,10 @@ | ||
```JavaScript | ||
function Component() { | ||
function Component() { | ||
return (<View> | ||
<> | ||
<OtherComponent /> | ||
</> | ||
</View>); | ||
} | ||
return (<View> | ||
<> | ||
<OtherComponent /> | ||
</> | ||
</View>); | ||
} | ||
``` | ||
@@ -271,9 +332,9 @@ | ||
```JavaScript | ||
function Component() { | ||
const children = useMemo(() => (<OtherComponent />), []); | ||
return (<View> | ||
{children} | ||
</View>); | ||
} | ||
function Component() { | ||
const children = useMemo(() => (<OtherComponent />), []); | ||
return (<View> | ||
{children} | ||
</View>); | ||
} | ||
``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
35722
481
331
2