/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule Incremental * @flow */ 'use strict'; const InteractionManager = require('InteractionManager'); const React = require('React'); const PropTypes = require('prop-types'); const infoLog = require('infoLog'); const DEBUG = false; /** * WARNING: EXPERIMENTAL. Breaking changes will probably happen a lot and will * not be reliably announced. The whole thing might be deleted, who knows? Use * at your own risk. * * React Native helps make apps smooth by doing all the heavy lifting off the * main thread, in JavaScript. That works great a lot of the time, except that * heavy operations like rendering may block the JS thread from responding * quickly to events like taps, making the app feel sluggish. * * `` solves this by slicing up rendering into chunks that are * spread across multiple event loops. Expensive components can be sliced up * recursively by wrapping pieces of them and their decendents in * `` components. `` can be used to make sure * everything in the group is rendered recursively before calling `onDone` and * moving on to another sibling group (e.g. render one row at a time, even if * rendering the top level row component produces more `` chunks). * `` is a type of `` that keeps it's * children invisible and out of the layout tree until all rendering completes * recursively. This means the group will be presented to the user as one unit, * rather than pieces popping in sequentially. * * `` only affects initial render - `setState` and other render * updates are unaffected. * * The chunks are rendered sequentially using the `InteractionManager` queue, * which means that rendering will pause if it's interrupted by an interaction, * such as an animation or gesture. * * Note there is some overhead, so you don't want to slice things up too much. * A target of 100-200ms of total work per event loop on old/slow devices might * be a reasonable place to start. * * Below is an example that will incrementally render all the parts of `Row` one * first, then present them together, then repeat the process for `Row` two, and * so on: * * render: function() { * return ( * * {Array(10).fill().map((rowIdx) => ( * * * {Array(20).fill().map((widgetIdx) => ( * * * * ))} * * * ))} * * ); * }; * * If SlowWidget takes 30ms to render, then without `Incremental`, this would * block the JS thread for at least `10 * 20 * 30ms = 6000ms`, but with * `Incremental` it will probably not block for more than 50-100ms at a time, * allowing user interactions to take place which might even unmount this * component, saving us from ever doing the remaining rendering work. */ export type Props = { /** * Called when all the decendents have finished rendering and mounting * recursively. */ onDone?: () => void, /** * Tags instances and associated tasks for easier debugging. */ name: string, children?: any, }; type DefaultProps = { name: string, }; type State = { doIncrementalRender: boolean, }; class Incremental extends React.Component { props: Props; state: State; context: Context; _incrementId: number; _mounted: boolean; _rendered: boolean; static defaultProps = { name: '', }; static contextTypes = { incrementalGroup: PropTypes.object, incrementalGroupEnabled: PropTypes.bool, }; constructor(props: Props, context: Context) { super(props, context); this._mounted = false; this.state = { doIncrementalRender: false, }; } getName(): string { const ctx = this.context.incrementalGroup || {}; return ctx.groupId + ':' + this._incrementId + '-' + this.props.name; } componentWillMount() { const ctx = this.context.incrementalGroup; if (!ctx) { return; } this._incrementId = ++(ctx.incrementalCount); InteractionManager.runAfterInteractions({ name: 'Incremental:' + this.getName(), gen: () => new Promise(resolve => { if (!this._mounted || this._rendered) { resolve(); return; } DEBUG && infoLog('set doIncrementalRender for ' + this.getName()); this.setState({doIncrementalRender: true}, resolve); }), }).then(() => { DEBUG && infoLog('call onDone for ' + this.getName()); this._mounted && this.props.onDone && this.props.onDone(); }).catch((ex) => { ex.message = `Incremental render failed for ${this.getName()}: ${ex.message}`; throw ex; }).done(); } render(): ?React.Element { if (this._rendered || // Make sure that once we render once, we stay rendered even if incrementalGroupEnabled gets flipped. !this.context.incrementalGroupEnabled || this.state.doIncrementalRender) { DEBUG && infoLog('render ' + this.getName()); this._rendered = true; return this.props.children; } return null; } componentDidMount() { this._mounted = true; if (!this.context.incrementalGroup) { this.props.onDone && this.props.onDone(); } } componentWillUnmount() { this._mounted = false; } } export type Context = { incrementalGroupEnabled: boolean, incrementalGroup: ?{ groupId: string, incrementalCount: number, }, }; module.exports = Incremental;