-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
useRef: Warn about reading or writing mutable values during render #18545
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ import { | |
enableSchedulingProfiler, | ||
enableNewReconciler, | ||
decoupleUpdatePriorityFromScheduler, | ||
enableUseRefAccessWarning, | ||
} from 'shared/ReactFeatureFlags'; | ||
|
||
import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode'; | ||
|
@@ -1175,14 +1176,92 @@ function pushEffect(tag, create, destroy, deps) { | |
return effect; | ||
} | ||
|
||
let stackContainsErrorMessage: boolean | null = null; | ||
|
||
function getCallerStackFrame(): string { | ||
const stackFrames = new Error('Error message').stack.split('\n'); | ||
|
||
// Some browsers (e.g. Chrome) include the error message in the stack | ||
// but others (e.g. Firefox) do not. | ||
if (stackContainsErrorMessage === null) { | ||
stackContainsErrorMessage = stackFrames[0].includes('Error message'); | ||
} | ||
|
||
return stackContainsErrorMessage | ||
? stackFrames.slice(3, 4).join('\n') | ||
: stackFrames.slice(2, 3).join('\n'); | ||
} | ||
|
||
function mountRef<T>(initialValue: T): {|current: T|} { | ||
const hook = mountWorkInProgressHook(); | ||
const ref = {current: initialValue}; | ||
if (__DEV__) { | ||
Object.seal(ref); | ||
if (enableUseRefAccessWarning) { | ||
if (__DEV__) { | ||
// Support lazy initialization pattern shown in docs. | ||
// We need to store the caller stack frame so that we don't warn on subsequent renders. | ||
let hasBeenInitialized = initialValue != null; | ||
bvaughn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let lazyInitGetterStack = null; | ||
let didCheckForLazyInit = false; | ||
|
||
// Only warn once per component+hook. | ||
let didWarnAboutRead = false; | ||
let didWarnAboutWrite = false; | ||
|
||
let current = initialValue; | ||
const ref = { | ||
get current() { | ||
if (!hasBeenInitialized) { | ||
didCheckForLazyInit = true; | ||
lazyInitGetterStack = getCallerStackFrame(); | ||
} else if (currentlyRenderingFiber !== null && !didWarnAboutRead) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if it's read inside a class component? Or written to. Should it warn? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Undefined I guess. I could expand the warning to include class components as well, if you think that's worth doing. |
||
if ( | ||
lazyInitGetterStack === null || | ||
lazyInitGetterStack !== getCallerStackFrame() | ||
) { | ||
didWarnAboutRead = true; | ||
console.warn( | ||
'%s: Unsafe read of a mutable value during render.\n\n' + | ||
'Reading from a ref during render is only safe if:\n' + | ||
'1. The ref value has not been updated, or\n' + | ||
'2. The ref holds a lazily-initialized value that is only set once.\n', | ||
getComponentName(currentlyRenderingFiber.type) || 'Unknown', | ||
); | ||
} | ||
} | ||
return current; | ||
}, | ||
set current(value) { | ||
if (currentlyRenderingFiber !== null && !didWarnAboutWrite) { | ||
if ( | ||
hasBeenInitialized || | ||
(!hasBeenInitialized && !didCheckForLazyInit) | ||
) { | ||
didWarnAboutWrite = true; | ||
console.warn( | ||
'%s: Unsafe write of a mutable value during render.\n\n' + | ||
'Writing to a ref during render is only safe if the ref holds ' + | ||
'a lazily-initialized value that is only set once.\n', | ||
getComponentName(currentlyRenderingFiber.type) || 'Unknown', | ||
); | ||
} | ||
} | ||
|
||
hasBeenInitialized = true; | ||
current = value; | ||
}, | ||
}; | ||
Object.seal(ref); | ||
hook.memoizedState = ref; | ||
return ref; | ||
} else { | ||
const ref = {current: initialValue}; | ||
hook.memoizedState = ref; | ||
return ref; | ||
} | ||
} else { | ||
const ref = {current: initialValue}; | ||
hook.memoizedState = ref; | ||
return ref; | ||
} | ||
hook.memoizedState = ref; | ||
return ref; | ||
} | ||
|
||
function updateRef<T>(initialValue: T): {|current: T|} { | ||
|
@@ -1534,24 +1613,24 @@ function startTransition(setPending, callback) { | |
|
||
function mountTransition(): [(() => void) => void, boolean] { | ||
const [isPending, setPending] = mountState(false); | ||
// The `start` method can be stored on a ref, since `setPending` | ||
// never changes. | ||
// The `start` method never changes. | ||
const start = startTransition.bind(null, setPending); | ||
mountRef(start); | ||
const hook = mountWorkInProgressHook(); | ||
hook.memoizedState = start; | ||
return [start, isPending]; | ||
} | ||
|
||
function updateTransition(): [(() => void) => void, boolean] { | ||
const [isPending] = updateState(false); | ||
const startRef = updateRef(); | ||
const start: (() => void) => void = (startRef.current: any); | ||
const hook = updateWorkInProgressHook(); | ||
const start = hook.memoizedState; | ||
return [start, isPending]; | ||
} | ||
|
||
function rerenderTransition(): [(() => void) => void, boolean] { | ||
const [isPending] = rerenderState(false); | ||
const startRef = updateRef(); | ||
const start: (() => void) => void = (startRef.current: any); | ||
const hook = updateWorkInProgressHook(); | ||
const start = hook.memoizedState; | ||
return [start, isPending]; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This nesting is to satisfy our no-logging-in-production lint rule.