Skip to content

16.4 blog post #904

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions content/blog/2018-05-23-react-v-16-4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
title: "React v16.4.0: Pointer Events"
author: [acdlite]
---

The latest minor release adds support for an oft-requested feature: pointer events!

It also includes a bugfix for `getDerivedStateFromProps`. Check out the full [changelog](#changelog) below.

## Pointer Events

The following event types are now available in React DOM:

- `onPointerDown`
- `onPointerMove`
- `onPointerUp`
- `onPointerCancel`
- `onGotPointerCapture`
- `onLostPointerCapture`
- `onPointerEnter`
- `onPointerLeave`
- `onPointerOver`
- `onPointerOut`

Please note that these events will only work in browsers that support the Pointer Events specification. (At the time of this writing, this includes the latest versions of Chrome, Firefox, Edge, and Internet Explorer.) If your application depends on pointer events, we recommend using a third-party pointer events polyfill. We have opted not to include such a polyfill in React DOM, to avoid an increase in bundle size.

[Check out this example on CodeSandbox.](codesandbox://16-4-release-blog-post/pointer-events-example)

Huge thanks to [Philip Spiess](https://github.com/philipp-spiess) for contributing this change!

## Bugfix for `getDerivedStateFromProps`

`getDerivedStateFromProps` is now called every time a component is rendered, regardless of the cause of the update. Previously, it was only called if the component was re-rendered by its parent, and would not fire as the result of a local `setState`. This was an oversight in the initial implementation that has now been corrected. The previous behavior was more similar to `componentWillReceiveProps`, but the improved behavior ensures compatibility with React's upcoming asynchronous rendering mode.

**This bug fix will not affect most apps**, but it may cause issues with a small fraction of components. The rare cases where it does matter fall into one of two categories:

### 1. Avoid Side Effects in `getDerivedStateFromProps`

Like the render method, `getDerivedStateFromProps` should be a pure function of props and state. Side effects in `getDerivedStateFromProps` were never supported, but since it now fires more often than it used to, the recent change may expose previously undiscovered bugs.

Side effectful code should be moved to other methods: for example, Flux dispatches typically belong inside the originating event handler, and manual DOM mutations belong inside componentDidMount or componentDidUpdate. You can read more about this in our recent post about [preparing for asynchronous rendering](/blog/2018/03/27/update-on-async-rendering.html).

### 2. Compare Incoming Props to Previous Props When Computing Controlled Values

The following code assumes `getDerivedStateFromProps` only fires on prop changes:

```js
static getDerivedStateFromProps(props, state) {
if (props.value !== state.controlledValue) {
return {
// Since this method fires on both props and state changes, local updates
Copy link
Contributor

@bvaughn bvaughn May 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super tiny nit. Please feel free to ignore.

Suggestion: Change "Since this method fires" to "Because this method fires"

(so there aren't two "since"s in the sentence)

((no idea if this is a grammatical no-no))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

// to the controlled value will be ignored, because the props version
// always overrides it. Oops!
controlledValue: props.value,
};
}
return null;
}
```

One possible way to fix this is to compare the incoming value to the previous value by storing the previous props in state:

```js
static getDerivedStateFromProps(props, state) {
const prevProps = state.prevProps;
// Compare the incoming prop to previous prop
const controlledValue =
prevProps.value !== props.value
? props.value
: state.controlledValue;
return {
// Store the previous props in state
prevProps: props,
controlledValue,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid this example. Even aside from gDSFP, this approach of half-controlled/half-uncontrolled leads to an awkward/ambiguous API.

For example, the only way to reset a half-controlled component's state to a value safely is to render twice, temporarily setting the prop to something else (e.g. null) and then setting it back.

I think instead, we should encourage fully controlled (no state) or fully uncontrolled (props.defaultValue sets initial state and never gets synced).

Maybe put another way, I think we may want to avoid a component ever updating a state attribute via setState that props also updates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's okay we show this, since we want to cover a before and after case. I don't know. In general, I'm just concerned about any perceived encouragement of this API/design. 😄

}
```
Copy link
Member

@gaearon gaearon May 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just mention here that most components don't need this pattern. We're only documenting for the cases in which you're already using it. But if you just need some value from props, you should just read it from props, and not attempt to put it into state.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example:

Note that typically the vast majority of the components don't need getDerivedStateFromProps at all. It shouldn't be used often, as making state dependent on props often leads to logic that is hard to understand. If possible, prefer to have one source of truth for any value (either in props or in state), and lift state up if it's necessary to share it between multiple components.

Copy link
Member Author

@acdlite acdlite May 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but I have mixed feelings about telling people not to use a feature, especially one we just added, unless we give them a clear alternatives. We need to follow up with a more comprehensive post soon (like this week or next).


However, try to avoid mixing props and state in this way -- it's rare that state needs to duplicate a value that exists in props and doing so can lead to subtle bugs. Prefer to have one source of truth for any value, and [lift state up](/docs/lifting-state-up.html) if necessary to share it between multiple components. Most uses of `getDerivedStateFromProps` (and its predecessor `componentWillReceiveProps`) can be fixed by moving the state management to a parent component.

Please remember that **most components do not need `getDerivedStateFromProps`**. It's not meant to serve as a one-to-one replacement for `componentWillReceiveProps`. We'll publish a follow-up post in the coming weeks with more recommendations on how to use (and not use) `getDerivedStateFromProps`.

## Installation

React v16.4.0 is available on the npm registry.

To install React 16 with Yarn, run:

```bash
yarn add react@^16.4.0 react-dom@^16.4.0
```

To install React 16 with npm, run:

```bash
npm install --save react@^16.4.0 react-dom@^16.4.0
```

We also provide UMD builds of React via a CDN:

```html
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
```

Refer to the documentation for [detailed installation instructions](/docs/installation.html).

## Changelog

### React

* Add a new [experimental](https://github.com/reactjs/rfcs/pull/51) `React.unstable_Profiler` component for measuring performance. ([@bvaughn](https://github.com/bvaughn) in [#12745](https://github.com/facebook/react/pull/12745))

### React DOM

* Add support for the Pointer Events specification. ([@philipp-spiess](https://github.com/philipp-spiess) in [#12507](https://github.com/facebook/react/pull/12507))
* Properly call `getDerivedStateFromProps()` regardless of the reason for re-rendering. ([@acdlite](https://github.com/acdlite) in [#12600](https://github.com/facebook/react/pull/12600) and [#12802](https://github.com/facebook/react/pull/12802))
* Fix a bug that prevented context propagation in some cases. ([@gaearon](https://github.com/gaearon) in [#12708](https://github.com/facebook/react/pull/12708))
* Fix re-rendering of components using `forwardRef()` on a deeper `setState()`. ([@gaearon](https://github.com/gaearon) in [#12690](https://github.com/facebook/react/pull/12690))
* Fix some attributes incorrectly getting removed from custom element nodes. ([@airamrguez](https://github.com/airamrguez) in [#12702](https://github.com/facebook/react/pull/12702))
* Fix context providers to not bail out on children if there's a legacy context provider above. ([@gaearon](https://github.com/gaearon) in [#12586](https://github.com/facebook/react/pull/12586))
* Add the ability to specify `propTypes` on a context provider component. ([@nicolevy](https://github.com/nicolevy) in [#12658](https://github.com/facebook/react/pull/12658))
* Fix a false positive warning when using `react-lifecycles-compat` in `<StrictMode>`. ([@bvaughn](https://github.com/bvaughn) in [#12644](https://github.com/facebook/react/pull/12644))
* Warn when the `forwardRef()` render function has `propTypes` or `defaultProps`. ([@bvaughn](https://github.com/bvaughn) in [#12644](https://github.com/facebook/react/pull/12644))
* Improve how `forwardRef()` and context consumers are displayed in the component stack. ([@sophiebits](https://github.com/sophiebits) in [#12777](https://github.com/facebook/react/pull/12777))
* Change internal event names. This can break third-party packages that rely on React internals in unsupported ways. ([@philipp-spiess](https://github.com/philipp-spiess) in [#12629](https://github.com/facebook/react/pull/12629))

### React Test Renderer

* Fix the `getDerivedStateFromProps()` support to match the new React DOM behavior. ([@koba04](https://github.com/koba04) in [#12676](https://github.com/facebook/react/pull/12676))
* Fix a `testInstance.parent` crash when the parent is a fragment or another special node. ([@gaearon](https://github.com/gaearon) in [#12813](https://github.com/facebook/react/pull/12813))
* `forwardRef()` components are now discoverable by the test renderer traversal methods. ([@gaearon](https://github.com/gaearon) in [#12725](https://github.com/facebook/react/pull/12725))
* Shallow renderer now ignores `setState()` updaters that return `null` or `undefined`. ([@koba04](https://github.com/koba04) in [#12756](https://github.com/facebook/react/pull/12756))

### React ART

* Fix reading context provided from the tree managed by React DOM. ([@acdlite](https://github.com/acdlite) in [#12779](https://github.com/facebook/react/pull/12779))

### React Call Return (Experimental)

* This experiment was deleted because it was affecting the bundle size and the API wasn't good enough. It's likely to come back in the future in some other form. ([@gaearon](https://github.com/gaearon) in [#12820](https://github.com/facebook/react/pull/12820))

### React Reconciler (Experimental)

* The [new host config shape](https://github.com/facebook/react/blob/c601f7a64640290af85c9f0e33c78480656b46bc/packages/react-noop-renderer/src/createReactNoop.js#L82-L285) is flat and doesn't use nested objects. ([@gaearon](https://github.com/gaearon) in [#12792](https://github.com/facebook/react/pull/12792))
35 changes: 35 additions & 0 deletions content/docs/reference-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ The event handlers below are triggered by an event in the bubbling phase. To reg
- [Focus Events](#focus-events)
- [Form Events](#form-events)
- [Mouse Events](#mouse-events)
- [Pointer Events](#pointer-events)
- [Selection Events](#selection-events)
- [Touch Events](#touch-events)
- [UI Events](#ui-events)
Expand Down Expand Up @@ -215,6 +216,40 @@ boolean shiftKey

* * *

### Pointer Events

Event names:

```
onPointerDown onPointerMove onPointerUp onPointerCancel onGotPointerCapture
onLostPointerCapture onPointerEnter onPointerLeave onPointerOver onPointerOut
```

The `onPointerEnter` and `onPointerLeave` events propagate from the element being left to the one being entered instead of ordinary bubbling and do not have a capture phase.

Properties:

As defined in the [W3 spec](https://www.w3.org/TR/pointerevents/), pointer events extend [Mouse Events](#mouse-events) with the following properties:

```javascript
number pointerId
number width
number height
number pressure
number tiltX
number tiltY
string pointerType
boolean isPrimary
```

A note on cross-browser support:

Pointer events are not yet supported in every browser (at the time of writing this article, supported browsers include: Chrome, Firefox, Edge, and Internet Explorer). React deliberately does not polyfill support for other browsers because a standard-conform polyfill would significantly increase the bundle size of `react-dom`.

If your application requires pointer events, we recommend adding a third party pointer event polyfill.

* * *

### Selection Events

Event names:
Expand Down
4 changes: 2 additions & 2 deletions content/docs/reference-react-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,9 @@ If you "fork" props by using them for state, you might also want to implement [`
static getDerivedStateFromProps(nextProps, prevState)
```

`getDerivedStateFromProps` is invoked after a component is instantiated as well as when it receives new props. It should return an object to update state, or null to indicate that the new props do not require any state updates.
`getDerivedStateFromProps` is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.

Note that if a parent component causes your component to re-render, this method will be called even if props have not changed. You may want to compare new and previous values if you only want to handle changes.
Note that this method is fired on *every* render, regardless of the cause. This is in contrast to `UNSAFE_componentWillReceiveProps`, which only fires when the parent causes a re-render and not as a result of a local `setState`.

* * *

Expand Down
95 changes: 95 additions & 0 deletions examples/16-4-release-blog-post/pointer-events-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';
import ReactDOM from 'react-dom';

const CIRCLE_SIZE = 85;

class DragBox extends React.Component {
state = {
hasCapture: false,
circleLeft: 80,
circleTop: 80,
};
isDragging = false;
previousLeft = 0;
previousTop = 0;

onDown = event => {
this.isDragging = true;
event.target.setPointerCapture(event.pointerId);

// We store the initial coordinates to be able to calculate the changes
// later on.
this.extractPositionDelta(event);
};

onMove = event => {
if (!this.isDragging) {
return;
}
const {left, top} = this.extractPositionDelta(event);

this.setState(({circleLeft, circleTop}) => ({
circleLeft: circleLeft + left,
circleTop: circleTop + top,
}));
};

onUp = event => (this.isDragging = false);
onGotCapture = event => this.setState({hasCapture: true});
onLostCapture = event =>
this.setState({hasCapture: false});

extractPositionDelta = event => {
const left = event.pageX;
const top = event.pageY;
const delta = {
left: left - this.previousLeft,
top: top - this.previousTop,
};
this.previousLeft = left;
this.previousTop = top;
return delta;
};

render() {
const {hasCapture, circleLeft, circleTop} = this.state;

const boxStyle = {
border: '1px solid #d9d9d9',
margin: '10px 0 20px',
minHeight: 400,
width: '100%',
position: 'relative',
};

const circleStyle = {
width: CIRCLE_SIZE,
height: CIRCLE_SIZE,
borderRadius: CIRCLE_SIZE / 2,
position: 'absolute',
left: circleLeft,
top: circleTop,
backgroundColor: hasCapture ? 'blue' : 'green',
touchAction: 'none',
};

return (
<div style={boxStyle}>
<div
style={circleStyle}
onPointerDown={this.onDown}
onPointerMove={this.onMove}
onPointerUp={this.onUp}
onPointerCancel={this.onUp}
onGotPointerCapture={this.onGotCapture}
onLostPointerCapture={this.onLostCapture}
/>
</div>
);
}
}

ReactDOM.render(
<DragBox />,
document.getElementById('root')
);
1 change: 1 addition & 0 deletions gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ module.exports = {
`//unpkg.com/react/umd/react.development.js`,
`//unpkg.com/react-dom/umd/react-dom.development.js`,
],
dependencies: [`react`, `react-dom`],
redirectTemplate: `${__dirname}/src/templates/codepen-example.js`,
target: '_blank',
},
Expand Down