Skip to content

Conversation

@bvaughn
Copy link
Contributor

@bvaughn bvaughn commented Apr 8, 2020

This warning is being tested within Facebook behind a feature flag to assess its impact, (e.g. how many warnings does it trigger, how many patterns (like usePrevious) does it flush out, do they all have safe alternate patterns, etc.)

I wouldn't recommend upgrading existing code until we have published recommended alternative patterns– but if it's easily done, avoid it in new code, you're aware of it now.


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.

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Apr 8, 2020
@codesandbox-ci
Copy link

codesandbox-ci bot commented Apr 8, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 551985a:

Sandbox Source
React Configuration

@sizebot
Copy link

sizebot commented Apr 8, 2020

Details of bundled changes.

Comparing: 51a3aa6...551985a

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js 0.0% -0.0% 872.44 KB 872.46 KB 199.73 KB 199.7 KB NODE_DEV
ReactDOMForked-prod.js 🔺+0.1% 🔺+0.1% 383.49 KB 383.8 KB 70.76 KB 70.81 KB FB_WWW_PROD
react-dom-server.node.development.js 0.0% 0.0% 137.06 KB 137.06 KB 36.37 KB 36.37 KB NODE_DEV
react-dom.production.min.js 0.0% 0.0% 117.72 KB 117.74 KB 37.94 KB 37.94 KB NODE_PROD
ReactDOMForked-profiling.js +0.1% +0.1% 398.04 KB 398.37 KB 73.27 KB 73.33 KB FB_WWW_PROFILING
react-dom-server.browser.development.js 0.0% 0.0% 143.14 KB 143.14 KB 36.57 KB 36.57 KB UMD_DEV
react-dom-server.node.production.min.js 0.0% 0.0% 20.2 KB 20.2 KB 7.58 KB 7.58 KB NODE_PROD
react-dom-test-utils.production.min.js 0.0% 0.0% 13.7 KB 13.7 KB 5.31 KB 5.32 KB UMD_PROD
ReactDOMTesting-dev.js 0.0% -0.0% 940.5 KB 940.52 KB 210.73 KB 210.71 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% 0.0% 66.27 KB 66.27 KB 18.83 KB 18.83 KB NODE_DEV
ReactDOMTesting-prod.js 0.0% -0.0% 384.57 KB 384.64 KB 72.36 KB 72.35 KB FB_WWW_PROD
react-dom-test-utils.production.min.js 0.0% 0.0% 13.68 KB 13.68 KB 5.26 KB 5.26 KB NODE_PROD
ReactTestUtils-dev.js +0.1% +0.1% 60.84 KB 60.91 KB 16.76 KB 16.78 KB FB_WWW_DEV
react-dom.development.js 0.0% -0.0% 916.83 KB 916.85 KB 202.27 KB 202.24 KB UMD_DEV
react-dom.production.min.js 0.0% -0.0% 117.62 KB 117.64 KB 38.64 KB 38.64 KB UMD_PROD
react-dom.profiling.min.js 0.0% -0.0% 121.52 KB 121.54 KB 39.87 KB 39.85 KB UMD_PROFILING
ReactDOMForked-dev.js +0.3% +0.3% 988.98 KB 991.71 KB 218.9 KB 219.53 KB FB_WWW_DEV
react-dom.profiling.min.js 0.0% 0.0% 121.8 KB 121.82 KB 39.12 KB 39.13 KB NODE_PROFILING
react-dom-server.browser.production.min.js 0.0% 0.0% 19.88 KB 19.88 KB 7.46 KB 7.46 KB UMD_PROD
ReactDOM-dev.js +0.3% +0.3% 977.29 KB 980.02 KB 217.31 KB 217.94 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.1% 🔺+0.1% 381.06 KB 381.36 KB 70.24 KB 70.29 KB FB_WWW_PROD
react-dom-server.browser.development.js 0.0% 0.0% 135.79 KB 135.79 KB 36.12 KB 36.12 KB NODE_DEV
ReactDOM-profiling.js +0.1% +0.1% 394.07 KB 394.37 KB 72.61 KB 72.67 KB FB_WWW_PROFILING
react-dom-server.browser.production.min.js 0.0% 0.0% 19.78 KB 19.78 KB 7.42 KB 7.42 KB NODE_PROD
ReactDOMServer-dev.js +0.1% +0.1% 145.44 KB 145.51 KB 37.28 KB 37.3 KB FB_WWW_DEV
ReactDOMServer-prod.js 0.0% -0.0% 47.3 KB 47.3 KB 11.04 KB 11.04 KB FB_WWW_PROD
react-dom-test-utils.development.js 0.0% 0.0% 71.46 KB 71.46 KB 19.34 KB 19.34 KB UMD_DEV

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-prod.js 🔺+0.1% 🔺+0.1% 240.1 KB 240.41 KB 42.44 KB 42.49 KB FB_WWW_PROD
react-art.development.js 0.0% -0.0% 664.4 KB 664.42 KB 141.61 KB 141.58 KB UMD_DEV
react-art.production.min.js 0.0% -0.0% 108.49 KB 108.51 KB 33.71 KB 33.7 KB UMD_PROD
react-art.development.js 0.0% -0.0% 566.82 KB 566.84 KB 123.81 KB 123.78 KB NODE_DEV
react-art.production.min.js 0.0% -0.0% 73.43 KB 73.45 KB 22.82 KB 22.82 KB NODE_PROD
ReactART-dev.js +0.4% +0.5% 628.77 KB 631.5 KB 133.55 KB 134.18 KB FB_WWW_DEV

ReactDOM: size: 0.0%, gzip: 0.0%

Size changes (stable)

Generated by 🚫 dangerJS against 551985a

@sizebot
Copy link

sizebot commented Apr 8, 2020

Details of bundled changes.

Comparing: 51a3aa6...551985a

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js 0.0% -0.0% 907.99 KB 908.01 KB 206.28 KB 206.25 KB NODE_DEV
ReactDOMForked-prod.js 🔺+0.1% 🔺+0.1% 372.17 KB 372.48 KB 68.91 KB 68.96 KB FB_WWW_PROD
react-dom-server.node.development.js 0.0% 0.0% 138.57 KB 138.57 KB 36.58 KB 36.58 KB NODE_DEV
react-dom.production.min.js 0.0% 0.0% 122.26 KB 122.28 KB 39.29 KB 39.3 KB NODE_PROD
ReactDOMForked-profiling.js +0.1% +0.1% 386.67 KB 386.99 KB 71.46 KB 71.5 KB FB_WWW_PROFILING
react-dom-server.browser.development.js 0.0% 0.0% 144.73 KB 144.73 KB 36.77 KB 36.77 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 13.71 KB 13.71 KB 5.32 KB 5.32 KB UMD_PROD
ReactDOMTesting-dev.js 0.0% -0.0% 912.16 KB 912.18 KB 205.21 KB 205.18 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% 0.0% 66.28 KB 66.28 KB 18.84 KB 18.84 KB NODE_DEV
ReactDOMTesting-prod.js 0.0% -0.0% 371.5 KB 371.58 KB 70.16 KB 70.15 KB FB_WWW_PROD
react-dom-unstable-fizz.node.development.js 0.0% -0.1% 5.52 KB 5.52 KB 1.84 KB 1.84 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 13.7 KB 13.7 KB 5.26 KB 5.27 KB NODE_PROD
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 5.25 KB 5.25 KB 1.78 KB 1.78 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.3% 1.22 KB 1.22 KB 711 B 713 B UMD_PROD
ReactTestUtils-dev.js +0.1% +0.1% 60.84 KB 60.91 KB 16.77 KB 16.79 KB FB_WWW_DEV
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 4.78 KB 4.78 KB 1.68 KB 1.68 KB NODE_DEV
react-dom.development.js 0.0% -0.0% 954.14 KB 954.16 KB 208.84 KB 208.82 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% 🔺+0.3% 1.01 KB 1.01 KB 615 B 617 B NODE_PROD
react-dom.production.min.js 0.0% 0.0% 122.09 KB 122.11 KB 40.01 KB 40.03 KB UMD_PROD
react-dom.profiling.min.js 0.0% -0.1% 127.36 KB 127.38 KB 41.71 KB 41.67 KB UMD_PROFILING
ReactDOMForked-dev.js +0.3% +0.3% 963.39 KB 966.13 KB 214.14 KB 214.77 KB FB_WWW_DEV
react-dom.profiling.min.js 0.0% -0.0% 127.71 KB 127.73 KB 40.96 KB 40.96 KB NODE_PROFILING
react-dom-server.browser.production.min.js 0.0% 0.0% 20.34 KB 20.34 KB 7.55 KB 7.55 KB UMD_PROD
ReactDOM-dev.js +0.3% +0.3% 951.7 KB 954.44 KB 212.61 KB 213.25 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.1% 🔺+0.1% 369.8 KB 370.11 KB 68.47 KB 68.51 KB FB_WWW_PROD
react-dom-server.browser.development.js 0.0% 0.0% 137.3 KB 137.3 KB 36.33 KB 36.33 KB NODE_DEV
ReactDOM-profiling.js +0.1% +0.1% 382.76 KB 383.07 KB 70.87 KB 70.92 KB FB_WWW_PROFILING
react-dom-server.browser.production.min.js 0.0% 0.0% 20.24 KB 20.24 KB 7.5 KB 7.5 KB NODE_PROD
ReactDOMServer-dev.js +0.1% +0.1% 141.41 KB 141.48 KB 36.26 KB 36.29 KB FB_WWW_DEV
ReactDOMServer-prod.js 0.0% 0.0% 46.44 KB 46.44 KB 10.83 KB 10.83 KB FB_WWW_PROD
react-dom-test-utils.development.js 0.0% 0.0% 71.47 KB 71.47 KB 19.35 KB 19.35 KB UMD_DEV

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-prod.js 🔺+0.1% 🔺+0.1% 232.96 KB 233.27 KB 41.18 KB 41.23 KB FB_WWW_PROD
react-art.development.js 0.0% -0.0% 691.3 KB 691.32 KB 146.62 KB 146.58 KB UMD_DEV
react-art.production.min.js 0.0% -0.0% 111.38 KB 111.4 KB 34.58 KB 34.58 KB UMD_PROD
react-art.development.js 0.0% -0.0% 592.46 KB 592.48 KB 128.74 KB 128.7 KB NODE_DEV
react-art.production.min.js 0.0% 0.0% 76.26 KB 76.29 KB 23.67 KB 23.67 KB NODE_PROD
ReactART-dev.js +0.4% +0.5% 618.76 KB 621.49 KB 131.52 KB 132.14 KB FB_WWW_DEV

ReactDOM: size: 0.0%, gzip: 0.0%

Size changes (experimental)

Generated by 🚫 dangerJS against 551985a

@markerikson
Copy link
Contributor

Just to be clear, this only kicks if you both write and read while rendering? Just reading is safe?

Does this warning also only kick in if they both happen within the same function component execution (ie, resets after rendering is done)?

@gaearon
Copy link
Collaborator

gaearon commented Apr 9, 2020

Just to be clear, this only kicks if you both write and read while rendering? Just reading is safe?

No, it’s reading itself that is a problem. Since it’s effectively the same as reading from a random global variable. It is non-deterministic because whatever you’re going to get depends on when render was called. If React calls render at a slightly different time you can have a different result.

@gaearon
Copy link
Collaborator

gaearon commented Apr 9, 2020

Does this warning also only kick in if they both happen within the same function component execution (ie, resets after rendering is done)?

Can you explain this pattern? First, it’s hard to guarantee any resetting. You would have to try/finally your entire component. Second, if you only need a ref value temporarily during render, you could have used a regular variable.

@billyjanitsch
Copy link
Contributor

Am I right to think that this warning would be triggered by the recommended implementation of usePrevious?

@gaearon
Copy link
Collaborator

gaearon commented Apr 9, 2020

Looks like it.

Can you show some code snippets of how you use it?

@bvaughn bvaughn force-pushed the read-ref-during-render-warning branch 2 times, most recently from a1edb82 to b3dc63b Compare April 9, 2020 18:31
@billyjanitsch
Copy link
Contributor

@gaearon Sorry, just now finding time to write this up.

Can you show some code snippets of how you use it?

Tbh, I most often see it used for derived state, which I know is better served by this pattern (if at all) since it avoids a multi-pass render. However, the ability to set state during render is only ever mentioned once in a "Hooks FAQ" answer and nowhere in the API reference. usePrevious is documented and has accordingly found its way into various popular hook technical writing and hook libraries, so I've found that, in practice, more developers are aware of it than of setting state during render and it often gets used for the same purpose. I know you'd want to steer people away from this but consider that the warning in this PR is not actionable for people using this for derived state who don't know that state can be set in render.

(Btw, I know that "React changes too often" chatter can be very frustrating given the team's focus on backwards compatibility, but I'm sure you can also understand that it could be discouraging to use a pattern sanctioned by the docs -- so much so that "it’s possible that in the future React will provide a usePrevious Hook out of the box since it’s a relatively common use case" -- only to get an error about it a few months later. I don't mean to be discouraging and I don't feel that way myself, but I hope this can give you a bit more empathy or patience towards folks who do.)


That said, I think I've seen a few legit uses of usePrevious in product code. Maybe I haven't sufficiently considered alternatives. The general case is wanting to perform some side effect in response to a changed value where the effect also depends on the previous value.

Logging

Sometimes we want to log when a value has changed, due to a user action or external event (we don't care by which mechanism). Often we want to log the old value as well as the new one, because we're interested in the transition. We generally only care to log the change if it actually got painted to the screen. For instance, imagine a UI where the width of a panel can be resized by the user.

const {width} = props
const prevWidth = usePrevious(width)

useEffect(() => {
  if (width !== prevWidth) {
    log('resize', {width, prevWidth})
  }
}, [width, prevWidth])

This could be rewritten to manually read/write to the ref in the effect, but how would you write something reusable that encapsulates what usePrevious is doing here?

Warning

A similar example is adding a dev-only warning when a prop that should be static (e.g. the initialization value of an uncontrolled component, like defaultValue for input) changes to indicate that the change will be ignored. The warning should include the old and new value.

#18547 is a nice change that lets us do it in render but only if we're using the native console.

Debugging

While debugging, sometimes we care about a render in which some value has changed from the previous commit. For example, we want to pause execution if a value has changed so we can poke around the scope, or we just want to be informed if a value changed, because we didn't expect that. Maybe we even want to know if a value changed twice across two commits. usePrevious serves all of these cases nicely without much code and I haven't been able to imagine a similarly simple alternative.

@bvaughn
Copy link
Contributor Author

bvaughn commented Apr 18, 2020

Thanks for the detailed response, @billyjanitsch 🙇

As we develop concurrent mode APIs, and see them used more broadly at Facebook, we learn about patterns that can cause problems that we didn't know about initially. It's understandable that it can be frustrating when our guidance changes. For what it's worth, we do try to share our latest way of thinking about these APIs with the larger open source community as our understanding evolves.

We'll also be working on a pretty substantial overhaul of the documentation over the next couple of months too, which will hopefully help.

I should have taken the time to write a more detailed description in this PR. It was meant for discussion/consideration on the team more than anything. At this point, I don't think we'll be able to move forward with this PR. There are probably too many pre-existing cases that would cause it to fire. We may end up investigating a lint rule instead, since that could be configured/disabled.


Logging

This could be rewritten to manually read/write to the ref in the effect, but how would you write something reusable that encapsulates what usePrevious is doing here?

I think you could do something like that with a custom hook too, unless I'm misunderstanding.

function useChangeCallback(value, onChange) {
  const prevValueRef = useRef(value);
  useEffect(() => {
    const prevValue = prevValueRef.current;
    if (prevValue !== value) {
      prevValueRef.current = value;
      onChange(value, prevValue);
    }
  });
}

Warning

A similar example is adding a dev-only warning when a prop that should be static (e.g. the initialization value of an uncontrolled component, like defaultValue for input) changes to indicate that the change will be ignored.

That's possible to do even with this warning.

function Example({ propThatShouldNotChange }) {
  const stableRef = useRef(propThatShouldNotChange);
  if (stableRef.current !== propThatShouldNotChange) {
    // Warn
  }
}

Because you never write to the ref (other than the initial value) reading is fine.

Debugging

I don't have a solution for this use case that wouldn't violate the warning this PR explores.

@TrySound
Copy link
Contributor

We actively use constant hook to get the first rendered value. It's also in docs
https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily

export const useConstant = <T>(fn: () => T): T => {
  const ref = React.useRef<null | {| value: T |}>(null);
  if (ref.current == null) {
    // we instantiate { value } to not conflict with returned null
    ref.current = { value: fn() };
  }
  return ref.current.value;
};

@bvaughn
Copy link
Contributor Author

bvaughn commented Apr 19, 2020

@TrySound This PR accounts for that pattern too. (The first time a value is set from null to something else doesn't count against the "unsafe to read later" rule.)

@dai-shi
Copy link
Contributor

dai-shi commented Apr 24, 2020

At this point, I don't think we'll be able to move forward with this PR. There are probably too many pre-existing cases that would cause it to fire. We may end up investigating a lint rule instead, since that could be configured/disabled.

It would be nice for us to intentionally test our apps and libs. What about opting in with StrictMode? Is it feasible? Could it be easier (and more correct) than investigating a new eslint rule?

<StrictMode warnReadingMutableRefValue>

if (!hasBeenInitialized) {
didCheckForLazyInit = true;
lazyInitGetterStack = getCallerStackFrame();
} else if (currentlyRenderingFiber !== null && !didWarnAboutRead) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.
@bvaughn bvaughn force-pushed the read-ref-during-render-warning branch from e4ba7cf to 552900b Compare October 19, 2020 19:20
@bvaughn
Copy link
Contributor Author

bvaughn commented Oct 19, 2020

Hey everyone 👋 I am going to merge this PR and roll it out behind a feature flag within Facebook to assess its impact, e.g. how many warnings does it trigger, how many patterns (like usePrevious) does it flush out, do they all have safe alternate patterns, etc.

I wouldn't recommend upgrading existing code until we have published recommended alternative patterns– but if it's easily done, avoid it in new code, you're aware of it now.

• Changed tests to use pragma
• Renamed feature flag
@bvaughn bvaughn force-pushed the read-ref-during-render-warning branch from 552900b to 551985a Compare October 19, 2020 19:34
@bvaughn bvaughn merged commit c59c3df into facebook:master Oct 19, 2020
@bvaughn bvaughn deleted the read-ref-during-render-warning branch October 19, 2020 20:05
@Andarist
Copy link
Contributor

Andarist commented Oct 20, 2020

would this warn for this type of situation? https://codesandbox.io/s/agitated-rubin-x2c1l?file=/src/index.js:380-595

Yes, in its current form.

Isn't this a classic lazy init pattern that is being tested here? 🤔

You could also use memo, like so:

Isn't this unsafe for this use case as useMemo doesn't have semantic guarantees and can be just freely reinitialized when React decides to do so (which includes recomputation during Fast Refresh)?

@bvaughn
Copy link
Contributor Author

bvaughn commented Oct 20, 2020

Isn't this a classic lazy init pattern that is being tested here? 🤔

The reason that sandbox would warn with the semantics on this PR is not because of the lazy init pattern, but because of the read further down on line 60 (passing ref.current to createPortal).

Isn't this unsafe for this use case as useMemo doesn't have semantic guarantees and can be just freely reinitialized when React decides to do so (which includes recomputation during Fast Refresh)?

My code example above had a silly mistake in the deps array. Should have been this:

const container = useMemo(() => document.createElement("div"), []);

useLayoutEffect(() => {
  modalRoot.append(container);
  return () => container.remove();
}, [container]);

If React did "forget" the memoized element between renders, the effect would clean up the old one and reparent the new one.

@josepot
Copy link

josepot commented Oct 20, 2020

The reason that sandbox would warn with the semantics on this PR is not because of the lazy init pattern, but because of the read further down on line 60 (passing ref.current to createPortal).

But since that ref would always yield the same value, that would be a "false positive", correct? I mean, as long as the render function always behaves the same for the same props, state and context, then it's ok to read a value from a ref, right?

@bvaughn
Copy link
Contributor Author

bvaughn commented Oct 20, 2020

Yes, it would be a false positive in this case.

The danger with refs is that they can be passed around and mutated outside of React's awareness (unlike state/reducer).

We could change the warning semantics to only warn on read if the ref was mutated after being initialized (not including lazy-init pattern). The danger there is that maybe there's a code path that triggers a second mutation that just doesn't run often. Maybe you'd rarely or never see it in dev. So you have a false sense of confidence that your component is safe, when really there's an unsafe read that might cause a bug in production if this infrequent code path gets triggered.

@josepot
Copy link

josepot commented Oct 20, 2020

We could change the warning semantics to only warn on read if the ref was mutated after being initialized (not including lazy-init pattern). The danger there is that maybe there's a code path that triggers a second mutation that just doesn't run often. Maybe you'd rarely or never see it in dev. So you have a false sense of confidence that your component is safe, when really there's an unsafe read that might cause a bug in production if this infrequent code path gets triggered.

Sorry, my intention was not to criticize the work done on this PR. I agree that it's better to err on the side of caution.

I made that comment because I wasn't sure if you were proposing those alternatives because you deemed the original code to be unsafe or because you wanted to highlight alternative implementations that accomplished the same thing while avoiding warnings.

Thanks a lot for clarifying that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.