diff --git a/content/blog/2018-03-27-update-on-async-rendering.md b/content/blog/2018-03-27-update-on-async-rendering.md index 1a3bf5f5..080dc2fe 100644 --- a/content/blog/2018-03-27-update-on-async-rendering.md +++ b/content/blog/2018-03-27-update-on-async-rendering.md @@ -33,22 +33,26 @@ If you'd like to start using the new component APIs introduced in React 16.3 (or Before we begin, here's a quick overview of the lifecycle changes planned for version 16.3: * We are **adding the following lifecycle aliases**: `UNSAFE_componentWillMount`, `UNSAFE_componentWillReceiveProps`, and `UNSAFE_componentWillUpdate`. (Both the old lifecycle names and the new aliases will be supported.) -* We are **introducing two new lifecycles**, static `getDerivedStateFromProps` and `getSnapshotBeforeUpdate`: - -`embed:update-on-async-rendering/new-lifecycles-overview.js` +* We are **introducing two new lifecycles**, static `getDerivedStateFromProps` and `getSnapshotBeforeUpdate`. ### New lifecycle: `getDerivedStateFromProps` +`embed:update-on-async-rendering/definition-getderivedstatefromprops.js` + The new static `getDerivedStateFromProps` lifecycle is invoked after a component is instantiated as well as when it receives new props. It can return an object to update `state`, or `null` to indicate that the new `props` do not require any `state` updates. Together with `componentDidUpdate`, this new lifecycle should cover all use cases for the legacy `componentWillReceiveProps`. ### New lifecycle: `getSnapshotBeforeUpdate` +`embed:update-on-async-rendering/definition-getsnapshotbeforeupdate.js` + The new `getSnapshotBeforeUpdate` lifecycle is called right before mutations are made (e.g. before the DOM is updated). The return value for this lifecycle will be passed as the third parameter to `componentDidUpdate`. (This lifecycle isn't often needed, but can be useful in cases like manually preserving scroll position during rerenders.) Together with `componentDidUpdate`, this new lifecycle should cover all use cases for the legacy `componentWillUpdate`. +You can find their type signatures [in this gist](https://gist.github.com/gaearon/88634d27abbc4feeb40a698f760f3264). + We'll look at examples of how both of these lifecycles can be used below. ## Examples @@ -57,7 +61,8 @@ We'll look at examples of how both of these lifecycles can be used below. - [Adding event listeners (or subscriptions)](#adding-event-listeners-or-subscriptions) - [Updating `state` based on props](#updating-state-based-on-props) - [Invoking external callbacks](#invoking-external-callbacks) -- [Updating external data when props change](#updating-external-data-when-props-change) +- [Side effects on props change](#side-effects-on-props-change) +- [Fetching external data when props change](#fetching-external-data-when-props-change) - [Reading DOM properties before an update](#reading-dom-properties-before-an-update) > Note @@ -89,6 +94,8 @@ There is a common misconception that fetching in `componentWillMount` lets you a > Some advanced use-cases (e.g. libraries like Relay) may want to experiment with eagerly prefetching async data. An example of how this can be done is available [here](https://gist.github.com/bvaughn/89700e525ff423a75ffb63b1b1e30a8f). > > In the longer term, the canonical way to fetch data in React components will likely be based on the “suspense” API [introduced at JSConf Iceland](/blog/2018/03/01/sneak-peek-beyond-react-16.html). Both simple data fetching solutions and libraries like Apollo and Relay will be able to use it under the hood. It is significantly less verbose than either of the above solutions, but will not be finalized in time for the 16.3 release. +> +> When supporting server rendering, it's currently necessary to provide the data synchronously – `componentWillMount` was often used for this purpose but the constructor can be used as a replacement. The upcoming suspense APIs will make async data fetching cleanly possible for both client and server rendering. ### Adding event listeners (or subscriptions) @@ -119,9 +126,15 @@ Here is an example of a component that uses the legacy `componentWillReceiveProp Although the above code is not problematic in itself, the `componentWillReceiveProps` lifecycle is often mis-used in ways that _do_ present problems. Because of this, the method will be deprecated. -As of version 16.3, the recommended way to update `state` in response to `props` changes is with the new `static getDerivedStateFromProps` lifecycle. (That lifecycle is called when a component is created and each time it receives new props.): +As of version 16.3, the recommended way to update `state` in response to `props` changes is with the new `static getDerivedStateFromProps` lifecycle. (That lifecycle is called when a component is created and each time it receives new props): `embed:update-on-async-rendering/updating-state-from-props-after.js` +You may notice in the example above that `props.currentRow` is mirrored in state (as `state.lastRow`). This enables `getDerivedStateFromProps` to access the previous props value in the same way as is done in `componentWillReceiveProps`. + +You may wonder why we don't just pass previous props as a parameter to `getDerivedStateFromProps`. We considered this option when designing the API, but ultimately decided against it for two reasons: +* A `prevProps` parameter would be null the first time `getDerivedStateFromProps` was called (after instantiation), requiring an if-not-null check to be added any time `prevProps` was accessed. +* Not passing the previous props to this function is a step toward freeing up memory in future versions of React. (If React does not need to pass previous props to lifecycles, then it does not need to keep the previous `props` object in memory.) + > Note > > If you're writing a shared component, the [`react-lifecycles-compat`](https://github.com/reactjs/react-lifecycles-compat) polyfill enables the new `getDerivedStateFromProps` lifecycle to be used with older versions of React as well. [Learn more about how to use it below.](#open-source-project-maintainers) @@ -136,12 +149,22 @@ Sometimes people use `componentWillUpdate` out of a misplaced fear that by the t Either way, it is unsafe to use `componentWillUpdate` for this purpose in async mode, because the external callback might get called multiple times for a single update. Instead, the `componentDidUpdate` lifecycle should be used since it is guaranteed to be invoked only once per update: `embed:update-on-async-rendering/invoking-external-callbacks-after.js` -### Updating external data when `props` change +### Side effects on props change + +Similar to the [example above](#invoking-external-callbacks), sometimes components have side effects when `props` change. + +`embed:update-on-async-rendering/side-effects-when-props-change-before.js` + +Like `componentWillUpdate`, `componentWillReceiveProps` might get called multiple times for a single update. For this reason it is important to avoid putting side effects in this method. Instead, `componentDidUpdate` should be used since it is guaranteed to be invoked only once per update: + +`embed:update-on-async-rendering/side-effects-when-props-change-after.js` + +### Fetching external data when `props` change Here is an example of a component that fetches external data based on `props` values: `embed:update-on-async-rendering/updating-external-data-when-props-change-before.js` -The recommended upgrade path for this component is to move data-updates into `componentDidUpdate`. You can also use the new `getDerivedStateFromProps` lifecycle to clear stale data before rendering the new props: +The recommended upgrade path for this component is to move data updates into `componentDidUpdate`. You can also use the new `getDerivedStateFromProps` lifecycle to clear stale data before rendering the new props: `embed:update-on-async-rendering/updating-external-data-when-props-change-after.js` > Note @@ -190,4 +213,4 @@ npm install react-lifecycles-compat --save Next, update your components to use the new lifecycles (as described above). Lastly, use the polyfill to make your component backwards compatible with older versions of React: -`embed:update-on-async-rendering/using-react-lifecycles-compat.js` \ No newline at end of file +`embed:update-on-async-rendering/using-react-lifecycles-compat.js` diff --git a/content/docs/accessibility.md b/content/docs/accessibility.md index eafc3019..634ce452 100644 --- a/content/docs/accessibility.md +++ b/content/docs/accessibility.md @@ -140,7 +140,7 @@ Read more about the use of these elements to enhance accessibility here: Our React applications continuously modify the HTML DOM during runtime, sometimes leading to keyboard focus being lost or set to an unexpected element. In order to repair this, we need to programmatically nudge the keyboard focus in the right direction. For example, by resetting keyboard focus to a button that opened a modal window after that modal window is closed. -The Mozilla Developer Network takes a look at this and describes how we can build [keyboard-navigable JavaScript widgets](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets). +MDN Web Docs takes a look at this and describes how we can build [keyboard-navigable JavaScript widgets](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets). To set focus in React, we can use [Refs to DOM elements](refs-and-the-dom.html). diff --git a/content/docs/design-principles.md b/content/docs/design-principles.md index c6db0209..e0f61a34 100644 --- a/content/docs/design-principles.md +++ b/content/docs/design-principles.md @@ -43,7 +43,7 @@ If we want to deprecate a pattern that we don't like, it is our responsibility t ### Stability -We value API stability. At Facebook, we have more than 20 thousand components using React. Many other companies, including [Twitter](https://twitter.com/) and [Airbnb](https://www.airbnb.com/), are also heavy users of React. This is why we are usually reluctant to change public APIs or behavior. +We value API stability. At Facebook, we have more than 50 thousand components using React. Many other companies, including [Twitter](https://twitter.com/) and [Airbnb](https://www.airbnb.com/), are also heavy users of React. This is why we are usually reluctant to change public APIs or behavior. However we think stability in the sense of "nothing changes" is overrated. It quickly turns into stagnation. Instead, we prefer the stability in the sense of "It is heavily used in production, and when something changes, there is a clear (and preferably automated) migration path." diff --git a/content/docs/integrating-with-other-libraries.md b/content/docs/integrating-with-other-libraries.md index 50bc87c7..255de4e6 100644 --- a/content/docs/integrating-with-other-libraries.md +++ b/content/docs/integrating-with-other-libraries.md @@ -194,7 +194,7 @@ React can be embedded into other applications thanks to the flexibility of [`Rea Although React is commonly used at startup to load a single root React component into the DOM, `ReactDOM.render()` can also be called multiple times for independent parts of the UI which can be as small as a button, or as large as an app. -In fact, this is exactly how React is used at Facebook. This lets us write applications in React piece by piece, and combine it with our existing server-generated templates and other client-side code. +In fact, this is exactly how React is used at Facebook. This lets us write applications in React piece by piece, and combine them with our existing server-generated templates and other client-side code. ### Replacing String-Based Rendering with React @@ -279,7 +279,7 @@ const ParagraphView = Backbone.View.extend({ It is important that we also call `ReactDOM.unmountComponentAtNode()` in the `remove` method so that React unregisters event handlers and other resources associated with the component tree when it is detached. -When a component is removed *from within* a React tree, the cleanup is performed automatically, but because we are removing the entire tree by hand, we must call it this method. +When a component is removed *from within* a React tree, the cleanup is performed automatically, but because we are removing the entire tree by hand, we must call this method. ## Integrating with Model Layers diff --git a/examples/update-on-async-rendering/definition-getderivedstatefromprops.js b/examples/update-on-async-rendering/definition-getderivedstatefromprops.js new file mode 100644 index 00000000..d19e4d15 --- /dev/null +++ b/examples/update-on-async-rendering/definition-getderivedstatefromprops.js @@ -0,0 +1,5 @@ +class Example extends React.Component { + static getDerivedStateFromProps(nextProps, prevState) { + // ... + } +} diff --git a/examples/update-on-async-rendering/definition-getsnapshotbeforeupdate.js b/examples/update-on-async-rendering/definition-getsnapshotbeforeupdate.js new file mode 100644 index 00000000..caf87abd --- /dev/null +++ b/examples/update-on-async-rendering/definition-getsnapshotbeforeupdate.js @@ -0,0 +1,5 @@ +class Example extends React.Component { + getSnapshotBeforeUpdate(prevProps, prevState) { + // ... + } +} diff --git a/examples/update-on-async-rendering/new-lifecycles-overview.js b/examples/update-on-async-rendering/new-lifecycles-overview.js deleted file mode 100644 index 4bf67772..00000000 --- a/examples/update-on-async-rendering/new-lifecycles-overview.js +++ /dev/null @@ -1,19 +0,0 @@ -class Example extends React.Component< - Props, - State, - Snapshot -> { - static getDerivedStateFromProps( - nextProps: Props, - prevState: State - ): $Shape | null { - // ... - } - - getSnapshotBeforeUpdate( - prevProps: Props, - prevState: State - ): Snapshot { - // ... - } -} diff --git a/examples/update-on-async-rendering/side-effects-when-props-change-after.js b/examples/update-on-async-rendering/side-effects-when-props-change-after.js new file mode 100644 index 00000000..b4962d9f --- /dev/null +++ b/examples/update-on-async-rendering/side-effects-when-props-change-after.js @@ -0,0 +1,9 @@ +// After +class ExampleComponent extends React.Component { + // highlight-range{1-5} + componentDidUpdate(prevProps, prevState) { + if (this.props.isVisible !== prevProps.isVisible) { + logVisibleChange(this.props.isVisible); + } + } +} diff --git a/examples/update-on-async-rendering/side-effects-when-props-change-before.js b/examples/update-on-async-rendering/side-effects-when-props-change-before.js new file mode 100644 index 00000000..11e85941 --- /dev/null +++ b/examples/update-on-async-rendering/side-effects-when-props-change-before.js @@ -0,0 +1,9 @@ +// Before +class ExampleComponent extends React.Component { + // highlight-range{1-5} + componentWillReceiveProps(nextProps) { + if (this.props.isVisible !== nextProps.isVisible) { + logVisibleChange(nextProps.isVisible); + } + } +} diff --git a/examples/update-on-async-rendering/updating-external-data-when-props-change-after.js b/examples/update-on-async-rendering/updating-external-data-when-props-change-after.js index 014a3c61..2426bc9a 100644 --- a/examples/update-on-async-rendering/updating-external-data-when-props-change-after.js +++ b/examples/update-on-async-rendering/updating-external-data-when-props-change-after.js @@ -20,13 +20,13 @@ class ExampleComponent extends React.Component { } componentDidMount() { - this._loadAsyncData(); + this._loadAsyncData(this.props.id); } // highlight-range{1-5} componentDidUpdate(prevProps, prevState) { if (prevState.externalData === null) { - this._loadAsyncData(); + this._loadAsyncData(this.props.id); } } @@ -44,8 +44,8 @@ class ExampleComponent extends React.Component { } } - _loadAsyncData() { - this._asyncRequest = asyncLoadData(this.props.id).then( + _loadAsyncData(id) { + this._asyncRequest = asyncLoadData(id).then( externalData => { this._asyncRequest = null; this.setState({externalData}); diff --git a/examples/update-on-async-rendering/updating-external-data-when-props-change-before.js b/examples/update-on-async-rendering/updating-external-data-when-props-change-before.js index 0b0af809..aa22358c 100644 --- a/examples/update-on-async-rendering/updating-external-data-when-props-change-before.js +++ b/examples/update-on-async-rendering/updating-external-data-when-props-change-before.js @@ -5,14 +5,14 @@ class ExampleComponent extends React.Component { }; componentDidMount() { - this._loadAsyncData(); + this._loadAsyncData(this.props.id); } // highlight-range{1-6} componentWillReceiveProps(nextProps) { if (nextProps.id !== this.props.id) { this.setState({externalData: null}); - this._loadAsyncData(); + this._loadAsyncData(nextProps.id); } } @@ -30,8 +30,8 @@ class ExampleComponent extends React.Component { } } - _loadAsyncData() { - this._asyncRequest = asyncLoadData(this.props.id).then( + _loadAsyncData(id) { + this._asyncRequest = asyncLoadData(id).then( externalData => { this._asyncRequest = null; this.setState({externalData}); diff --git a/src/components/MarkdownPage/MarkdownPage.js b/src/components/MarkdownPage/MarkdownPage.js index b727f825..9f7d3a4b 100644 --- a/src/components/MarkdownPage/MarkdownPage.js +++ b/src/components/MarkdownPage/MarkdownPage.js @@ -31,6 +31,18 @@ type Props = { titlePostfix: string, }; +const getPageById = (sectionList: Array, templateFile: ?string) => { + if (!templateFile) { + return null; + } + + const sectionItems = sectionList.map(section => section.items); + const flattenedSectionItems = [].concat.apply([], sectionItems); + const linkId = templateFile.replace('.html', ''); + + return flattenedSectionItems.find(item => item.id === linkId); +}; + const MarkdownPage = ({ authors = [], createLink, @@ -45,6 +57,9 @@ const MarkdownPage = ({ const hasAuthors = authors.length > 0; const titlePrefix = markdownRemark.frontmatter.title || ''; + const prev = getPageById(sectionList, markdownRemark.frontmatter.prev); + const next = getPageById(sectionList, markdownRemark.frontmatter.next); + return ( - {/* TODO Read prev/next from index map, not this way */} - {(markdownRemark.frontmatter.next || markdownRemark.frontmatter.prev) && ( - + {(next || prev) && ( + )} ); diff --git a/src/templates/components/NavigationFooter/NavigationFooter.js b/src/templates/components/NavigationFooter/NavigationFooter.js index d29c16c6..f5ce55ab 100644 --- a/src/templates/components/NavigationFooter/NavigationFooter.js +++ b/src/templates/components/NavigationFooter/NavigationFooter.js @@ -45,8 +45,8 @@ const NavigationFooter = ({next, prev, location}) => { css={{ paddingTop: 10, }}> - - {linkToTitle(prev)} + + {prev.title} @@ -66,8 +66,8 @@ const NavigationFooter = ({next, prev, location}) => { css={{ paddingTop: 10, }}> - - {linkToTitle(next)} + + {next.title} @@ -80,14 +80,18 @@ const NavigationFooter = ({next, prev, location}) => { }; NavigationFooter.propTypes = { - next: PropTypes.string, - prev: PropTypes.string, + next: PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + }), + prev: PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + }), }; export default NavigationFooter; -const linkToTitle = link => link.replace(/-/g, ' ').replace('.html', ''); - const PrimaryLink = ({children, to, location}) => { // quick fix // TODO: replace this with better method of getting correct full url @@ -97,7 +101,6 @@ const PrimaryLink = ({children, to, location}) => {