-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[New]
jsx-no-constructed-context-values
: add new rule which checks …
…when the value passed to a Context Provider will cause needless rerenders Adds a new rule that checks if the value prop passed to a Context Provider will cause needless rerenders. A common example is inline object declaration such as: ```js <Context.Provider value={{foo: 'bar'}}> ... </Context.Provider> ``` which will cause the Context to rerender every time this is run because the object is defined with a new reference. This can lead to performance hits since not only will this rerender all the children of a context, it will also update all consumers of the value. The search React does to search the tree can also be very expensive. Some other instances that this rule covers are: array declarations, function declarations (i.e. arrow functions), or new class object instantians.
- Loading branch information
Showing
6 changed files
with
665 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# Prevent react contexts from taking non-stable values (react/jsx-no-constructed-context-values) | ||
|
||
This rule prevents non-stable values (i.e. object identities) from being used as a value for `Context.Provider`. | ||
|
||
## Rule Details | ||
|
||
One way to resolve this issue may be to wrap the value in a `useMemo()`. If it's a function then `useCallback()` can be used as well. | ||
|
||
If you _expect_ the context to be rerun on each render, then consider adding a comment/lint supression explaining why. | ||
|
||
## Examples | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
``` | ||
return ( | ||
<SomeContext.Provider value={{foo: 'bar'}}> | ||
... | ||
</SomeContext.Provider> | ||
) | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
``` | ||
const foo = useMemo(() => {foo: 'bar'}, []); | ||
return ( | ||
<SomeContext.Provider value={foo}> | ||
... | ||
</SomeContext.Provider> | ||
) | ||
``` | ||
|
||
## Legitimate Uses | ||
React Context, and all its child nodes and Consumers are rerendered whenever the value prop changes. Because each Javascript object carries its own *identity*, things like object expressions (`{foo: 'bar'}`) or function expressions get a new identity on every run through the component. This makes the context think it has gotten a new object and can cause needless rerenders and unintended consequences. | ||
|
||
This can be a pretty large performance hit because not only will it cause the context providers and consumers to rerender with all the elements in its subtree, the processing for the tree scan react does to render the provider and find consumers is also wasted. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
/** | ||
* @fileoverview Prevents jsx context provider values from taking values that | ||
* will cause needless rerenders. | ||
* @author Dylan Oshima | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const docsUrl = require('../util/docsUrl'); | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Helpers | ||
// ------------------------------------------------------------------------------ | ||
|
||
// Recursively checks if an element is a construction. | ||
// A construction is a variable that changes identity every render. | ||
function isConstruction(node, callScope) { | ||
switch (node.type) { | ||
case 'Literal': | ||
if (node.regex != null) { | ||
return {type: 'regular expression', node}; | ||
} | ||
return null; | ||
case 'Identifier': { | ||
const variableScoping = callScope.set.get(node.name); | ||
|
||
if (variableScoping == null || variableScoping.defs == null) { | ||
// If it's not in scope, we don't care. | ||
return null; // Handled | ||
} | ||
|
||
// Gets the last variable identity | ||
const variableDefs = variableScoping.defs; | ||
const def = variableDefs[variableDefs.length - 1]; | ||
if (def != null | ||
&& def.type !== 'Variable' | ||
&& def.type !== 'FunctionName' | ||
) { | ||
// Parameter or an unusual pattern. Bail out. | ||
return null; // Unhandled | ||
} | ||
|
||
if (def.node.type === 'FunctionDeclaration') { | ||
return {type: 'function declaration', node: def.node, usage: node}; | ||
} | ||
|
||
const init = def.node.init; | ||
if (init == null) { | ||
return null; | ||
} | ||
|
||
const initConstruction = isConstruction(init, callScope); | ||
if (initConstruction == null) { | ||
return null; | ||
} | ||
|
||
return { | ||
type: initConstruction.type, | ||
node: initConstruction.node, | ||
usage: node | ||
}; | ||
} | ||
case 'ObjectExpression': | ||
// Any object initialized inline will create a new identity | ||
return {type: 'object', node}; | ||
case 'ArrayExpression': | ||
return {type: 'array', node}; | ||
case 'ArrowFunctionExpression': | ||
case 'FunctionExpression': | ||
// Functions that are initialized inline will have a new identity | ||
return {type: 'function expression', node}; | ||
case 'ClassExpression': | ||
return {type: 'class expression', node}; | ||
case 'NewExpression': | ||
// `const a = new SomeClass();` is a construction | ||
return {type: 'new expression', node}; | ||
case 'ConditionalExpression': | ||
return (isConstruction(node.consequent, callScope) | ||
|| isConstruction(node.alternate, callScope) | ||
); | ||
case 'LogicalExpression': | ||
return (isConstruction(node.left, callScope) | ||
|| isConstruction(node.right, callScope) | ||
); | ||
case 'MemberExpression': { | ||
const objConstruction = isConstruction(node.object, callScope); | ||
if (objConstruction == null) { | ||
return null; | ||
} | ||
return { | ||
type: objConstruction.type, | ||
node: objConstruction.node, | ||
usage: node.object | ||
}; | ||
} | ||
case 'JSXFragment': | ||
return {type: 'JSX fragment', node}; | ||
case 'JSXElement': | ||
return {type: 'JSX element', node}; | ||
case 'AssignmentExpression': { | ||
const construct = isConstruction(node.right); | ||
if (construct != null) { | ||
return { | ||
type: 'assignment expression', | ||
node: construct.node, | ||
usage: node | ||
}; | ||
} | ||
return null; | ||
} | ||
case 'TypeCastExpression': | ||
case 'TSAsExpression': | ||
return isConstruction(node.expression); | ||
default: | ||
return null; | ||
} | ||
} | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
|
||
module.exports = { | ||
meta: { | ||
docs: { | ||
description: 'Prevents JSX context provider values from taking values that will cause needless rerenders.', | ||
category: 'Best Practices', | ||
recommended: false, | ||
url: docsUrl('jsx-no-constructed-context-values') | ||
}, | ||
messages: { | ||
withIdentifierMsg: | ||
"The '{{variableName}}' {{type}} (at line {{nodeLine}}) passed as the value prop to the Context provider (at line {{usageLine}}) changes every render. To fix this consider wrapping it in a useMemo hook.", | ||
withIdentifierMsgFunc: | ||
"The '{{variableName}}' {{type}} (at line {{nodeLine}}) passed as the value prop to the Context provider (at line {{usageLine}}) changes every render. To fix this consider wrapping it in a useCallback hook.", | ||
defaultMsg: | ||
'The {{type}} passed as the value prop to the Context provider (at line {{nodeLine}}) changes every render. To fix this consider wrapping it in a useMemo hook.', | ||
defaultMsgFunc: | ||
'The {{type}} passed as the value prop to the Context provider (at line {{nodeLine}}) changes every render. To fix this consider wrapping it in a useCallback hook.' | ||
} | ||
}, | ||
|
||
create(context) { | ||
return { | ||
JSXOpeningElement(node) { | ||
const openingElementName = node.name; | ||
if (openingElementName.type !== 'JSXMemberExpression') { | ||
// Has no member | ||
return; | ||
} | ||
|
||
const isJsxContext = openingElementName.property.name === 'Provider'; | ||
if (!isJsxContext) { | ||
// Member is not Provider | ||
return; | ||
} | ||
|
||
// Contexts can take in more than just a value prop | ||
// so we need to iterate through all of them | ||
const jsxValueAttribute = node.attributes.find( | ||
(attribute) => attribute.type === 'JSXAttribute' && attribute.name.name === 'value' | ||
); | ||
|
||
if (jsxValueAttribute == null) { | ||
// No value prop was passed | ||
return; | ||
} | ||
|
||
const valueNode = jsxValueAttribute.value; | ||
if (valueNode.type !== 'JSXExpressionContainer') { | ||
// value could be a literal | ||
return; | ||
} | ||
|
||
const valueExpression = valueNode.expression; | ||
const invocationScope = context.getScope(); | ||
|
||
// Check if the value prop is a construction | ||
const constructInfo = isConstruction(valueExpression, invocationScope); | ||
if (constructInfo == null) { | ||
return; | ||
} | ||
|
||
// Report found error | ||
const constructType = constructInfo.type; | ||
const constructNode = constructInfo.node; | ||
const constructUsage = constructInfo.usage; | ||
const data = { | ||
type: constructType, nodeLine: constructNode.loc.start.line | ||
}; | ||
let messageId = 'defaultMsg'; | ||
|
||
// Variable passed to value prop | ||
if (constructUsage != null) { | ||
messageId = 'withIdentifierMsg'; | ||
data.usageLine = constructUsage.loc.start.line; | ||
data.variableName = constructUsage.name; | ||
} | ||
|
||
// Type of expression | ||
if (constructType === 'function expression' | ||
|| constructType === 'function declaration' | ||
) { | ||
messageId += 'Func'; | ||
} | ||
|
||
context.report({ | ||
node: constructNode, | ||
messageId, | ||
data | ||
}); | ||
} | ||
}; | ||
} | ||
}; |
Oops, something went wrong.