From 9f8c4cef7b60cdd1b8bc70d5c9a08623533faf50 Mon Sep 17 00:00:00 2001 From: Daniel Friesen Date: Mon, 12 Mar 2018 02:19:45 -0700 Subject: [PATCH 01/22] Introduce the RFC for the reparenting API --- text/0000-reparenting.md | 351 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 text/0000-reparenting.md diff --git a/text/0000-reparenting.md b/text/0000-reparenting.md new file mode 100644 index 00000000..8e452b5a --- /dev/null +++ b/text/0000-reparenting.md @@ -0,0 +1,351 @@ +- Start Date: 2018-03-11 +- RFC PR: (leave this empty) +- React Issue: (leave this empty) + +# Summary + +When writing a component that contains a set of large subtrees that stay relatively the same, but are simply moved around such that React's virtual DOM diffing can't detect the movement, React will end up recreating huge trees it should simply be moving. + +The goal of this RFC is to introduce a "Reparent" API that allows portions of the React tree to be marked in a way that allows React to know when to move them from one part of the tree to another instead of deleting and recreating the DOM and component instances. + +# Basic example + +## Layout +Using reparents to render a page layout that can change structure between desktop and mobile without causing the page contents and sidebar to be recreated from scratch. +```js +class Layout extends PureComponent { + header = React.createReparent(this); + content = React.createReparent(this); + sidebar = React.createReparent(this); + + render() { + const {isMobile, children} = this.props; + + const header = this.header( +
+ + +
+ ); + + const sidebar = this.sidebar( +
+ +
+ ); + + const content = this.content( + + children + + ); + + if ( isMobile ){ + return ( +
+ {header} + {content} + {sidebar} +
+ ); + } else { + return ( +
+ {header} +
+ {content} + {sidebar} +
+
+ ); + } + } +} +``` + +## Dynamic template +Using reparents in a dynamic template/widget structure to avoid recreating widgets from scratch when they are move from one section of the template to another. + +```js +const TemplateWidgetReparentContext = React.createContext({}); + +class ReparentableTemplateWidget extends PureComponent { + static getDerivedStateFromProps(nextProps, prevState) { + if ( !prevState.widget || prevState.widgetType !== nextProps.widget ) { + const WidgetClass = getWidgetClassOfType(nextProps.widget); + return { + widgetType: nextProps.widget, + widget: new WidgetClass(), + }; + } + } + + render() { + const {widget} = this.state; + + return ( +
+ {widget.render()} +
+ ); + } +} + +/** + * This wrapper uses reparents from Template and the widget's own id + * to ensure that when a widget is moved from one section to another + * it is moved by react instead of recreated. + * + * This could be turned into an HOC + */ +class TemplateWidget extends Component { + render() { + const {id} = this.props; + + return ( + + {templateWidgetReparents => templateWidgetReparents[id]( + + )} + + ); + } +} + +class Section extends PureComponent { + render() { + const {title, widgets} = this.props; + + return ( +
+

{title}

+ {widgets.map(widget => ( + + ))} +
+ ); + } +} + +class Template extends PureComponent { + static getDerivedStateFromProps(nextProps, prevState) { + if ( nextProps.sections === prevState.sections ) return; + + let templateWidgetReparents = prevState.templateWidgetReparents; + + const widgetIds = new Set(); + nextProps.sections.forEach(section => section.widgets.forEach(widget => widgetIds.add(widget.id))); + + return { + widgetIds: widgetIds.toArray(), + }; + } + + templateWidgetReparents = {}; + + render() { + const {sections} = this.props; + const {widgetIds} = this.state; + + for ( const id of widgetIds ) { + if ( !(id in templateWidgetReparents) ) { + this.templateWidgetReparents = Object.assign(this.templateWidgetReparents, { + [id]: React.createReparent(this), + }); + } + } + + for ( const id in this.templateWidgetReparents ) { + if ( !widgetIds.has(id) ) { + this.templateWidgetReparents[id].unmount(); + this.templateWidgetReparents = omit(this.templateWidgetReparents, id); + } + } + + return ( + + {sections.map(section => { +
+ })} + + ); + } +} + +let templateData = { + sections: [ + { + id: 's-1', + title: 'Untitled 1', + widgets: [ + { + id: 'w-1', + widget: 'paragraph', + props: { + text: 'Lorem ipsum...' + } + } + ] + }, + { + id: 's-2', + title: 'Lorem ipsum', + widgets: [] + } + ] +}; + +ReactDOM.render(