Skip to content

Commit 81b82d3

Browse files
committed
fix(rule): detect constructed context values in React 19 <Context> usage
1 parent e6b5b41 commit 81b82d3

File tree

3 files changed

+140
-7
lines changed

3 files changed

+140
-7
lines changed

docs/rules/jsx-no-constructed-context-values.md

+15
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ return (
2222
)
2323
```
2424

25+
```jsx
26+
import React from 'react';
27+
28+
const MyContext = React.createContext();
29+
function Component() {
30+
function foo() {};
31+
return (<MyContext value={foo}></MyContext>)
32+
}
33+
```
34+
2535
Examples of **correct** code for this rule:
2636

2737
```jsx
@@ -33,6 +43,11 @@ return (
3343
)
3444
```
3545

46+
```jsx
47+
const SomeContext = createContext();
48+
const Component = () => <SomeContext value="Some string"><SomeContext>;
49+
```
50+
3651
## Legitimate Uses
3752

3853
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.

lib/rules/jsx-no-constructed-context-values.js

+45-7
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,38 @@ function isConstruction(node, callScope) {
119119
}
120120
}
121121

122+
function isReactContext(context, node) {
123+
let scope = getScope(context, node);
124+
let variableScoping = null;
125+
const contextName = node.name;
126+
127+
while (scope && !variableScoping) { // Walk up the scope chain to find the variable
128+
variableScoping = scope.set.get(contextName);
129+
scope = scope.upper;
130+
}
131+
132+
if (!variableScoping) { // Context was not found in scope
133+
return false;
134+
}
135+
136+
// Get the variable's definition
137+
const def = variableScoping.defs[0];
138+
139+
if (!def || def.node.type !== 'VariableDeclarator') return false;
140+
141+
const init = def.node.init; // Variable initializer
142+
143+
const isCreateContext = init
144+
&& init.type === 'CallExpression'
145+
&& ((init.callee.type === 'Identifier'
146+
&& init.callee.name === 'createContext')
147+
|| (init.callee.type === 'MemberExpression'
148+
&& init.callee.object.name === 'React'
149+
&& init.callee.property.name === 'createContext'));
150+
151+
return isCreateContext;
152+
}
153+
122154
// ------------------------------------------------------------------------------
123155
// Rule Definition
124156
// ------------------------------------------------------------------------------
@@ -148,14 +180,20 @@ module.exports = {
148180
return {
149181
JSXOpeningElement(node) {
150182
const openingElementName = node.name;
151-
if (openingElementName.type !== 'JSXMemberExpression') {
152-
// Has no member
153-
return;
154-
}
155183

156-
const isJsxContext = openingElementName.property.name === 'Provider';
157-
if (!isJsxContext) {
158-
// Member is not Provider
184+
if (openingElementName.type === 'JSXMemberExpression') {
185+
const isJsxContext = openingElementName.property.name === 'Provider';
186+
if (!isJsxContext) {
187+
// Member is not Provider
188+
return;
189+
}
190+
} else if (openingElementName.type === 'JSXIdentifier') {
191+
const isJsxContext = isReactContext(context, openingElementName);
192+
if (!isJsxContext) {
193+
// Member is not context
194+
return;
195+
}
196+
} else {
159197
return;
160198
}
161199

tests/lib/rules/jsx-no-constructed-context-values.js

+80
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,28 @@ ruleTester.run('react-no-constructed-context-values', rule, {
147147
);
148148
`,
149149
},
150+
{
151+
code: `
152+
import React from 'react';
153+
154+
const MyContext = React.createContext();
155+
const Component = () => <MyContext value={props}></MyContext>;
156+
`,
157+
},
158+
{
159+
code: `
160+
import React from 'react';
161+
162+
const MyContext = React.createContext();
163+
const Component = () => <MyContext value={100}></MyContext>;
164+
`,
165+
},
166+
{
167+
code: `
168+
const SomeContext = createContext();
169+
const Component = () => <SomeContext value="Some string"></SomeContext>;
170+
`,
171+
},
150172
]),
151173
invalid: parsers.all([
152174
{
@@ -468,5 +490,63 @@ ruleTester.run('react-no-constructed-context-values', rule, {
468490
},
469491
],
470492
},
493+
{
494+
// Invalid because function declaration creates a new identity
495+
code: `
496+
import React from 'react';
497+
498+
const Context = React.createContext();
499+
function Component() {
500+
function foo() {};
501+
return (<Context value={foo}></Context>)
502+
}
503+
`,
504+
errors: [
505+
{
506+
messageId: 'withIdentifierMsgFunc',
507+
data: {
508+
variableName: 'foo',
509+
type: 'function declaration',
510+
nodeLine: '6',
511+
usageLine: '7',
512+
},
513+
},
514+
],
515+
},
516+
{
517+
// Invalid because the object value will create a new identity
518+
code: `
519+
const MyContext = createContext();
520+
function Component() { const foo = {}; return (<MyContext value={foo}></MyContext>) }
521+
`,
522+
errors: [
523+
{
524+
messageId: 'withIdentifierMsg',
525+
data: {
526+
variableName: 'foo',
527+
type: 'object',
528+
nodeLine: '3',
529+
usageLine: '3',
530+
},
531+
},
532+
],
533+
},
534+
{
535+
// Invalid because inline object construction will create a new identity
536+
code: `
537+
const MyContext = createContext();
538+
function Component() { return (<MyContext value={{foo: "bar"}}></MyContext>); }
539+
`,
540+
errors: [
541+
{
542+
messageId: 'defaultMsg',
543+
data: {
544+
type: 'object',
545+
nodeLine: '3',
546+
usageLine: '3',
547+
},
548+
},
549+
],
550+
},
471551
]),
472552
});

0 commit comments

Comments
 (0)