Skip to content

Commit 2428618

Browse files
TildaDaresljharb
authored andcommitted
[Fix] jsx-no-constructed-context-values: detect constructed context values in React 19 <Context> usage
1 parent 60b7316 commit 2428618

4 files changed

+165
-8
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# Change Log
32

43
All notable changes to this project will be documented in this file.
@@ -10,11 +9,13 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
109
### Fixed
1110
* [`no-unknown-property`]: allow shadow root attrs on `<template>` ([#3912][] @ljharb)
1211
* [`prop-types`]: support `ComponentPropsWithRef` from a namespace import ([#3651][] @corydeppen)
12+
* [`jsx-no-constructed-context-values`]: detect constructed context values in React 19 `<Context>` usage ([#3910][] @TildaDares)
1313

1414
### Changed
1515
* [Docs] [`button-has-type`]: clean up phrasing ([#3909][] @hamirmahal)
1616

1717
[#3912]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3912
18+
[#3910]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3910
1819
[#3909]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3909
1920
[#3651]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3651
2021

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

+52-7
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,45 @@ 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') {
140+
return false;
141+
}
142+
143+
const init = def.node.init; // Variable initializer
144+
145+
const isCreateContext = init
146+
&& init.type === 'CallExpression'
147+
&& (
148+
(
149+
init.callee.type === 'Identifier'
150+
&& init.callee.name === 'createContext'
151+
) || (
152+
init.callee.type === 'MemberExpression'
153+
&& init.callee.object.name === 'React'
154+
&& init.callee.property.name === 'createContext'
155+
)
156+
);
157+
158+
return isCreateContext;
159+
}
160+
122161
// ------------------------------------------------------------------------------
123162
// Rule Definition
124163
// ------------------------------------------------------------------------------
@@ -148,14 +187,20 @@ module.exports = {
148187
return {
149188
JSXOpeningElement(node) {
150189
const openingElementName = node.name;
151-
if (openingElementName.type !== 'JSXMemberExpression') {
152-
// Has no member
153-
return;
154-
}
155190

156-
const isJsxContext = openingElementName.property.name === 'Provider';
157-
if (!isJsxContext) {
158-
// Member is not Provider
191+
if (openingElementName.type === 'JSXMemberExpression') {
192+
const isJSXContext = openingElementName.property.name === 'Provider';
193+
if (!isJSXContext) {
194+
// Member is not Provider
195+
return;
196+
}
197+
} else if (openingElementName.type === 'JSXIdentifier') {
198+
const isJSXContext = isReactContext(context, openingElementName);
199+
if (!isJSXContext) {
200+
// Member is not context
201+
return;
202+
}
203+
} else {
159204
return;
160205
}
161206

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

+96
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,44 @@ ruleTester.run('react-no-constructed-context-values', rule, {
147147
);
148148
`,
149149
},
150+
{
151+
code: `
152+
// Passes because the context is not a provider
153+
function Component() {
154+
return <MyContext.Consumer value={{ foo: 'bar' }} />;
155+
}
156+
`,
157+
},
158+
{
159+
code: `
160+
import React from 'react';
161+
162+
const MyContext = React.createContext();
163+
const Component = () => <MyContext value={props}></MyContext>;
164+
`,
165+
},
166+
{
167+
code: `
168+
import React from 'react';
169+
170+
const MyContext = React.createContext();
171+
const Component = () => <MyContext value={100}></MyContext>;
172+
`,
173+
},
174+
{
175+
code: `
176+
const SomeContext = createContext();
177+
const Component = () => <SomeContext value="Some string"></SomeContext>;
178+
`,
179+
},
180+
{
181+
code: `
182+
// Passes because MyContext is not a variable declarator
183+
function Component({ MyContext }) {
184+
return <MyContext value={{ foo: "bar" }} />;
185+
}
186+
`,
187+
},
150188
]),
151189
invalid: parsers.all([
152190
{
@@ -468,5 +506,63 @@ ruleTester.run('react-no-constructed-context-values', rule, {
468506
},
469507
],
470508
},
509+
{
510+
// Invalid because function declaration creates a new identity
511+
code: `
512+
import React from 'react';
513+
514+
const Context = React.createContext();
515+
function Component() {
516+
function foo() {};
517+
return (<Context value={foo}></Context>)
518+
}
519+
`,
520+
errors: [
521+
{
522+
messageId: 'withIdentifierMsgFunc',
523+
data: {
524+
variableName: 'foo',
525+
type: 'function declaration',
526+
nodeLine: '6',
527+
usageLine: '7',
528+
},
529+
},
530+
],
531+
},
532+
{
533+
// Invalid because the object value will create a new identity
534+
code: `
535+
const MyContext = createContext();
536+
function Component() { const foo = {}; return (<MyContext value={foo}></MyContext>) }
537+
`,
538+
errors: [
539+
{
540+
messageId: 'withIdentifierMsg',
541+
data: {
542+
variableName: 'foo',
543+
type: 'object',
544+
nodeLine: '3',
545+
usageLine: '3',
546+
},
547+
},
548+
],
549+
},
550+
{
551+
// Invalid because inline object construction will create a new identity
552+
code: `
553+
const MyContext = createContext();
554+
function Component() { return (<MyContext value={{foo: "bar"}}></MyContext>); }
555+
`,
556+
errors: [
557+
{
558+
messageId: 'defaultMsg',
559+
data: {
560+
type: 'object',
561+
nodeLine: '3',
562+
usageLine: '3',
563+
},
564+
},
565+
],
566+
},
471567
]),
472568
});

0 commit comments

Comments
 (0)