Skip to content
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

Adding 0.1 incrementally with setState gives out approximate numbers #118

Closed
ldhieu opened this issue Jun 21, 2013 · 2 comments
Closed

Adding 0.1 incrementally with setState gives out approximate numbers #118

ldhieu opened this issue Jun 21, 2013 · 2 comments

Comments

@ldhieu
Copy link

ldhieu commented Jun 21, 2013

I'm not sure whether this is as intended, or a problem with JavaScript, but I fiddled around with your example on facebook.github.io/react and tried this code:

var Timer = React.createClass({
  getInitialState: function() {
    return {secondsElapsed: 0};
  },
  tick: React.autoBind(function() {
    this.setState({secondsElapsed: this.state.secondsElapsed + 0.1});
  }),
  componentDidMount: function() {
    setInterval(this.tick, 100);
  },
  render: function() {
    return React.DOM.div({},
      'Seconds Elapsed: ' + this.state.secondsElapsed
    );
  }
});

React.renderComponent(Timer({}), mountNode);

The result comes out as 0.1, 0.2, ... as intended, but at some point the adding will give out numbers such as 0.30000000004 or 0.79999999999

Hope this explains well.

Hieu

@sophiebits
Copy link
Collaborator

This is standard behavior for JavaScript (unrelated to React), and is expected.

image

Floating-point numbers such as 0.1 and 0.2 can't be represented exactly in binary, so an approximation is used. It's as if you (in base 10) added 0.333 + 0.333 + 0.333 and got 0.999 instead of 1 -- just an artifact of the rounding that happens. Here's a Stack Overflow question explaining this phenomenon: http://stackoverflow.com/q/588004/49485

@zpao
Copy link
Member

zpao commented Jun 24, 2013

@ldhieu Yea, this is expected behavior with JS. One common solution when working with decimals (eg, currency) is to just use whole numbers, then divide by some number when you need to display the number to a use. $1.01 would be stored as 101 and then when displaying that to the user you would do num / 100.

@zpao zpao closed this as completed Jun 24, 2013
bvaughn added a commit to bvaughn/react that referenced this issue Aug 13, 2019
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.

Currently, we propagate these changes eagerly, at the provider.

However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.

We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.

Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.

Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.

There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.

I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.

I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.

---

This is largely based on [RFC facebook#118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.

The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.

This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.

I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.

Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants