mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
afec07aca2
Summary: changelog: [internal] `useLayoutEffect` has two guarantees which React Native breaks: - Layout metrics are ready. - Updates triggered inside `useLayoutEffect` are applied before paint. State between first commit and update is not shown on the screen. React Native breaks the first guarantee because it uses Background Executor. Background executor moves Yoga layout to another thread. If user core reads layout metrics in `useLayoutEffect` hook, it is a race. The information might be there, or it might not. They can even read partially update information. This diff does not affect this. We already have a way to turn off Background Executor. We haven't done this because it introduces regressions on one screen but we have a solution for that. React Native breaks the second guarantee. After Fabric's commit phase, Fabric moves to mounting the changes right away. In this diff, we queue the mounting phase and give React a chance to change what is committed to the screen. To do that, we schedule a task with user blocking priority in `RuntimeScheduler`. React, if there is an update in `useLayoutEffect`, schedules a task in the scheduler with higher priority and stops the mounting phase. We are not delaying mounting, this just gives React a chance to interrupt it. Fabric commit phase may be triggered by different mechanisms. C++ state update, surface tear down, template update (not used atm), setNativeProps, to name a few. Fabric only needs to block paint if commit originates from React. Otherwise the scheduling is wrong and we will get into undefined behaviour land. Rollout: This change is gated behind `react_fabric:block_paint_for_use_layout_effect` and will be rolled out incrementally. Reviewed By: javache Differential Revision: D43083051 fbshipit-source-id: bb494cf56a11763e38dce7ba0093c4dafdd8bf43
181 lines
5.9 KiB
C++
181 lines
5.9 KiB
C++
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#include <memory>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
|
#include <react/renderer/components/view/ViewComponentDescriptor.h>
|
|
#include <react/renderer/core/PropsParserContext.h>
|
|
#include <react/renderer/element/ComponentBuilder.h>
|
|
#include <react/renderer/element/Element.h>
|
|
#include <react/renderer/mounting/MountingCoordinator.h>
|
|
#include <react/renderer/mounting/ShadowTree.h>
|
|
#include <react/renderer/mounting/ShadowTreeDelegate.h>
|
|
|
|
#include <react/renderer/element/testUtils.h>
|
|
|
|
using namespace facebook::react;
|
|
|
|
class DummyShadowTreeDelegate : public ShadowTreeDelegate {
|
|
public:
|
|
RootShadowNode::Unshared shadowTreeWillCommit(
|
|
ShadowTree const & /*shadowTree*/,
|
|
RootShadowNode::Shared const & /*oldRootShadowNode*/,
|
|
RootShadowNode::Unshared const &newRootShadowNode) const override {
|
|
return newRootShadowNode;
|
|
};
|
|
|
|
void shadowTreeDidFinishTransaction(
|
|
MountingCoordinator::Shared mountingCoordinator,
|
|
bool mountSynchronously) const override{};
|
|
};
|
|
|
|
inline ShadowNode const *findDescendantNode(
|
|
ShadowNode const &shadowNode,
|
|
ShadowNodeFamily const &family) {
|
|
ShadowNode const *result = nullptr;
|
|
shadowNode.cloneTree(family, [&](ShadowNode const &oldShadowNode) {
|
|
result = &oldShadowNode;
|
|
return oldShadowNode.clone({});
|
|
});
|
|
return result;
|
|
}
|
|
|
|
inline ShadowNode const *findDescendantNode(
|
|
ShadowTree const &shadowTree,
|
|
ShadowNodeFamily const &family) {
|
|
return findDescendantNode(
|
|
*shadowTree.getCurrentRevision().rootShadowNode, family);
|
|
}
|
|
|
|
TEST(StateReconciliationTest, testStateReconciliation) {
|
|
auto builder = simpleComponentBuilder();
|
|
|
|
auto shadowNodeA = std::shared_ptr<RootShadowNode>{};
|
|
auto shadowNodeAA = std::shared_ptr<ViewShadowNode>{};
|
|
auto shadowNodeAB = std::shared_ptr<ScrollViewShadowNode>{};
|
|
auto shadowNodeABA = std::shared_ptr<ViewShadowNode>{};
|
|
auto shadowNodeABB = std::shared_ptr<ViewShadowNode>{};
|
|
auto shadowNodeABC = std::shared_ptr<ViewShadowNode>{};
|
|
|
|
// clang-format off
|
|
auto element =
|
|
Element<RootShadowNode>()
|
|
.reference(shadowNodeA)
|
|
.finalize([](RootShadowNode &shadowNode){
|
|
shadowNode.sealRecursive();
|
|
})
|
|
.children({
|
|
Element<ViewShadowNode>()
|
|
.reference(shadowNodeAA),
|
|
Element<ScrollViewShadowNode>()
|
|
.reference(shadowNodeAB)
|
|
.children({
|
|
Element<ViewShadowNode>()
|
|
.children({
|
|
Element<ViewShadowNode>()
|
|
.reference(shadowNodeABA),
|
|
Element<ViewShadowNode>()
|
|
.reference(shadowNodeABB),
|
|
Element<ViewShadowNode>()
|
|
.reference(shadowNodeABC)
|
|
})
|
|
})
|
|
});
|
|
// clang-format on
|
|
|
|
ContextContainer contextContainer{};
|
|
|
|
auto shadowNode = builder.build(element);
|
|
|
|
auto rootShadowNodeState1 = shadowNode->ShadowNode::clone({});
|
|
|
|
auto &scrollViewComponentDescriptor = shadowNodeAB->getComponentDescriptor();
|
|
auto &family = shadowNodeAB->getFamily();
|
|
auto state1 = shadowNodeAB->getState();
|
|
auto shadowTreeDelegate = DummyShadowTreeDelegate{};
|
|
ShadowTree shadowTree{
|
|
SurfaceId{11},
|
|
LayoutConstraints{},
|
|
LayoutContext{},
|
|
shadowTreeDelegate,
|
|
contextContainer};
|
|
|
|
shadowTree.commit(
|
|
[&](RootShadowNode const & /*oldRootShadowNode*/) {
|
|
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState1);
|
|
},
|
|
{true});
|
|
|
|
EXPECT_EQ(state1->getMostRecentState(), state1);
|
|
|
|
EXPECT_EQ(
|
|
findDescendantNode(*rootShadowNodeState1, family)->getState(), state1);
|
|
|
|
auto state2 = scrollViewComponentDescriptor.createState(
|
|
family, std::make_shared<ScrollViewState const>());
|
|
|
|
auto rootShadowNodeState2 =
|
|
shadowNode->cloneTree(family, [&](ShadowNode const &oldShadowNode) {
|
|
return oldShadowNode.clone(
|
|
{ShadowNodeFragment::propsPlaceholder(),
|
|
ShadowNodeFragment::childrenPlaceholder(),
|
|
state2});
|
|
});
|
|
|
|
EXPECT_EQ(
|
|
findDescendantNode(*rootShadowNodeState2, family)->getState(), state2);
|
|
|
|
shadowTree.commit(
|
|
[&](RootShadowNode const & /*oldRootShadowNode*/) {
|
|
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState2);
|
|
},
|
|
{true});
|
|
|
|
EXPECT_EQ(state1->getMostRecentState(), state2);
|
|
EXPECT_EQ(state2->getMostRecentState(), state2);
|
|
|
|
auto state3 = scrollViewComponentDescriptor.createState(
|
|
family, std::make_shared<ScrollViewState const>());
|
|
|
|
auto rootShadowNodeState3 = rootShadowNodeState2->cloneTree(
|
|
family, [&](ShadowNode const &oldShadowNode) {
|
|
return oldShadowNode.clone(
|
|
{ShadowNodeFragment::propsPlaceholder(),
|
|
ShadowNodeFragment::childrenPlaceholder(),
|
|
state3});
|
|
});
|
|
|
|
EXPECT_EQ(
|
|
findDescendantNode(*rootShadowNodeState3, family)->getState(), state3);
|
|
|
|
shadowTree.commit(
|
|
[&](RootShadowNode const & /*oldRootShadowNode*/) {
|
|
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState3);
|
|
},
|
|
{true});
|
|
|
|
EXPECT_EQ(findDescendantNode(shadowTree, family)->getState(), state3);
|
|
|
|
EXPECT_EQ(state1->getMostRecentState(), state3);
|
|
EXPECT_EQ(state2->getMostRecentState(), state3);
|
|
EXPECT_EQ(state3->getMostRecentState(), state3);
|
|
|
|
// This is the core part of the whole test.
|
|
// Here we commit the old tree but we expect that the state associated with
|
|
// the node will stay the same (newer that the old tree has).
|
|
shadowTree.commit(
|
|
[&](RootShadowNode const & /*oldRootShadowNode*/) {
|
|
return std::static_pointer_cast<RootShadowNode>(rootShadowNodeState2);
|
|
},
|
|
{true});
|
|
|
|
EXPECT_EQ(findDescendantNode(shadowTree, family)->getState(), state3);
|
|
}
|