Skip to content

Conversation

@acdlite
Copy link
Collaborator

@acdlite acdlite commented Jul 24, 2016

workInProgress (yay puns)

Implements setState for Fiber. Updates are scheduled by setting a work priority on the fiber and bubbling it to the root. Because the instance does not know which tree is current at any given time, the update is scheduled on both fiber alternates.

Need to add more unit tests to cover edge cases.

A few questions I have:

  • How to deal with the circular dependency between ReactFiberScheduler and ReactFiberBeginWork. I'm using a closure to lazily access the scheduler but seems like there may be a better way.
  • How to queue update callbacks. The only way I could think to do it was to add another field to Fiber, so I've punted on this for now.
  • How to access the fiber from the updater. I decided to use a private field on the instance. Could also create a new updater for each instance and close over its fiber, but that seems wasteful.
  • Should I put setState related tests in their own module? I think I should but not sure.

(I discussed with @sebmarkbage before working on this PR.)

Based on #7448. Compare view: sebmarkbage/react@fiberstarvation...acdlite:fibersetstate

enqueueSetState(instance, partialState) {
const fiber = instance._fiber;

const prevState = fiber.pendingState || fiber.memoizedState;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure using fiber.memoizedState here is a bug because we don't know if fiber is current. But, I don't know how to get the current state. It would be nice if we could rely on instance.state but that won't work in the case of an aborted update.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will have the same problem with getting the current props for the updater form of setState((state, props) => newState).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's why this will need to become a queue of state transitions for those cases.

You may also need to be able to schedule different priority level state transitions on the same component. In that case the queue can be reordered. We can wait on that though. Not sure it is worth the trouble.

Can pendingState just use the partial state and that way you can do the merge at the time you apply it?

@sebmarkbage
Copy link
Collaborator

We'll figure something out with the dependency later. One technique is to just put everything in the same file and then try to break it apart again into reasonable pieces.

I figured that the simple pendingState could an object, and then if you use setState((state, props) => ...); or the callback model or it would turn into an array or linked list.

To access the fiber from the updater, you can use a field. That's what we do now. However, you could switch the implementation to use ReactInstanceMap which the current implementation does so we can share that code. Just move it to: src/renderers/shared/shared/ to indicate that we would like to share it between both implementations.

@sebmarkbage
Copy link
Collaborator

As for the unit tests, we can keep them in Incremental for now. In the near future we'll just run this against the existing unit tests for setState. We'll evolve the tests you're writing now into tests that specifically tests the new incremental effects of setState.

@acdlite
Copy link
Collaborator Author

acdlite commented Jul 24, 2016

Ah keeping a queue of pending states makes sense. A linked list sounds like the way to go, but how to disambiguate between a state object and a linked list? $$typeof?

@sebmarkbage
Copy link
Collaborator

Always wrapping it in a linked list node might be fine. Should be short lived and not a very hot path.

On Jul 24, 2016, at 4:44 PM, Andrew Clark notifications@github.com wrote:

Ah keeping a queue of pending states makes sense. A linked list sounds like the way to go, but how to disambiguate between a state object and a linked list? $$typeof?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

@acdlite
Copy link
Collaborator Author

acdlite commented Jul 25, 2016

Changed the type of pendingState to be a queue.

Regarding typing, I'm guessing eventually the type of the props and state will be inferred from the element? For now I've used any because that's what props uses, but I know that's bad.

Also not sure about naming. pendingState is confusing because it's a queue of partial states, whereas pendingProps is a single props object.

@acdlite
Copy link
Collaborator Author

acdlite commented Jul 25, 2016

Maybe rename pendingState to stateQueue?

@sebmarkbage
Copy link
Collaborator

Sure. Wouldn't worry too much about it yet until all the pieces are there and have proven themselves.

It's quite possible that props will become a queue too if we want to solve the problem of props not being consistent when an event happens.

On Jul 24, 2016, at 11:04 PM, Andrew Clark notifications@github.com wrote:

Maybe rename pendingState to stateQueue?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

// is current.
scheduleUpdate(fiber, pendingState, LowPriority);
if (fiber.alternate) {
scheduleUpdate(fiber.alternate, pendingState, LowPriority);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than bubble up the priority both trees, could we bubble once and assign to the alternate at each level? I think so...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's the only correct way to do it, because .return is not guaranteed to point to two different nodes even for two children.

These are not two entirely parallel trees. They can overlap. Two parent fibers can point to the same exact child fiber when the child gets reused.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm confused. I see your point about how the trees aren't parallel, but given that, isn't bubbling up both trees the only way? The first part of your comment seems to contradict the second.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent path is guaranteed to be the same conceptual component path. However, you can't rely on any particular node being the current one along the path.

So this is safe:

while (node) {
  addState(node);
  addState(node.alternate);
  node = node.return;
}

But this is not safe, because it is not guaranteed to cover every node:

node = start;
while (node) {
  addState(node);
  node = node.return;
}
node = start.alternate;
while (node) {
  addState(node);
  node = node.return;
}

An example when this happens:

// Assume the cycles are resolved
a1 = { child: b1, return: null, alternate: a2 };
a2 = { child: b2, return: null, alternate: a1 };
b1 = { child: c1, return: a1, alternate: b2 };
b2 = { child: c1, return: a2, alternate: b1 };
c1 = { child: null, return: b1, alternate: c2 };
c2 = { child: null, return: b1, alternate: c1 }; // Note that the return is the same b1.

Since two versions of a fiber can point to the same child, it is not possible for a parent pointer alone to point to every parent since there can be multiple.

Conceptually this pointer is used for two purposes. A pointer to the parent "Instance" which I've merged into the Fiber. See the type definition.

However, it is also used as a temporary pointer for knowing which fiber to step back through after processing.

If I wasn't so unnecessarily clever about allocations the structure would like this:

type Instance = { parent: ?Instance };
type Fiber = { inst: Instance, return: ?Fiber };

and you would just walk the parent pointer. However, I'm being clever to see how far we can get but it adds some complexity.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooh, okay. I think I get it now! Thanks for the detailed explanation (and for being patient as I try to keep up, haha).

@ghost ghost added the CLA Signed label Aug 1, 2016
@ghost ghost added the CLA Signed label Aug 2, 2016
@acdlite
Copy link
Collaborator Author

acdlite commented Aug 2, 2016

A few things left to do.

We need a separate field for the priority of a fiber's pending props and state, distinct from the existing field pendingWorkPriority, which represents the highest priority of the entire subtree. Otherwise, there's no way to schedule work deep in the tree without overriding the priority of every ancestor. I got this working locally but I'm holding off until @sebmarkbage fixes some priority-related bugs on his branch.

We also need more tests, specifically around preempted updates. In order to do this, we need to implement high priority updates. @sebmarkbage's idea is an API like ReactNoop.performHighPriWork(fn) where all updates called within that scope have high priority by default.

@ghost ghost added the CLA Signed label Aug 2, 2016
@ghost ghost added the CLA Signed label Aug 3, 2016
@ghost ghost added the CLA Signed label Aug 3, 2016
@ghost ghost added the CLA Signed label Aug 5, 2016
if (fiber.callbackList) {
// If this fiber was preempted and already has callbacks queued up,
// we need to make sure they are part of the new update queue
updateQueue = concatQueues(fiber.callbackList, updateQueue);
Copy link
Collaborator Author

@acdlite acdlite Aug 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, this isn't right because this also adds the previous state updates to the beginning of the queue! This causes the updates to be applied twice, because the updates were already used to compute the previous memoized state. I believe this is unobservable unless there are side-effects inside state updaters (which there shouldn't be), but it's wasteful regardless.

Should instead combine the callbacks into a single callback, add to an empty node, and prepend it to the update queue.

@acdlite
Copy link
Collaborator Author

acdlite commented Aug 6, 2016

@sebmarkbage Rebased

@ghost ghost added the CLA Signed label Aug 6, 2016
@ghost ghost added the CLA Signed label Sep 6, 2016
@coveralls
Copy link

Coverage Status

Coverage increased (+0.005%) to 87.196% when pulling 319e755 on acdlite:fibersetstate into 6a525fd on facebook:master.

acdlite and others added 15 commits September 13, 2016 15:26
Updates are scheduled by setting a work priority on the fiber and bubbling it to
the root. Because the instance does not know which tree is current at any given
time, the update is scheduled on both fiber alternates.

Need to add more unit tests to cover edge cases.
Changes the type of pendingState to be a linked list of partial
state objects.
Add support for setState((state, props) => newState).

Rename pendingState to stateQueue.
Rather than bubble up both trees, bubble up once and assign to the
alternate at each level.

Extract logic for adding to the queue to the StateQueue module.
Use a union type for the head of StateQueue.
Move ReactInstanceMap to src/renderers/shared/shared to indicate that
this logic is shared across implementations.
Also cleans up some types.
Callbacks are stored on the same queue as updates. They care called
during the commit phase, after the updates have been flushed.

Because the update queue is cleared during the completion phase (before
commit), a new field has been added to fiber called callbackList. The
queue is transferred from updateQueue to callbackList during completion.
During commit, the list is reset.

Need a test to confirm that callbacks are not lost if an update is
preempted.
Adds a field to UpdateQueue that indicates whether an update should
replace the previous state completely.
Adds a field to the update queue that causes shouldComponentUpdate to
be skipped.
We should be able to abort an update without any side-effects to the
current tree. This fixes a few cases where that was broken.

The callback list should only ever be set on the workInProgress.
There's no reason to add it to the current tree because they're not
needed after they are called during the commit phase.

Also found a bug where the memoizedProps were set to null in the
case of an update, because the pendingProps were null. Fixed by
transfering the props from the instance, like we were already doing
with state.

Added a test to ensure that setState can be called inside a
callback.
This is unfortunate since we agreed on using the `null | Fiber`
convention instead of `?Fiber` but haven't upgraded yet and this
is the pattern I've been using everywhere else so far.
This also buffers all rows into a single console.log call.
This is because jest nows adds the line number of each console.log
call which becomes quite noisy for these trees.
@sebmarkbage
Copy link
Collaborator

Ninja merging this. Will fix travis if errors are caused. #workedonmymachine

@sebmarkbage sebmarkbage merged commit 3e54b28 into facebook:master Sep 13, 2016
@zpao zpao modified the milestone: 15-next Sep 19, 2016
@zpao zpao modified the milestones: 15-next, 15.4.0 Oct 4, 2016
zpao pushed a commit that referenced this pull request Oct 4, 2016
[Fiber] setState
(cherry picked from commit 3e54b28)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants