@arthurgeron/eslint-plugin-react-usememo
Advanced tools
Comparing version 1.1.4 to 2.0.0
@@ -40,2 +40,71 @@ 'use strict'; | ||
var _a, _b, _c, _d; | ||
var jsxEmptyExpressionClassData = (_a = {}, | ||
_a[MemoStatus.UnmemoizedObject.toString()] = "object-class-memo-props", | ||
_a[MemoStatus.UnmemoizedArray.toString()] = "array-class-memo-props", | ||
_a[MemoStatus.UnmemoizedNew.toString()] = "instance-class-memo-props", | ||
_a[MemoStatus.UnmemoizedFunction.toString()] = 'instance-class-memo-props', | ||
_a[MemoStatus.UnmemoizedFunctionCall.toString()] = "unknown-class-memo-props", | ||
_a[MemoStatus.UnmemoizedOther.toString()] = "unknown-class-memo-props", | ||
_a[MemoStatus.UnsafeLet.toString()] = "usememo-const", | ||
_a); | ||
var jsxEmptyExpressionData = (_b = {}, | ||
_b[MemoStatus.UnmemoizedObject.toString()] = "object-usememo-props", | ||
_b[MemoStatus.UnmemoizedArray.toString()] = "array-usememo-props", | ||
_b[MemoStatus.UnmemoizedNew.toString()] = "instance-usememo-props", | ||
_b[MemoStatus.UnmemoizedFunction.toString()] = "function-usecallback-props", | ||
_b[MemoStatus.UnmemoizedFunctionCall.toString()] = "unknown-usememo-props", | ||
_b[MemoStatus.UnmemoizedOther.toString()] = "unknown-usememo-props", | ||
_b[MemoStatus.UnmemoizedJSX.toString()] = "jsx-usememo-props", | ||
_b[MemoStatus.UnsafeLet.toString()] = "usememo-const", | ||
_b); | ||
var hookReturnExpressionData = (_c = {}, | ||
_c[MemoStatus.UnmemoizedObject.toString()] = "object-usememo-hook", | ||
_c[MemoStatus.UnmemoizedArray.toString()] = "array-usememo-hook", | ||
_c[MemoStatus.UnmemoizedNew.toString()] = "instance-usememo-hook", | ||
_c[MemoStatus.UnmemoizedFunction.toString()] = "function-usecallback-hook", | ||
_c[MemoStatus.UnmemoizedFunctionCall.toString()] = "unknown-usememo-hook", | ||
_c[MemoStatus.UnmemoizedOther.toString()] = "unknown-usememo-hook", | ||
_c[MemoStatus.UnmemoizedJSX.toString()] = "jsx-usememo-hook", | ||
_c[MemoStatus.UnsafeLet.toString()] = "usememo-const", | ||
_c); | ||
var callExpressionData = (_d = {}, | ||
_d[MemoStatus.UnmemoizedObject.toString()] = "object-usememo-deps", | ||
_d[MemoStatus.UnmemoizedArray.toString()] = "array-usememo-deps", | ||
_d[MemoStatus.UnmemoizedNew.toString()] = "instance-usememo-deps", | ||
_d[MemoStatus.UnmemoizedFunction.toString()] = "function-usecallback-deps", | ||
_d[MemoStatus.UnmemoizedFunctionCall.toString()] = "unknown-usememo-deps", | ||
_d[MemoStatus.UnmemoizedOther.toString()] = "unknown-usememo-deps", | ||
_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 | ||
}; | ||
var messageIdToHookDict = { | ||
'function-usecallback-props': 'useCallback', | ||
'function-usecallback-hook': 'useCallback', | ||
'function-usecallback-deps': 'useCallback', | ||
'object-usememo-props': 'useMemo', | ||
'usememo-const': 'useMemo' | ||
}; | ||
function shouldIgnoreNode(node, ignoredNames) { | ||
@@ -62,3 +131,4 @@ var _a, _b; | ||
if (node.type === "Identifier") { | ||
return node.name[0] === 'u' && node.name[1] === 's' && node.name[2] === 'e'; | ||
var name_1 = node.name; | ||
return name_1[0] === 'u' && name_1[1] === 's' && name_1[2] === 'e'; | ||
} | ||
@@ -68,3 +138,3 @@ else if (node.type === "MemberExpression" && | ||
getIsHook(node.property)) { | ||
var obj = node.object; | ||
var obj = node.object; // Utilizing Object destructuring | ||
return obj.type === "Identifier" && obj.name === "React"; | ||
@@ -76,2 +146,107 @@ } | ||
} | ||
// Helper function to find parent of a specified type. | ||
function findParentType(node, type) { | ||
var parent = node.parent; | ||
while (parent) { | ||
if (parent.type === type) | ||
return parent; | ||
parent = parent.parent; | ||
} | ||
return undefined; | ||
} | ||
function fixFunction(node, context, shouldSetName) { | ||
var _a, _b; | ||
var sourceCode = context.getSourceCode(); | ||
var body = node.body, _c = node.params, params = _c === void 0 ? [] : _c; | ||
var funcBody = sourceCode.getText(body); | ||
var funcParams = params.map(function (node) { return sourceCode.getText(node); }); | ||
var fixedCode = "React.useCallback((".concat(funcParams.join(', '), ") => ").concat(funcBody, ", [])").concat(shouldSetName ? ';' : ''); | ||
if (shouldSetName && ((_a = node === null || node === void 0 ? void 0 : node.id) === null || _a === void 0 ? void 0 : _a.name)) { | ||
var name_2 = (_b = node === null || node === void 0 ? void 0 : node.id) === null || _b === void 0 ? void 0 : _b.name; | ||
fixedCode = "const ".concat(name_2, " = ").concat(fixedCode); | ||
} | ||
return fixedCode; | ||
} | ||
function getSafeVariableName(context, name) { | ||
var tempVarPlaceholder = 'renameMe'; | ||
if (!getVariableInScope(context, name)) { | ||
return name; | ||
} | ||
if (!getVariableInScope(context, "_".concat(name))) { | ||
return "_".concat(name); | ||
} | ||
return tempVarPlaceholder; | ||
} | ||
// Eslint Auto-fix logic, functional components/hooks only | ||
function fixBasedOnMessageId(node, messageId, fixer, context) { | ||
var _a, _b, _c, _d; | ||
var sourceCode = context.getSourceCode(); | ||
var hook = messageIdToHookDict[messageId] || 'useMemo'; | ||
var isObjExpression = node.type === 'ObjectExpression'; | ||
var parentIsVariableDeclarator = node.parent.type === 'VariableDeclarator'; | ||
var isArrowFunctionExpression = node.type === 'ArrowFunctionExpression'; | ||
var isFunctionExpression = node.type === 'FunctionExpression'; | ||
var isCorrectableFunctionExpression = isFunctionExpression || (isArrowFunctionExpression && parentIsVariableDeclarator); | ||
// Determine what type of behavior to follow according to the error message | ||
switch (messageId) { | ||
case 'function-usecallback-props': | ||
case 'object-usememo-props': | ||
case 'usememo-const': { | ||
var sourceCode_1 = context.getSourceCode(); | ||
var variableDeclaration = node.type === 'VariableDeclaration' ? node : findParentType(node, 'VariableDeclaration'); | ||
var fixes = []; | ||
// Check if it is a hook being stored in let/var, change to const if so | ||
if ((variableDeclaration === null || variableDeclaration === void 0 ? void 0 : variableDeclaration.kind) !== 'const') { | ||
var tokens = sourceCode_1.getTokens(variableDeclaration); | ||
var letKeywordToken = tokens === null || tokens === void 0 ? void 0 : tokens[0]; | ||
if ((letKeywordToken === null || letKeywordToken === void 0 ? void 0 : letKeywordToken.value) !== 'const') { | ||
fixes.push(fixer.replaceTextRange(letKeywordToken.range, 'const')); | ||
} | ||
} | ||
// If it's an dynamic object - Add useMemo/Callback | ||
if ((isObjExpression || isCorrectableFunctionExpression)) { | ||
var fixed_1 = isCorrectableFunctionExpression ? fixFunction(node, context) : "React.useMemo(() => (".concat(sourceCode_1.getText(node), "), [])"); | ||
var parent_1 = node.parent; | ||
// Means we have a object expression declared directly in jsx | ||
if (parent_1.type === 'JSXExpressionContainer') { | ||
var parentPropName = (_b = (_a = parent_1 === null || parent_1 === void 0 ? void 0 : parent_1.parent) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.name.toString(); | ||
var newVarName = getSafeVariableName(context, parentPropName); | ||
var returnStatement = findParentType(node, 'ReturnStatement'); | ||
if (returnStatement) { | ||
var indentationLevel = sourceCode_1.lines[returnStatement.loc.start.line - 1].search(/\S/); | ||
var indentation = ' '.repeat(indentationLevel); | ||
// Creates a declaration for the variable and inserts it before the return statement | ||
fixes.push(fixer.insertTextBeforeRange(returnStatement.range, "const ".concat(newVarName, " = ").concat(fixed_1, ";\n").concat(indentation))); | ||
// Replaces the old inline object expression with the variable name | ||
fixes.push(fixer.replaceText(node, newVarName)); | ||
} | ||
} | ||
else { | ||
fixes.push(fixer.replaceText(node, fixed_1)); | ||
} | ||
} | ||
return !fixes.length ? null : fixes; | ||
} | ||
// Unknown cases are usually complex issues or false positives, so we ignore them | ||
case 'unknown-class-memo-props': | ||
case 'unknown-usememo-hook': | ||
case 'unknown-usememo-deps': | ||
case 'unknown-usememo-props': | ||
return null; | ||
} | ||
// Simpler cases bellow, all of them are just adding useMemo/Callback | ||
var fixed = "React.".concat(hook, "(() => ").concat(isObjExpression ? "(" : '').concat(sourceCode.getText(node)).concat(isObjExpression ? ")" : '', ", [])"); | ||
if (node.type === 'FunctionDeclaration') { | ||
var _node = node; | ||
if (_node && ((_c = _node === null || _node === void 0 ? void 0 : _node.id) === null || _c === void 0 ? void 0 : _c.type) === "Identifier") { | ||
fixed = fixFunction(_node, context, true); | ||
} | ||
} | ||
if ('computed' in node && ((_d = node === null || node === void 0 ? void 0 : node.computed) === null || _d === void 0 ? void 0 : _d.type) === 'ArrowFunctionExpression') { | ||
return fixer.replaceText(node.computed, fixed); | ||
} | ||
else { | ||
return fixer.replaceText(node, fixed); | ||
} | ||
} | ||
@@ -108,6 +283,9 @@ var componentNameRegex = /^[^a-z]/; | ||
} | ||
function getVariableInScope(context, name) { | ||
return context.getScope().variables.find(function (variable) { return variable.name === name; }); | ||
} | ||
function getIdentifierMemoStatus(context, _a) { | ||
var _b, _c, _d, _e, _f; | ||
var name = _a.name; | ||
var variableInScope = context.getScope().variables.find(function (v) { return v.name === name; }); | ||
var variableInScope = getVariableInScope(context, name); | ||
if (variableInScope === undefined) | ||
@@ -254,64 +432,2 @@ return { status: MemoStatus.Memoized }; | ||
var _a, _b, _c, _d; | ||
var jsxEmptyExpressionClassData = (_a = {}, | ||
_a[MemoStatus.UnmemoizedObject.toString()] = "object-class-memo-props", | ||
_a[MemoStatus.UnmemoizedArray.toString()] = "array-class-memo-props", | ||
_a[MemoStatus.UnmemoizedNew.toString()] = "instance-class-memo-props", | ||
_a[MemoStatus.UnmemoizedFunction.toString()] = 'instance-class-memo-props', | ||
_a[MemoStatus.UnmemoizedFunctionCall.toString()] = "unknown-class-memo-props", | ||
_a[MemoStatus.UnmemoizedOther.toString()] = "unknown-class-memo-props", | ||
_a[MemoStatus.UnsafeLet.toString()] = "usememo-const", | ||
_a); | ||
var jsxEmptyExpressionData = (_b = {}, | ||
_b[MemoStatus.UnmemoizedObject.toString()] = "object-usememo-props", | ||
_b[MemoStatus.UnmemoizedArray.toString()] = "array-usememo-props", | ||
_b[MemoStatus.UnmemoizedNew.toString()] = "instance-usememo-props", | ||
_b[MemoStatus.UnmemoizedFunction.toString()] = "function-usecallback-props", | ||
_b[MemoStatus.UnmemoizedFunctionCall.toString()] = "unknown-usememo-props", | ||
_b[MemoStatus.UnmemoizedOther.toString()] = "unknown-usememo-props", | ||
_b[MemoStatus.UnmemoizedJSX.toString()] = "jsx-usememo-props", | ||
_b[MemoStatus.UnsafeLet.toString()] = "usememo-const", | ||
_b); | ||
var hookReturnExpressionData = (_c = {}, | ||
_c[MemoStatus.UnmemoizedObject.toString()] = "object-usememo-hook", | ||
_c[MemoStatus.UnmemoizedArray.toString()] = "array-usememo-hook", | ||
_c[MemoStatus.UnmemoizedNew.toString()] = "instance-usememo-hook", | ||
_c[MemoStatus.UnmemoizedFunction.toString()] = "function-usecallback-hook", | ||
_c[MemoStatus.UnmemoizedFunctionCall.toString()] = "unknown-usememo-hook", | ||
_c[MemoStatus.UnmemoizedOther.toString()] = "unknown-usememo-hook", | ||
_c[MemoStatus.UnmemoizedJSX.toString()] = "jsx-usememo-hook", | ||
_c[MemoStatus.UnsafeLet.toString()] = "usememo-const", | ||
_c); | ||
var callExpressionData = (_d = {}, | ||
_d[MemoStatus.UnmemoizedObject.toString()] = "object-usememo-deps", | ||
_d[MemoStatus.UnmemoizedArray.toString()] = "array-usememo-deps", | ||
_d[MemoStatus.UnmemoizedNew.toString()] = "instance-usememo-deps", | ||
_d[MemoStatus.UnmemoizedFunction.toString()] = "function-usecallback-deps", | ||
_d[MemoStatus.UnmemoizedFunctionCall.toString()] = "unknown-usememo-deps", | ||
_d[MemoStatus.UnmemoizedOther.toString()] = "unknown-usememo-deps", | ||
_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 | ||
}; | ||
var MessagesRequireUseMemo = { | ||
@@ -361,2 +477,3 @@ "object-usememo-props": "Object literal should be wrapped in useMemo() or be static when used as a prop", | ||
}, | ||
fixable: 'code', | ||
schema: [ | ||
@@ -373,3 +490,8 @@ { | ||
function report(node, messageId) { | ||
context.report({ node: node, messageId: messageId }); | ||
context.report({ node: node, messageId: messageId, fix: function (fixer) { | ||
if (isClass) { | ||
return null; | ||
} | ||
return fixBasedOnMessageId(node, messageId, fixer, context); | ||
} }); | ||
} | ||
@@ -376,0 +498,0 @@ function process(node, _expression, expressionData) { |
{ | ||
"name": "@arthurgeron/eslint-plugin-react-usememo", | ||
"version": "1.1.4", | ||
"version": "2.0.0", | ||
"description": "", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
347
README.md
@@ -1,330 +0,91 @@ | ||
# eslint-plugin-react-usememo | ||
# ESLint-Plugin-React-UseMemo | ||
Enforce that functions or complex objects that can generate unecessary renders or side-effects are wrapped in `useMemo` or `useCallback`, allow for devs to enforce that functional components be wrapped in `memo` programatically, and that all props and deps are wrapped in `useMemo`/`useCallback`; The intended outcome is that component's tree and/or expensive lifecycles (e.g. React Native's FlatLists, useEffect, useMemo, etc) only re-calculate or render again when really necessary, controlling expensive expressions and bringing out the best scalability and performance that your application can get. | ||
This plugin enforces the wrapping of complex objects or functions (which might generate unnecessary renders or side-effects) in `useMemo` or `useCallback`. It also allows you to programmatically enforce the wrapping of functional components in `memo`, and that all props and dependencies are wrapped in `useMemo`/`useCallback`. | ||
## Rationale | ||
React Native's own [docs](https://reactnative.dev/docs/0.61/optimizing-flatlist-configuration#avoid-anonymous-function-on-renderitem) state how it's important to use static or memoized as props for complex children (FlatList on that case), that applies even more broadly when we are talking about custom components (the Components you've created), it might not seem necessary at first but you'll be making a bet that the component in question will never grow to use `memo` or those props in hooks (i.e. useEffect, useMemo, useCallback), you'll only notice once your solution starts freezing and dropping frames, that's why using the `require-usememo` rule is recommended. | ||
## Purpose | ||
The objective is to ensure that your application's component tree and/or expensive lifecycles (such as React Native's FlatLists, useEffect, useMemo, etc.) only re-calculate or render again when absolutely necessary. By controlling expensive expressions, you can achieve optimal scalability and performance for your application. | ||
_**Note:**_ Use of memoization everywhere is not advised, as everything comes with a cost. Overusing memoization might slow down your application instead of speeding it up. | ||
# Installation | ||
## Guidelines for Memoization | ||
``` | ||
yarn add @arthurgeron/eslint-plugin-react-usememo --dev | ||
``` | ||
or | ||
``` | ||
npm install @arthurgeron/eslint-plugin-react-usememo --save-dev | ||
``` | ||
Here are two primary rules for situations where dynamic objects should be memoed: | ||
1. Variables or expressions that return non-primitive objects or functions passed as props to other components. | ||
2. Variables or expressions that return non-primitive objects returned from custom hooks. | ||
# Usage | ||
It is not recommended to use memoization in the following cases: | ||
To enable the plugin add the following to the `plugin` property your `eslintrc` file: | ||
```json | ||
"plugins": ["@arthurgeron/react-usememo"], | ||
``` | ||
- When the resulting value (expression or variable) is primitive (string, number, boolean). | ||
- If you're passing props to a native component of the framework (e.g. Div, Touchable, etc), except in some instances in react-native (e.g. FlatList). | ||
- Values that can be a global/context outside the react Context. | ||
Then enable any rules as you like, example: | ||
```json | ||
"rules": { | ||
"@arthurgeron/react-usememo/require-usememo": [2], | ||
}, | ||
``` | ||
# Rules | ||
Example for better understanding: | ||
## `require-usememo` **Recommended** | ||
***Incorrect*** | ||
```js | ||
function Component() { | ||
const breakpoints = [100]; | ||
Requires complex values (objects, arrays, functions, and JSX) that get passed props or referenced as a hook dependency to be wrapped in `React.useMemo()` or `React.useCallback()`. | ||
Options: | ||
- `{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; | ||
- `{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() { | ||
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 ?? []} />); | ||
} | ||
return <Modal breakpoints={breakpoints}> | ||
} | ||
``` | ||
#### **Correct** | ||
```JavaScript | ||
// Has no dynamic dependencies therefore should be static, will be declared only once. | ||
function renderItem({ item }) { | ||
return <Text>item.name</Text>; | ||
} | ||
const EMPTY_ARRAY = []; | ||
function Component() { | ||
const [data, setData] = useState(EMPTY_ARRAY); | ||
// Will only render again if data changes | ||
return (<FlatList renderItem={renderItem} data={data ?? EMPTY_ARRAY} />); | ||
} | ||
***Correct*** | ||
```js | ||
const breakpoints = [100]; | ||
function Component() { | ||
return <Modal breakpoints={breakpoints}> | ||
} | ||
``` | ||
### Class Components | ||
#### **Incorrect** | ||
```JavaScript | ||
class Component() { | ||
> For more details, please refer to React Native's [documentation](https://reactnative.dev/docs/0.61/optimizing-flatlist-configuration#avoid-anonymous-function-on-renderitem) on the importance of using static or memoized props for complex children. | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
data: undefined, | ||
propDrivenData: props., | ||
}; | ||
} | ||
// This will NOT be redeclared each render | ||
getItemName(item) { | ||
return item.name; | ||
} | ||
## Installation | ||
render() { | ||
// This function will be redeclared each render | ||
function renderItem({ item }) { | ||
return (<Text>{this.getItemName(item)}</Text>); | ||
} | ||
Install it with yarn: | ||
// 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)} | ||
/>); | ||
} | ||
} | ||
``` | ||
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. | ||
#### **Correct** | ||
```JavaScript | ||
// 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 { | ||
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>); | ||
} | ||
render() { | ||
const { data } = this.state; | ||
// Will only cause a new render if data changes | ||
return (<FlatList renderItem={this.renderItem} data={data ?? EMPTY_ARRAY} />); | ||
} | ||
} | ||
yarn add @arthurgeron/eslint-plugin-react-usememo --dev | ||
``` | ||
#### **Correct** | ||
```JavaScript | ||
const EMPTY_ARRAY = []; | ||
function Component() { | ||
const [data, setData] = useState(EMPTY_ARRAY); | ||
const [isEditing, setIsEditing] = useState(false); | ||
or npm: | ||
// 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} />); | ||
} | ||
``` | ||
### Hooks | ||
Hooks return statements follow the same motto, they can and usually are used inside other hooks. | ||
#### **Incorrect** | ||
```JavaScript | ||
function useData() { | ||
let otherData = {}; | ||
function getData() {} | ||
return { getData, otherData}; | ||
} | ||
npm install @arthurgeron/eslint-plugin-react-usememo --save-dev | ||
``` | ||
#### **Incorrect** (checkHookReturnObject: true) | ||
```JavaScript | ||
function getData() {} | ||
function useData() { | ||
return { getData }; | ||
} | ||
``` | ||
## Usage | ||
#### **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); | ||
} | ||
Add the plugin to your `eslintrc` file: | ||
```json | ||
"plugins": ["@arthurgeron/react-usememo"], | ||
``` | ||
#### **Correct** | ||
```JavaScript | ||
const otherData = {}; // Or declare inside hook with useMemo | ||
Then enable any rules as you like: | ||
function useData() { | ||
const getData = useCallback(() => { // Or declare statically | ||
}, []); | ||
return {getData, otherData}; | ||
} | ||
```json | ||
"rules": { | ||
"@arthurgeron/react-usememo/require-usememo": [2], | ||
}, | ||
``` | ||
In this guide, we will cover three rules - `require-usememo`, `require-memo`, and `require-usememo-children`. | ||
#### **Correct** (checkHookReturnObject: true) | ||
```JavaScript | ||
function getData() { | ||
// Doing something | ||
} | ||
function useData() { | ||
return useMemo(() => ({ getData }), []); | ||
} | ||
``` | ||
#### **Correct** Passing a dependency to a hook | ||
```JavaScript | ||
function getData() {} | ||
## Rule #1: `require-usememo` ***(recommended)*** | ||
This rule requires complex values (objects, arrays, functions, and JSX) that get passed props or referenced as a hook dependency to be wrapped in useMemo() or useCallback(). | ||
function useData() { | ||
return useMemo(() => ({ getData }), []); | ||
} | ||
``` | ||
#### **Correct** Calling a custom hook | ||
```JavaScript | ||
function doSomething() {} | ||
One of the great features of this rule is its amazing autofix functionality. It intelligently wraps necessary components with useMemo() or useCallback(), making your code more efficient and saving you valuable time. | ||
function useStateManagement(someFlag) { | ||
useEffect(() => { | ||
doSomething(someFlag); | ||
}, [someFlag]); | ||
} | ||
For detailed examples, options available for this rule, and information about the autofix functionality, please refer to our rules documentation. | ||
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]); | ||
} | ||
## Rule #2: `require-memo` | ||
This rule requires all function components to be wrapped in `React.memo()`. | ||
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); | ||
} | ||
``` | ||
For detailed examples and usage of this rule, please refer to our [rules documentation](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/require-memo.md) | ||
## Rule #3: `require-usememo-children` | ||
This rule requires complex values (objects, arrays, functions, and JSX) that get passed as children to be wrapped in `useMemo()` or `useCallback()`. | ||
For detailed examples and options available for this rule, please refer to our [rules documentation](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/require-usememo-children.md). | ||
## `require-memo` | ||
Requires all function components to be wrapped in `React.memo()`. | ||
May be useful when used with overrides in your eslint config, I do not recommend enabling this globally, while there's great advantaje in memoing a complex tree of components some smaller/basic components with no children might not need to be memoized. | ||
## **Incorrect** | ||
```JavaScript | ||
export default function Component() { | ||
return (<Text>This is a component</Text>); | ||
} | ||
``` | ||
## **Correct** | ||
```JavaScript | ||
export default memo(function Component() { | ||
return (<Text>This is a component</Text>); | ||
}); | ||
``` | ||
## `require-usememo-children` **Advanced** | ||
Requires complex values (objects, arrays, functions, and JSX) that get passed as children to be wrapped in `React.useMemo()` or `React.useCallback()`. | ||
Options: | ||
- `{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.). | ||
## **Incorrect** | ||
```JavaScript | ||
function Component() { | ||
return (<View> | ||
<> | ||
<OtherComponent /> | ||
</> | ||
</View>); | ||
} | ||
``` | ||
## **Correct** | ||
```JavaScript | ||
function Component() { | ||
const children = useMemo(() => (<OtherComponent />), []); | ||
return (<View> | ||
{children} | ||
</View>); | ||
} | ||
``` | ||
## Conclusion | ||
By efficiently using `useMemo`, `useCallback`, and `React.memo()`, we can optimize our React and React Native applications. It allows us to control the re-calculation and re-rendering of components, offering better scalability and performance. |
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
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
38225
619
91