Skip to content

Commit fc26ec1

Browse files
authored
feat(React): add nodeRef prop (#478)
If running in React Strict mode, ReactDOM.findDOMNode() is deprecated. Unfortunately, in order for <Draggable> to work properly, we need raw access to the underlying DOM node. If you want to avoid the warning, pass a `nodeRef` as in this example: function MyComponent() { const nodeRef = React.useRef(null); return ( <Draggable nodeRef={nodeRef}> <div ref={nodeRef}>Example Target</div> </Draggable> ); } This can be used for arbitrarily nested components, so long as the ref ends up pointing to the actual child DOM node and not a custom component. Thanks to react-transition-group for the inspiration. `nodeRef` is also available on <DraggableCore>.
1 parent f101283 commit fc26ec1

File tree

5 files changed

+83
-9
lines changed

5 files changed

+83
-9
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,28 @@ onDrag: DraggableEventHandler,
224224
// Called when dragging stops.
225225
onStop: DraggableEventHandler,
226226

227+
// If running in React Strict mode, ReactDOM.findDOMNode() is deprecated.
228+
// Unfortunately, in order for <Draggable> to work properly, we need raw access
229+
// to the underlying DOM node. If you want to avoid the warning, pass a `nodeRef`
230+
// as in this example:
231+
//
232+
// function MyComponent() {
233+
// const nodeRef = React.useRef(null);
234+
// return (
235+
// <Draggable nodeRef={nodeRef}>
236+
// <div ref={nodeRef}>Example Target</div>
237+
// </Draggable>
238+
// );
239+
// }
240+
//
241+
// This can be used for arbitrarily nested components, so long as the ref ends up
242+
// pointing to the actual child DOM node and not a custom component.
243+
//
244+
// Thanks to react-transition-group for the inspiration.
245+
//
246+
// `nodeRef` is also available on <DraggableCore>.
247+
nodeRef: React.Ref<typeof React.Component>,
248+
227249
// Much like React form elements, if this property is present, the item
228250
// becomes 'controlled' and is not responsive to user input. Use `position`
229251
// if you need to have direct control of the element.

lib/Draggable.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @flow
2-
import React from 'react';
2+
import * as React from 'react';
33
import PropTypes from 'prop-types';
44
import ReactDOM from 'react-dom';
55
import classNames from 'classnames';
@@ -29,6 +29,7 @@ export type DraggableProps = {
2929
defaultClassNameDragging: string,
3030
defaultClassNameDragged: string,
3131
defaultPosition: ControlPosition,
32+
nodeRef?: ?React.ElementRef<any>,
3233
positionOffset: PositionOffsetControlPosition,
3334
position: ControlPosition,
3435
scale: number
@@ -226,7 +227,7 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
226227

227228
componentDidMount() {
228229
// Check to see if the element passed is an instanceof SVGElement
229-
if(typeof window.SVGElement !== 'undefined' && ReactDOM.findDOMNode(this) instanceof window.SVGElement) {
230+
if(typeof window.SVGElement !== 'undefined' && this.findDOMNode() instanceof window.SVGElement) {
230231
this.setState({isElementSVG: true});
231232
}
232233
}
@@ -235,6 +236,12 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
235236
this.setState({dragging: false}); // prevents invariant if unmounted while dragging
236237
}
237238

239+
// React Strict Mode compatibility: if `nodeRef` is passed, we will use it instead of trying to find
240+
// the underlying DOM node ourselves. See the README for more information.
241+
findDOMNode(): ?HTMLElement {
242+
return this.props.nodeRef ? this.props.nodeRef.current : ReactDOM.findDOMNode(this);
243+
}
244+
238245
onDragStart: DraggableEventHandler = (e, coreData) => {
239246
log('Draggable: onDragStart: %j', coreData);
240247

lib/DraggableCore.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @flow
2-
import React from 'react';
2+
import * as React from 'react';
33
import PropTypes from 'prop-types';
44
import ReactDOM from 'react-dom';
55
import {matchesSelectorAndParentsTo, addEvent, removeEvent, addUserSelectStyles, getTouchIdentifier,
@@ -56,6 +56,7 @@ export type DraggableCoreProps = {
5656
offsetParent: HTMLElement,
5757
grid: [number, number],
5858
handle: string,
59+
nodeRef?: ?React.ElementRef<any>,
5960
onStart: DraggableEventHandler,
6061
onDrag: DraggableEventHandler,
6162
onStop: DraggableEventHandler,
@@ -155,6 +156,25 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
155156
*/
156157
cancel: PropTypes.string,
157158

159+
/* If running in React Strict mode, ReactDOM.findDOMNode() is deprecated.
160+
* Unfortunately, in order for <Draggable> to work properly, we need raw access
161+
* to the underlying DOM node. If you want to avoid the warning, pass a `nodeRef`
162+
* as in this example:
163+
*
164+
* function MyComponent() {
165+
* const nodeRef = React.useRef(null);
166+
* return (
167+
* <Draggable nodeRef={nodeRef}>
168+
* <div ref={nodeRef}>Example Target</div>
169+
* </Draggable>
170+
* );
171+
* }
172+
*
173+
* This can be used for arbitrarily nested components, so long as the ref ends up
174+
* pointing to the actual child DOM node and not a custom component.
175+
*/
176+
nodeRef: PropTypes.object,
177+
158178
/**
159179
* Called when dragging starts.
160180
* If this function returns the boolean false, dragging will be canceled.
@@ -221,7 +241,7 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
221241
this.mounted = true;
222242
// Touch handlers must be added with {passive: false} to be cancelable.
223243
// https://developers.google.com/web/updates/2017/01/scrolling-intervention
224-
const thisNode = ReactDOM.findDOMNode(this);
244+
const thisNode = this.findDOMNode();
225245
if (thisNode) {
226246
addEvent(thisNode, eventsFor.touch.start, this.onTouchStart, {passive: false});
227247
}
@@ -231,7 +251,7 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
231251
this.mounted = false;
232252
// Remove any leftover event handlers. Remove both touch and mouse handlers in case
233253
// some browser quirk caused a touch event to fire during a mouse move, or vice versa.
234-
const thisNode = ReactDOM.findDOMNode(this);
254+
const thisNode = this.findDOMNode();
235255
if (thisNode) {
236256
const {ownerDocument} = thisNode;
237257
removeEvent(ownerDocument, eventsFor.mouse.move, this.handleDrag);
@@ -243,6 +263,12 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
243263
}
244264
}
245265

266+
// React Strict Mode compatibility: if `nodeRef` is passed, we will use it instead of trying to find
267+
// the underlying DOM node ourselves. See the README for more information.
268+
findDOMNode(): ?HTMLElement {
269+
return this.props.nodeRef ? this.props.nodeRef.current : ReactDOM.findDOMNode(this);
270+
}
271+
246272
handleDragStart: EventHandler<MouseTouchEvent> = (e) => {
247273
// Make it possible to attach event handlers on top of this one.
248274
this.props.onMouseDown(e);
@@ -251,7 +277,7 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
251277
if (!this.props.allowAnyClick && typeof e.button === 'number' && e.button !== 0) return false;
252278

253279
// Get nodes. Be sure to grab relative document (could be iframed)
254-
const thisNode = ReactDOM.findDOMNode(this);
280+
const thisNode = this.findDOMNode();
255281
if (!thisNode || !thisNode.ownerDocument || !thisNode.ownerDocument.body) {
256282
throw new Error('<DraggableCore> not mounted on DragStart!');
257283
}
@@ -365,7 +391,7 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
365391
const shouldContinue = this.props.onStop(e, coreEvent);
366392
if (shouldContinue === false || this.mounted === false) return false;
367393

368-
const thisNode = ReactDOM.findDOMNode(this);
394+
const thisNode = this.findDOMNode();
369395
if (thisNode) {
370396
// Remove user-select hack
371397
if (this.props.enableUserSelectHack) removeUserSelectStyles(thisNode.ownerDocument);

lib/utils/positionFns.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// @flow
22
import {isNum, int} from './shims';
3-
import ReactDOM from 'react-dom';
43
import {getTouch, innerWidth, innerHeight, offsetXYFromParent, outerWidth, outerHeight} from './domFns';
54

65
import type Draggable from '../Draggable';
@@ -126,7 +125,7 @@ function cloneBounds(bounds: Bounds): Bounds {
126125
}
127126

128127
function findDOMNode(draggable: Draggable | DraggableCore): HTMLElement {
129-
const node = ReactDOM.findDOMNode(draggable);
128+
const node = draggable.findDOMNode();
130129
if (!node) {
131130
throw new Error('<DraggableCore>: Unmounted during event!');
132131
}

specs/draggable.spec.jsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,7 @@ describe('react-draggable', function () {
722722
assert(data.y === 100);
723723
assert(data.deltaX === 100);
724724
assert(data.deltaY === 100);
725+
assert(data.node === ReactDOM.findDOMNode(drag));
725726
}
726727
drag = TestUtils.renderIntoDocument(
727728
<Draggable onDrag={onDrag}>
@@ -733,6 +734,25 @@ describe('react-draggable', function () {
733734
simulateMovementFromTo(drag, 0, 0, 100, 100);
734735
});
735736

737+
it('should call back with correct dom node with nodeRef', function () {
738+
function onDrag(event, data) {
739+
// Being tricky here and installing the ref on the inner child, to ensure it's working
740+
// and not just falling back on ReactDOM.findDOMNode()
741+
assert(data.node === ReactDOM.findDOMNode(drag).firstChild);
742+
}
743+
const nodeRef = React.createRef();
744+
drag = TestUtils.renderIntoDocument(
745+
<Draggable onDrag={onDrag} nodeRef={nodeRef}>
746+
<span>
747+
<div ref={nodeRef} />
748+
</span>
749+
</Draggable>
750+
);
751+
752+
// (element, fromX, fromY, toX, toY)
753+
simulateMovementFromTo(drag, 0, 0, 100, 100);
754+
});
755+
736756
it('should call back on drag, with values within the defined bounds', function(){
737757
function onDrag(event, data) {
738758
assert(data.x === 90);

0 commit comments

Comments
 (0)