Skip to content

Commit c59c3df

Browse files
author
Brian Vaughn
authored
useRef: Warn about reading or writing mutable values during render (#18545)
Reading or writing a ref value during render is only safe if you are implementing the lazy initialization pattern. Other types of reading are unsafe as the ref is a mutable source. Other types of writing are unsafe as they are effectively side effects. This change also refactors useTransition to no longer use a ref hook, but instead manage its own (stable) hook state.
1 parent 75726fa commit c59c3df

15 files changed

+567
-121
lines changed

packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js

+8-10
Original file line numberDiff line numberDiff line change
@@ -474,31 +474,29 @@ describe('ReactDOMServerHooks', () => {
474474
describe('useRef',()=>{
475475
itRenders('basic render',asyncrender=>{
476476
functionCounter(props){
477-
constcount=useRef(0);
478-
return<span>Count: {count.current}</span>;
477+
constref=useRef();
478+
return<spanref={ref}>Hi</span>;
479479
}
480480

481481
constdomNode=awaitrender(<Counter/>);
482-
expect(domNode.textContent).toEqual('Count: 0');
482+
expect(domNode.textContent).toEqual('Hi');
483483
});
484484

485485
itRenders(
486486
'multiple times when updates happen during the render phase',
487487
asyncrender=>{
488488
functionCounter(props){
489489
const[count,setCount]=useState(0);
490-
constref=useRef(count);
490+
constref=useRef();
491491

492492
if(count<3){
493493
constnewCount=count+1;
494-
495-
ref.current=newCount;
496494
setCount(newCount);
497495
}
498496

499497
yieldValue(count);
500498

501-
return<span>Count: {ref.current}</span>;
499+
return<spanref={ref}>Count: {count}</span>;
502500
}
503501

504502
constdomNode=awaitrender(<Counter/>);
@@ -513,7 +511,7 @@ describe('ReactDOMServerHooks', () => {
513511
letfirstRef=null;
514512
functionCounter(props){
515513
const[count,setCount]=useState(0);
516-
constref=useRef(count);
514+
constref=useRef();
517515
if(firstRef===null){
518516
firstRef=ref;
519517
}elseif(firstRef!==ref){
@@ -528,12 +526,12 @@ describe('ReactDOMServerHooks', () => {
528526

529527
yieldValue(count);
530528

531-
return<span>Count: {ref.current}</span>;
529+
return<spanref={ref}>Count: {count}</span>;
532530
}
533531

534532
constdomNode=awaitrender(<Counter/>);
535533
expect(clearYields()).toEqual([0,1,2,3]);
536-
expect(domNode.textContent).toEqual('Count: 0');
534+
expect(domNode.textContent).toEqual('Count: 3');
537535
},
538536
);
539537
});

packages/react-reconciler/src/ReactFiberHooks.new.js

+91-12
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
enableNewReconciler,
2828
decoupleUpdatePriorityFromScheduler,
2929
enableDoubleInvokingEffects,
30+
enableUseRefAccessWarning,
3031
}from'shared/ReactFeatureFlags';
3132

3233
import{
@@ -1197,14 +1198,92 @@ function pushEffect(tag, create, destroy, deps) {
11971198
return effect;
11981199
}
11991200

1201+
letstackContainsErrorMessage: boolean|null=null;
1202+
1203+
functiongetCallerStackFrame(): string{
1204+
conststackFrames=newError('Error message').stack.split('\n');
1205+
1206+
// Some browsers (e.g. Chrome) include the error message in the stack
1207+
// but others (e.g. Firefox) do not.
1208+
if(stackContainsErrorMessage===null){
1209+
stackContainsErrorMessage =stackFrames[0].includes('Error message');
1210+
}
1211+
1212+
returnstackContainsErrorMessage
1213+
? stackFrames.slice(3,4).join('\n')
1214+
: stackFrames.slice(2,3).join('\n');
1215+
}
1216+
12001217
function mountRef<T>(initialValue: T): {|current: T|}{
12011218
consthook=mountWorkInProgressHook();
1202-
constref={current: initialValue};
1203-
if(__DEV__){
1204-
Object.seal(ref);
1219+
if(enableUseRefAccessWarning){
1220+
if(__DEV__){
1221+
// Support lazy initialization pattern shown in docs.
1222+
// We need to store the caller stack frame so that we don't warn on subsequent renders.
1223+
lethasBeenInitialized=initialValue!=null;
1224+
letlazyInitGetterStack=null;
1225+
letdidCheckForLazyInit=false;
1226+
1227+
// Only warn once per component+hook.
1228+
letdidWarnAboutRead=false;
1229+
letdidWarnAboutWrite=false;
1230+
1231+
letcurrent=initialValue;
1232+
constref={
1233+
getcurrent(){
1234+
if(!hasBeenInitialized){
1235+
didCheckForLazyInit=true;
1236+
lazyInitGetterStack=getCallerStackFrame();
1237+
}elseif(currentlyRenderingFiber!==null&&!didWarnAboutRead){
1238+
if(
1239+
lazyInitGetterStack===null||
1240+
lazyInitGetterStack!==getCallerStackFrame()
1241+
){
1242+
didWarnAboutRead=true;
1243+
console.warn(
1244+
'%s: Unsafe read of a mutable value during render.\n\n'+
1245+
'Reading from a ref during render is only safe if:\n'+
1246+
'1. The ref value has not been updated, or\n'+
1247+
'2. The ref holds a lazily-initialized value that is only set once.\n',
1248+
getComponentName(currentlyRenderingFiber.type)||'Unknown',
1249+
);
1250+
}
1251+
}
1252+
returncurrent;
1253+
},
1254+
setcurrent(value){
1255+
if(currentlyRenderingFiber!==null&&!didWarnAboutWrite){
1256+
if(
1257+
hasBeenInitialized||
1258+
(!hasBeenInitialized&&!didCheckForLazyInit)
1259+
){
1260+
didWarnAboutWrite=true;
1261+
console.warn(
1262+
'%s: Unsafe write of a mutable value during render.\n\n'+
1263+
'Writing to a ref during render is only safe if the ref holds '+
1264+
'a lazily-initialized value that is only set once.\n',
1265+
getComponentName(currentlyRenderingFiber.type)||'Unknown',
1266+
);
1267+
}
1268+
}
1269+
1270+
hasBeenInitialized=true;
1271+
current=value;
1272+
},
1273+
};
1274+
Object.seal(ref);
1275+
hook.memoizedState=ref;
1276+
returnref;
1277+
}else{
1278+
constref={current: initialValue};
1279+
hook.memoizedState=ref;
1280+
returnref;
1281+
}
1282+
}else{
1283+
constref={current: initialValue};
1284+
hook.memoizedState=ref;
1285+
returnref;
12051286
}
1206-
hook.memoizedState = ref;
1207-
return ref;
12081287
}
12091288

12101289
functionupdateRef<T>(initialValue: T): {|current: T|}{
@@ -1591,24 +1670,24 @@ function startTransition(setPending, callback) {
15911670

15921671
functionmountTransition(): [(()=>void)=>void,boolean]{
15931672
const[isPending,setPending]=mountState(false);
1594-
// The `start` method can be stored on a ref, since `setPending`
1595-
// never changes.
1673+
// The `start` method never changes.
15961674
conststart=startTransition.bind(null,setPending);
1597-
mountRef(start);
1675+
consthook=mountWorkInProgressHook();
1676+
hook.memoizedState=start;
15981677
return[start,isPending];
15991678
}
16001679

16011680
function updateTransition(): [(() =>void)=>void,boolean]{
16021681
const[isPending]=updateState(false);
1603-
conststartRef=updateRef();
1604-
conststart: (()=>void)=>void=(startRef.current: any);
1682+
consthook=updateWorkInProgressHook();
1683+
conststart=hook.memoizedState;
16051684
return[start,isPending];
16061685
}
16071686

16081687
function rerenderTransition(): [(() =>void)=>void,boolean]{
16091688
const[isPending]=rerenderState(false);
1610-
conststartRef=updateRef();
1611-
conststart: (()=>void)=>void=(startRef.current: any);
1689+
consthook=updateWorkInProgressHook();
1690+
conststart=hook.memoizedState;
16121691
return[start,isPending];
16131692
}
16141693

packages/react-reconciler/src/ReactFiberHooks.old.js

+91-12
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
enableSchedulingProfiler,
2727
enableNewReconciler,
2828
decoupleUpdatePriorityFromScheduler,
29+
enableUseRefAccessWarning,
2930
}from'shared/ReactFeatureFlags';
3031

3132
import{NoMode,BlockingMode,DebugTracingMode}from'./ReactTypeOfMode';
@@ -1175,14 +1176,92 @@ function pushEffect(tag, create, destroy, deps) {
11751176
return effect;
11761177
}
11771178

1179+
letstackContainsErrorMessage: boolean|null=null;
1180+
1181+
functiongetCallerStackFrame(): string{
1182+
conststackFrames=newError('Error message').stack.split('\n');
1183+
1184+
// Some browsers (e.g. Chrome) include the error message in the stack
1185+
// but others (e.g. Firefox) do not.
1186+
if(stackContainsErrorMessage===null){
1187+
stackContainsErrorMessage =stackFrames[0].includes('Error message');
1188+
}
1189+
1190+
returnstackContainsErrorMessage
1191+
? stackFrames.slice(3,4).join('\n')
1192+
: stackFrames.slice(2,3).join('\n');
1193+
}
1194+
11781195
function mountRef<T>(initialValue: T): {|current: T|}{
11791196
consthook=mountWorkInProgressHook();
1180-
constref={current: initialValue};
1181-
if(__DEV__){
1182-
Object.seal(ref);
1197+
if(enableUseRefAccessWarning){
1198+
if(__DEV__){
1199+
// Support lazy initialization pattern shown in docs.
1200+
// We need to store the caller stack frame so that we don't warn on subsequent renders.
1201+
lethasBeenInitialized=initialValue!=null;
1202+
letlazyInitGetterStack=null;
1203+
letdidCheckForLazyInit=false;
1204+
1205+
// Only warn once per component+hook.
1206+
letdidWarnAboutRead=false;
1207+
letdidWarnAboutWrite=false;
1208+
1209+
letcurrent=initialValue;
1210+
constref={
1211+
getcurrent(){
1212+
if(!hasBeenInitialized){
1213+
didCheckForLazyInit=true;
1214+
lazyInitGetterStack=getCallerStackFrame();
1215+
}elseif(currentlyRenderingFiber!==null&&!didWarnAboutRead){
1216+
if(
1217+
lazyInitGetterStack===null||
1218+
lazyInitGetterStack!==getCallerStackFrame()
1219+
){
1220+
didWarnAboutRead=true;
1221+
console.warn(
1222+
'%s: Unsafe read of a mutable value during render.\n\n'+
1223+
'Reading from a ref during render is only safe if:\n'+
1224+
'1. The ref value has not been updated, or\n'+
1225+
'2. The ref holds a lazily-initialized value that is only set once.\n',
1226+
getComponentName(currentlyRenderingFiber.type)||'Unknown',
1227+
);
1228+
}
1229+
}
1230+
returncurrent;
1231+
},
1232+
setcurrent(value){
1233+
if(currentlyRenderingFiber!==null&&!didWarnAboutWrite){
1234+
if(
1235+
hasBeenInitialized||
1236+
(!hasBeenInitialized&&!didCheckForLazyInit)
1237+
){
1238+
didWarnAboutWrite=true;
1239+
console.warn(
1240+
'%s: Unsafe write of a mutable value during render.\n\n'+
1241+
'Writing to a ref during render is only safe if the ref holds '+
1242+
'a lazily-initialized value that is only set once.\n',
1243+
getComponentName(currentlyRenderingFiber.type)||'Unknown',
1244+
);
1245+
}
1246+
}
1247+
1248+
hasBeenInitialized=true;
1249+
current=value;
1250+
},
1251+
};
1252+
Object.seal(ref);
1253+
hook.memoizedState=ref;
1254+
returnref;
1255+
}else{
1256+
constref={current: initialValue};
1257+
hook.memoizedState=ref;
1258+
returnref;
1259+
}
1260+
}else{
1261+
constref={current: initialValue};
1262+
hook.memoizedState=ref;
1263+
returnref;
11831264
}
1184-
hook.memoizedState = ref;
1185-
return ref;
11861265
}
11871266

11881267
functionupdateRef<T>(initialValue: T): {|current: T|}{
@@ -1534,24 +1613,24 @@ function startTransition(setPending, callback) {
15341613

15351614
functionmountTransition(): [(()=>void)=>void,boolean]{
15361615
const[isPending,setPending]=mountState(false);
1537-
// The `start` method can be stored on a ref, since `setPending`
1538-
// never changes.
1616+
// The `start` method never changes.
15391617
conststart=startTransition.bind(null,setPending);
1540-
mountRef(start);
1618+
consthook=mountWorkInProgressHook();
1619+
hook.memoizedState=start;
15411620
return[start,isPending];
15421621
}
15431622

15441623
function updateTransition(): [(() =>void)=>void,boolean]{
15451624
const[isPending]=updateState(false);
1546-
conststartRef=updateRef();
1547-
conststart: (()=>void)=>void=(startRef.current: any);
1625+
consthook=updateWorkInProgressHook();
1626+
conststart=hook.memoizedState;
15481627
return[start,isPending];
15491628
}
15501629

15511630
function rerenderTransition(): [(() =>void)=>void,boolean]{
15521631
const[isPending]=rerenderState(false);
1553-
conststartRef=updateRef();
1554-
conststart: (()=>void)=>void=(startRef.current: any);
1632+
consthook=updateWorkInProgressHook();
1633+
conststart=hook.memoizedState;
15551634
return[start,isPending];
15561635
}
15571636

0 commit comments

Comments
 (0)
close