Skip to content

Commit f46fe87

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

File tree

4 files changed

+157
-7
lines changed

4 files changed

+157
-7
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1010
### Fixed
1111
* [`no-unknown-property`]: allow shadow root attrs on `<template>` ([#3912][] @ljharb)
1212
* [`prop-types`]: support `ComponentPropsWithRef` from a namespace import ([#3651][] @corydeppen)
13+
* [`jsx-no-constructed-context-values`]: detect constructed context values in React 19 `<Context>` usage ([#3910][] @TildaDares)
1314

1415
### Changed
1516
* [Docs] [`button-has-type`]: clean up phrasing ([#3909][] @hamirmahal)
1617

1718
[#3912]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3912
19+
[#3910]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3910
1820
[#3909]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3909
1921
[#3651]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3651
2022

Diff for: 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.

Diff for: 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

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

+88
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,36 @@ 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+
},
150180
]),
151181
invalid: parsers.all([
152182
{
@@ -468,5 +498,63 @@ ruleTester.run('react-no-constructed-context-values', rule, {
468498
},
469499
],
470500
},
501+
{
502+
// Invalid because function declaration creates a new identity
503+
code: `
504+
import React from 'react';
505+
506+
const Context = React.createContext();
507+
function Component() {
508+
function foo() {};
509+
return (<Context value={foo}></Context>)
510+
}
511+
`,
512+
errors: [
513+
{
514+
messageId: 'withIdentifierMsgFunc',
515+
data: {
516+
variableName: 'foo',
517+
type: 'function declaration',
518+
nodeLine: '6',
519+
usageLine: '7',
520+
},
521+
},
522+
],
523+
},
524+
{
525+
// Invalid because the object value will create a new identity
526+
code: `
527+
const MyContext = createContext();
528+
function Component() { const foo = {}; return (<MyContext value={foo}></MyContext>) }
529+
`,
530+
errors: [
531+
{
532+
messageId: 'withIdentifierMsg',
533+
data: {
534+
variableName: 'foo',
535+
type: 'object',
536+
nodeLine: '3',
537+
usageLine: '3',
538+
},
539+
},
540+
],
541+
},
542+
{
543+
// Invalid because inline object construction will create a new identity
544+
code: `
545+
const MyContext = createContext();
546+
function Component() { return (<MyContext value={{foo: "bar"}}></MyContext>); }
547+
`,
548+
errors: [
549+
{
550+
messageId: 'defaultMsg',
551+
data: {
552+
type: 'object',
553+
nodeLine: '3',
554+
usageLine: '3',
555+
},
556+
},
557+
],
558+
},
471559
]),
472560
});

0 commit comments

Comments
 (0)