diff --git a/ReactNative/ReactNative.csproj b/ReactNative/ReactNative.csproj index 928cf6469a2..d1187f84482 100644 --- a/ReactNative/ReactNative.csproj +++ b/ReactNative/ReactNative.csproj @@ -244,13 +244,13 @@ - + + - @@ -262,8 +262,8 @@ - - + + diff --git a/ReactNative/Shell/MainReactPackage.cs b/ReactNative/Shell/MainReactPackage.cs index 120303e99d7..267b04fd0f3 100644 --- a/ReactNative/Shell/MainReactPackage.cs +++ b/ReactNative/Shell/MainReactPackage.cs @@ -2,6 +2,7 @@ using ReactNative.Modules.Core; using ReactNative.Modules.WebSocket; using ReactNative.UIManager; +using ReactNative.Views.Scroll; using ReactNative.Views.Text; using ReactNative.Views.TextInput; using ReactNative.Views.View; @@ -39,7 +40,7 @@ public IReadOnlyList CreateViewManagers( //new ReactProgressBarViewManager(), new ReactRawTextManager(), //new RecyclerViewBackedScrollViewManager(), - //new ReactScrollViewManager(), + new ReactScrollViewManager(), //new ReactSwitchManager(), new ReactTextInputManager(), new ReactTextViewManager(), diff --git a/ReactNative/UIManager/NativeViewHierarchyManager.cs b/ReactNative/UIManager/NativeViewHierarchyManager.cs index 5409535b0ab..e7d75264fe5 100644 --- a/ReactNative/UIManager/NativeViewHierarchyManager.cs +++ b/ReactNative/UIManager/NativeViewHierarchyManager.cs @@ -133,8 +133,23 @@ public void UpdateLayout(int parentTag, int tag, int x, int y, int width, int he .With("tag", tag)) { var viewToUpdate = ResolveView(tag); - // TODO: call viewToUpdate.Measure() - //throw new NotImplementedException(); + + var parentViewManager = default(ViewManager); + var parentViewGroupManager = default(ViewGroupManager); + if (!_tagsToViewManagers.TryGetValue(parentTag, out parentViewManager) || + (parentViewGroupManager = parentViewManager as ViewGroupManager) == null) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Trying to use view with tag '{0}' as a parent, but its manager doesn't extend ViewGroupManager.", + tag)); + } + + if (!parentViewGroupManager.NeedsCustomLayoutForChildren) + { + UpdateLayout(viewToUpdate, x, y, width, height); + } } } @@ -189,7 +204,7 @@ public void ManageChildren(int tag, int[] indicesToRemove, ViewAtIndex[] viewsTo } var viewGroupManager = (ViewGroupManager)viewManager; - var viewToManage = (Panel)_tagsToViews[tag]; + var viewToManage = _tagsToViews[tag]; var lastIndexToRemove = viewGroupManager.GetChildCount(viewToManage); if (indicesToRemove != null) @@ -513,5 +528,13 @@ private void DropView(FrameworkElement view) _tagsToViews.Remove(tag); _tagsToViewManagers.Remove(tag); } + + private void UpdateLayout(FrameworkElement viewToUpdate, int x, int y, int width, int height) + { + viewToUpdate.Width = width; + viewToUpdate.Height = height; + Canvas.SetLeft(viewToUpdate, x); + Canvas.SetTop(viewToUpdate, y); + } } } diff --git a/ReactNative/UIManager/NativeViewHierarchyOptimizer.cs b/ReactNative/UIManager/NativeViewHierarchyOptimizer.cs index 642db078d0f..61c5ad5b6b7 100644 --- a/ReactNative/UIManager/NativeViewHierarchyOptimizer.cs +++ b/ReactNative/UIManager/NativeViewHierarchyOptimizer.cs @@ -79,7 +79,7 @@ public void HandleCreateView( _uiViewOperationQueue.EnqueueCreateView( themedContext, node.ReactTag, - node.ViewClassName, + node.ViewClass, initialProperties); #else var isLayoutOnly = node.ViewClass == ViewProperties.ViewClassName @@ -376,7 +376,7 @@ private void ApplyLayoutRecursive(ReactShadowNode node, int x, int y) if (!node.IsLayoutOnly && node.NativeParent != null) { _uiViewOperationQueue.EnqueueUpdateLayout( - node.Parent.ReactTag, + node.NativeParent.ReactTag, node.ReactTag, x, y, diff --git a/ReactNative/UIManager/ViewGroupManager.Generic.cs b/ReactNative/UIManager/PanelViewGroupManager.cs similarity index 66% rename from ReactNative/UIManager/ViewGroupManager.Generic.cs rename to ReactNative/UIManager/PanelViewGroupManager.cs index b9016a51374..ab9abc5332c 100644 --- a/ReactNative/UIManager/ViewGroupManager.Generic.cs +++ b/ReactNative/UIManager/PanelViewGroupManager.cs @@ -9,7 +9,7 @@ namespace ReactNative.UIManager /// extending . /// /// Type of panel. - public abstract class ViewGroupManager : ViewGroupManager + public abstract class PanelViewGroupManager : ViewGroupManager where TPanel : Panel { /// @@ -53,6 +53,57 @@ public sealed override void UpdateExtraData(FrameworkElement root, object extraD UpdateExtraData((TPanel)root, extraData); } + /// + /// Gets the number of children in the view group. + /// + /// The view group. + /// The number of children. + public sealed override int GetChildCount(FrameworkElement parent) + { + return GetChildCount((TPanel)parent); + } + + /// + /// Gets the child at the given index. + /// + /// The parent view. + /// The index. + /// The child view. + public sealed override FrameworkElement GetChildAt(FrameworkElement parent, int index) + { + return GetChildAt((TPanel)parent, index); + } + + /// + /// Adds a child at the given index. + /// + /// The parent view. + /// The child view. + /// The index. + public sealed override void AddView(FrameworkElement parent, FrameworkElement child, int index) + { + AddView((TPanel)parent, child, index); + } + + /// + /// Removes the child at the given index. + /// + /// The view group. + /// The index. + public override void RemoveChildAt(FrameworkElement parent, int index) + { + RemoveChildAt((TPanel)parent, index); + } + + /// + /// Removes all children from the view group. + /// + /// The view group. + public override void RemoveAllChildren(FrameworkElement parent) + { + RemoveAllChildren((TPanel)parent); + } + /// /// Subclasses can override this method to install custom event /// emitters on the given view. @@ -172,5 +223,56 @@ protected virtual void ReceiveCommand(TPanel view, int commandId, JArray args) protected virtual void UpdateExtraData(TPanel root, object extraData) { } + + /// + /// Gets the number of children in the view group. + /// + /// The view group. + /// The number of children. + protected virtual int GetChildCount(TPanel parent) + { + return parent.Children.Count; + } + + /// + /// Gets the child at the given index. + /// + /// The parent view. + /// The index. + /// The child view. + protected virtual FrameworkElement GetChildAt(TPanel parent, int index) + { + return (FrameworkElement)parent.Children[index]; + } + + /// + /// Adds a child at the given index. + /// + /// The parent view. + /// The child view. + /// The index. + protected virtual void AddView(TPanel parent, FrameworkElement child, int index) + { + parent.Children.Insert(index, child); + } + + /// + /// Removes the child at the given index. + /// + /// The view group. + /// The index. + protected virtual void RemoveChildAt(TPanel parent, int index) + { + parent.Children.RemoveAt(index); + } + + /// + /// Removes all children from the view group. + /// + /// The view group. + protected virtual void RemoveAllChildren(TPanel parent) + { + parent.Children.Clear(); + } } } diff --git a/ReactNative/UIManager/RootViewManager.cs b/ReactNative/UIManager/RootViewManager.cs index f2aebefd5ad..c1b5c7c8f4d 100644 --- a/ReactNative/UIManager/RootViewManager.cs +++ b/ReactNative/UIManager/RootViewManager.cs @@ -1,11 +1,9 @@ -using Windows.UI.Xaml; - -namespace ReactNative.UIManager +namespace ReactNative.UIManager { /// /// View manager for react root view components. /// - public class RootViewManager : ViewGroupManager + public class RootViewManager : PanelViewGroupManager { /// /// The name of the react root view. @@ -23,7 +21,7 @@ public override string Name /// /// The react context. /// The view instance. - protected override FrameworkElement CreateViewInstance(ThemedReactContext reactContext) + protected override SizeMonitoringPanel CreateViewInstanceCore(ThemedReactContext reactContext) { return new SizeMonitoringPanel(); } diff --git a/ReactNative/UIManager/UIImplementation.cs b/ReactNative/UIManager/UIImplementation.cs index 507661dbb8f..8509811e0dc 100644 --- a/ReactNative/UIManager/UIImplementation.cs +++ b/ReactNative/UIManager/UIImplementation.cs @@ -463,6 +463,8 @@ public void MeasureLayoutRelativeToParent(int tag, ICallback errorCallback, ICal /// The batch identifier. public void DispatchViewUpdates(EventDispatcher eventDispatcher, int batchId) { + DispatcherHelpers.AssertOnDispatcher(); + foreach (var tag in _shadowNodeRegistry.RootNodeTags) { var cssRoot = _shadowNodeRegistry.GetNode(tag); @@ -472,7 +474,7 @@ public void DispatchViewUpdates(EventDispatcher eventDispatcher, int batchId) } _nativeViewHierarchyOptimizer.OnBatchComplete(); - _operationsQueue.DispatchViewUpdates(batchId); + _operationsQueue.ExecuteOperations(batchId); } /// diff --git a/ReactNative/UIManager/UIManagerModule.cs b/ReactNative/UIManager/UIManagerModule.cs index a91ee14ef29..7b8ef01689f 100644 --- a/ReactNative/UIManager/UIManagerModule.cs +++ b/ReactNative/UIManager/UIManagerModule.cs @@ -387,13 +387,16 @@ public void OnBatchComplete() { var batchId = _batchId++; - using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "onBatchCompleteUI") - .With("BatchId", batchId)) + Context.RunOnDispatcherQueueThread(() => { - _uiImplementation.DispatchViewUpdates(_eventDispatcher, batchId); - } - - // TODO: coordinate with UI operations? + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "onBatchCompleteUI") + .With("BatchId", batchId)) + { + _uiImplementation.DispatchViewUpdates(_eventDispatcher, batchId); + } + }); + + // TODO: coordinate with UI operations a la choreographer? _eventDispatcher.OnBatchComplete(); } diff --git a/ReactNative/UIManager/UIViewOperationQueue.cs b/ReactNative/UIManager/UIViewOperationQueue.cs index 7a5f4c143ec..77ec05e3fb4 100644 --- a/ReactNative/UIManager/UIViewOperationQueue.cs +++ b/ReactNative/UIManager/UIViewOperationQueue.cs @@ -312,8 +312,10 @@ public void EnqueueFindTargetForTouch( /// Dispatches the view updates. /// /// The batch identifier. - internal void DispatchViewUpdates(int batchId) + internal void ExecuteOperations(int batchId) { + DispatcherHelpers.AssertOnDispatcher(); + var operations = default(IList); lock (_operationsLock) { @@ -324,20 +326,17 @@ internal void DispatchViewUpdates(int batchId) } } - _reactContext.RunOnDispatcherQueueThread(() => + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "DispatchUI") + .With("BatchId", batchId)) { - using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "DispatchUI") - .With("BatchId", batchId)) + if (operations != null) { - if (operations != null) + foreach (var operation in operations) { - foreach (var operation in operations) - { - operation(); - } + operation(); } } - }); + } } private void EnqueueOperation(Action action) diff --git a/ReactNative/UIManager/ViewGroupManager.cs b/ReactNative/UIManager/ViewGroupManager.cs index ac6b202e4c1..97b8d97c155 100644 --- a/ReactNative/UIManager/ViewGroupManager.cs +++ b/ReactNative/UIManager/ViewGroupManager.cs @@ -5,8 +5,7 @@ namespace ReactNative.UIManager { /// - /// Class providing child management API for view managers of classes - /// extending . + /// Class providing child management API for view managers. /// public abstract class ViewGroupManager : ViewManager { @@ -57,20 +56,14 @@ public override void UpdateExtraData(FrameworkElement root, object extraData) /// The parent view. /// The child view. /// The index. - public virtual void AddView(Panel parent, UIElement child, int index) - { - parent.Children.Insert(index, child); - } + public abstract void AddView(FrameworkElement parent, FrameworkElement child, int index); /// /// Gets the number of children in the view group. /// /// The view group. /// The number of children. - public virtual int GetChildCount(Panel parent) - { - return parent.Children.Count; - } + public abstract int GetChildCount(FrameworkElement parent); /// /// Gets the child at the given index. @@ -78,28 +71,19 @@ public virtual int GetChildCount(Panel parent) /// The parent view. /// The index. /// The child view. - public virtual FrameworkElement GetChildAt(Panel parent, int index) - { - return (FrameworkElement)parent.Children[index]; - } + public abstract FrameworkElement GetChildAt(FrameworkElement parent, int index); /// /// Removes the child at the given index. /// /// The view group. /// The index. - public virtual void RemoveChildAt(Panel parent, int index) - { - parent.Children.RemoveAt(index); - } + public abstract void RemoveChildAt(FrameworkElement parent, int index); /// /// Removes all children from the view group. /// /// The view group. - public virtual void RemoveAllChildren(Panel parent) - { - parent.Children.Clear(); - } + public abstract void RemoveAllChildren(FrameworkElement parent); } } diff --git a/ReactNative/UIManager/ViewProperties.cs b/ReactNative/UIManager/ViewProperties.cs index 9d03398f3be..b17e08490ec 100644 --- a/ReactNative/UIManager/ViewProperties.cs +++ b/ReactNative/UIManager/ViewProperties.cs @@ -10,7 +10,6 @@ namespace ReactNative.UIManager public static class ViewProperties { public const string ViewClassName = "RCTView"; - public const string TextInputClassName = "RCTTextField"; // Layout only (only affect positions of children, causes no drawing) // !!! Keep in sync with s_layoutOnlyProperties below !!! diff --git a/ReactNative/Views/Scroll/ReactScrollViewManager.cs b/ReactNative/Views/Scroll/ReactScrollViewManager.cs new file mode 100644 index 00000000000..955a7b5a62c --- /dev/null +++ b/ReactNative/Views/Scroll/ReactScrollViewManager.cs @@ -0,0 +1,49 @@ +using ReactNative.UIManager; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace ReactNative.Views.Scroll +{ + public class ReactScrollViewManager : ViewGroupManager + { + private const string ReactClass = "RCTScrollView"; + + public override string Name + { + get + { + return ReactClass; + } + } + + public override void AddView(FrameworkElement parent, FrameworkElement child, int index) + { + ((ListView)parent).Items.Insert(index, child); + } + + public override FrameworkElement GetChildAt(FrameworkElement parent, int index) + { + return (FrameworkElement)((ListView)parent).Items[index]; + } + + public override int GetChildCount(FrameworkElement parent) + { + return ((ListView)parent).Items.Count; + } + + public override void RemoveAllChildren(FrameworkElement parent) + { + ((ListView)parent).Items.Clear(); + } + + public override void RemoveChildAt(FrameworkElement parent, int index) + { + ((ListView)parent).Items.RemoveAt(index); + } + + protected override FrameworkElement CreateViewInstance(ThemedReactContext reactContext) + { + return new ListView(); + } + } +} diff --git a/ReactNative/Views/Text/InlineManager.cs b/ReactNative/Views/Text/InlineManager.cs deleted file mode 100644 index e8227923411..00000000000 --- a/ReactNative/Views/Text/InlineManager.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using Windows.UI.Xaml.Documents; - -namespace ReactNative.Views.Text -{ - abstract class InlineManager - { - private List> _actions = new List>(); - - public void Do(Action action) - { - _actions.Add(action); - } - - protected abstract Inline Create(); - - public Inline Evaluate() - { - var inline = Create(); - - foreach (var action in _actions) - { - action(inline); - } - - foreach (var action in _actions) - { - action(inline); - } - - return inline; - } - } -} diff --git a/ReactNative/Views/Text/ReactTextShadowNode.cs b/ReactNative/Views/Text/ReactTextShadowNode.cs index 5fba37e912c..be3ba4c7702 100644 --- a/ReactNative/Views/Text/ReactTextShadowNode.cs +++ b/ReactNative/Views/Text/ReactTextShadowNode.cs @@ -1,10 +1,13 @@ using Facebook.CSSLayout; +using ReactNative.Bridge; using ReactNative.UIManager; using System; using System.Collections.Generic; using System.Globalization; +using Windows.Foundation; using Windows.UI; using Windows.UI.Text; +using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Documents; using Windows.UI.Xaml.Media; @@ -32,13 +35,18 @@ public class ReactTextShadowNode : LayoutShadowNode private string _fontFamily; private string _text; - private InlineManager _inline; + private Inline _inline; private readonly bool _isVirtual; public ReactTextShadowNode(bool isVirtual) { _isVirtual = isVirtual; + + if (!isVirtual) + { + MeasureFunction = MeasureText; + } } public override bool IsVirtual @@ -59,6 +67,12 @@ public override bool IsVirtualAnchor public override void OnBeforeLayout() { + // We need to perform this operation on the dispatcher in UWP as + // WinRT lacks the tools needed to "predict" the height of text. + // Instead, we simply instantiate the Inline object, insert it into + // a text block, and extract how tall the element will be. + DispatcherHelpers.AssertOnDispatcher(); + if (_isVirtual) { return; @@ -200,6 +214,35 @@ public void SetFontStyle(string fontStyleString) } } + private static MeasureOutput MeasureText(CSSNode node, float width, float height) + { + // This is not a terribly efficient way of projecting the height of + // the text elements. It requires that we have access to the + // dispatcher in order to do measurement, which, for obvious + // reasons, can cause perceived performance issues as it will block + // the UI thread from handling other work. + // + // TODO: determine another way to measure text elements. + + var shadowNode = (ReactTextShadowNode)node; + var textBlock = new TextBlock(); + textBlock.Inlines.Add(shadowNode._inline); + + try + { + var adjustedWidth = float.IsNaN(width) ? double.PositiveInfinity : width; + var adjustedHeight = float.IsNaN(height) ? double.PositiveInfinity : height; + textBlock.Measure(new Size(width, adjustedHeight)); + return new MeasureOutput( + (float)textBlock.DesiredSize.Width, + (float)textBlock.DesiredSize.Height); + } + finally + { + textBlock.Inlines.Clear(); + } + } + private static int ParseNumericFontWeight(string fontWeightString) { return fontWeightString.Length == 3 && fontWeightString.EndsWith("00") && @@ -208,22 +251,22 @@ private static int ParseNumericFontWeight(string fontWeightString) : -1; } - private static InlineManager FromTextCSSNode(ReactTextShadowNode textNode) + private static Inline FromTextCSSNode(ReactTextShadowNode textNode) { return BuildInlineFromTextCSSNode(textNode); } - private static InlineManager BuildInlineFromTextCSSNode(ReactTextShadowNode textNode) + private static Inline BuildInlineFromTextCSSNode(ReactTextShadowNode textNode) { var length = textNode.ChildCount; - var inline = default(InlineManager); + var inline = default(Inline); if (length == 0) { - inline = new RunManager(textNode._text); + inline = new Run { Text = textNode._text }; } else { - var span = new SpanManager(); + var span = new Span(); for (var i = 0; i < length; ++i) { var child = textNode.GetChildAt(i); @@ -238,7 +281,7 @@ private static InlineManager BuildInlineFromTextCSSNode(ReactTextShadowNode text } var childInline = BuildInlineFromTextCSSNode(textChild); - span.Add(childInline); + span.Inlines.Add(childInline); } inline = span; @@ -255,71 +298,34 @@ private static InlineManager BuildInlineFromTextCSSNode(ReactTextShadowNode text color >>= 8; var a = (byte)color; var c = Color.FromArgb(a, r, g, b); - inline.Do(i => i.Foreground = new SolidColorBrush(c)); + inline.Foreground = new SolidColorBrush(c); } if (textNode._fontSize != UNSET) { var fontSize = textNode._fontSize; - inline.Do(i => i.FontSize = fontSize); + inline.FontSize = fontSize; } if (textNode._fontStyle.HasValue) { var fontStyle = textNode._fontStyle.Value; - inline.Do(i => i.FontStyle = fontStyle); + inline.FontStyle = fontStyle; } if (textNode._fontWeight.HasValue) { var fontWeight = textNode._fontWeight.Value; - inline.Do(i => i.FontWeight = fontWeight); + inline.FontWeight = fontWeight; } if (textNode._fontFamily != null) { var fontFamily = new FontFamily(textNode._fontFamily); - inline.Do(i => i.FontFamily = fontFamily); + inline.FontFamily = fontFamily; } return inline; } - - class RunManager : InlineManager - { - private readonly string _text; - - public RunManager(string text) - { - _text = text; - } - - protected override Inline Create() - { - return new Run { Text = _text }; - } - } - - class SpanManager : InlineManager - { - private readonly List _children = new List(); - - public void Add(InlineManager child) - { - _children.Add(child); - } - - protected override Inline Create() - { - var span = new Span(); - - foreach (var child in _children) - { - span.Inlines.Add(child.Evaluate()); - } - - return span; - } - } } } \ No newline at end of file diff --git a/ReactNative/Views/Text/ReactTextViewManager.cs b/ReactNative/Views/Text/ReactTextViewManager.cs index adcae94941f..40042a0f7a7 100644 --- a/ReactNative/Views/Text/ReactTextViewManager.cs +++ b/ReactNative/Views/Text/ReactTextViewManager.cs @@ -1,5 +1,6 @@ using ReactNative.UIManager; using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Documents; namespace ReactNative.Views.Text { @@ -27,8 +28,9 @@ protected override ReactTextShadowNode CreateShadowNodeInstanceCore() protected override void UpdateExtraData(TextBlock root, object extraData) { - var inlineManager = (InlineManager)extraData; - root.Inlines.Add(inlineManager.Evaluate()); + var inline = (Inline)extraData; + + root.Inlines.Add(inline); } } } diff --git a/ReactNative/Views/TextInput/ReactTextInputManager.cs b/ReactNative/Views/TextInput/ReactTextInputManager.cs index 57f2189b644..e656e9f2a04 100644 --- a/ReactNative/Views/TextInput/ReactTextInputManager.cs +++ b/ReactNative/Views/TextInput/ReactTextInputManager.cs @@ -20,7 +20,7 @@ class ReactTextInputManager : BaseViewManager { private static readonly int FOCUS_TEXT_INPUT = 1; private static readonly int BLUR_TEXT_INPUT = 2; - private static readonly string REACT_CLASS = ViewProperties.TextInputClassName; + private static readonly string REACT_CLASS = "RCTTextField"; private const string PROP_ROTATION_X = "rotationX"; private const string PROP_TEXT_ALIGN = "textAlign"; diff --git a/ReactNative/Views/View/ReactViewManager.cs b/ReactNative/Views/View/ReactViewManager.cs index 20224e27dcd..c6a2d69e912 100644 --- a/ReactNative/Views/View/ReactViewManager.cs +++ b/ReactNative/Views/View/ReactViewManager.cs @@ -1,11 +1,7 @@ -using Facebook.CSSLayout; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using ReactNative.UIManager; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Windows.UI.Xaml; using Windows.UI.Xaml.Automation.Peers; using Windows.UI.Xaml.Automation.Provider; @@ -13,7 +9,7 @@ namespace ReactNative.Views.View { - public class ReactViewManager : ViewGroupManager + public class ReactViewManager : PanelViewGroupManager { public static readonly string REACT_CLASS = ViewProperties.ViewClassName; private static readonly int CMD_SET_PRESSED = 1; @@ -139,13 +135,9 @@ protected override void ReceiveCommand(ReactPanel view, int commandId, JArray ar /// The parent view. /// The child view. /// The index. - public override void AddView(Panel parent, UIElement child, int index) + protected override void AddView(ReactPanel parent, FrameworkElement child, int index) { parent.Children.Insert(index, child); } - - protected override void UpdateExtraData(ReactPanel root, object extraData) - { - } } }