/** * @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, callScope); if (construct != null) { return { type: 'assignment expression', node: construct.node, usage: node }; } return null; } case 'TypeCastExpression': case 'TSAsExpression': return isConstruction(node.expression, callScope); 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) { // attribute is a boolean shorthand return; } 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 }); } }; } };