react-dnd doesn't seem to work with Preact.
I've narrowed down the problem to that where React gives connectDragSource(...) a DOM node, Preact gives it a component instead. When the HTML5-Backend of react-dnd then tries to add attributes to the DOM nodes it fails because the component doesn't have a setAttribute method.
I'm not quite sure what's causing this, but i'm guessing it's some API difference. Most things seem to work but when it comes to adding then exceptions are thrown.
Good catch @linde12 - if you still have it available, what kind of component are you seeing where the DOM node was expected?
This issue might actually be more for preact-compat, but we can always fix it there and mark it closed here.
Ah, yes, of course!
The component i got was, in my case, a DraggableRow. In my application i have a table using react-virtualized(which by the way seems to work fine) and each row is a draggable. I have made a MakeDraggable function which looks something like this:
import React, {Component, PropTypes} from 'react';
import {DragSource} from 'react-dnd';
import {findDOMNode} from 'react-dom';
const defaultSource = {
beginDrag: function (props) {
return props;
}
};
function collect (connect, monitor) {
return {
connectDragPreview: connect.dragPreview(),
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
};
}
export default (Component, type, options) => {
class Draggable extends Component {
render () {
let {connectDragPreview, connectDragSource, ...props} = this.props;
return (
<Component
{...props}
ref={c => connectDragSource(findDOMNode(c))}
/>
);
}
}
Draggable.propTypes = {
connectDragPreview: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired
};
let source = defaultSource;
if (options) {
source = {
...source,
...options.source
};
}
return DragSource(type || 'ANY', source, collect)(Draggable);
};
So then with my Row component i do something like this:
const DraggableRow = MakeDraggable(Row)
But when using it with Preact i get thrown an error saying that there is no method addEventListener on the node.
What HTML5Backend(from react-dnd) does internally is something like this: node.addEventListener('dragenter', handleDragEnter); where node should be a DOM node but in Preact's case is a component(instance of DraggableRow).
If react-dnd would have done node.base.addEventListener('dragenter', handleDragEnter); it would probably have worked.
I'm not sure what the difference causing this is but there has to be something
I'd be curious to know what c is in the code you posted - findDOMNode() should be looking for a .base property on it, which should always exist. Very odd...
@developit Wow! Nice catch. It turns out that findDOMNode actually returns the Row component. I will try to find why
Super funky. Perhaps the ref is getting invoked prior to a child component being rendered.... maybe something to do with Pure Functional Components in the tree.
I'd be curious if this implementation of findDOMNode() fixes it:
const findDOMNode = c => c._component ? findDOMNode(c._component) : c.base || c;
Seems like that. The code for findDOMNode returns the component itself if .base was falsy. But the strange thing is that Row isn't a functional component. It's a stateful component. I'll try your fix asap.
@developit That didn't seem to change things. The stateful component doesn't have a base-property if i put a breakpoint there with the debugger. But if i console.log them and check what the objects contain afterwards they have gotten a base-property. Can it have something to do with the ref being called too early?
Could be, yup. Haven't seen that before!
It(refs) works fine when i do it on "normal" components. I will debug some more
Something is fishy with refs i think. See the following code:
// Awesome HOC
export const MakeAwesome = WrappableComponent => {
class AwesomeWrapper extends Component {
render () {
return <WrappableComponent ref={c => {debugger; console.log(c)}} />
}
}
return AwesomeWrapper
}
// Usage
const Dude = () => <div>Oscar Linde</div>
const AwesomeDude = MakeAwesome(Dude)
ReactDOM.render(<AwesomeDude />, document.getElementById('root'))
In this case c.base is not defined whereas if it Dude is not wrapped by the HOC and has a ref on it like ReactDOM.render(<Dude ref={c => {debugger; console.log(c)}} />, document.getElementById('root')) then c.base is defined.
In vdom/component.js at L122 in the renderComponent function the following if-statement is made(i put out two comments):
if (inst && inst.constructor===childComponent) {
setComponentProps(inst, childProps, SYNC_RENDER, context);
}
else {
toUnmount = inst;
inst = createComponent(childComponent, childProps, context);
inst.nextBase = inst.nextBase || nextBase; // this is undefined as well
inst._parentComponent = component;
component._component = inst;
// Here setComponentProps is run even though inst has no base
setComponentProps(inst, childProps, NO_RENDER, context);
renderComponent(inst, SYNC_RENDER, mountAll, true);
}
inst is undefined here so setComponentProps will be run and at the end of that function the ref-callback will be run. @developit do you have any idea what could be causing this?
EDIT: The above can, of course, be simplified even further. Any class-component that returns a component will produce this behavior.
Hmm - that actually all seems correct. It's just that it shouldn't be calling the ref before the base has been added...
hmm.
Yeah but setComponentProps is run synchronously so it would always execute before the base is set? Or am i missing something?
I think it's something in the setComponentProps and renderComponent "loop" thats wrong. I put out some console logs:
preact.js:283, setComponentProps: Rendering App SYNC_RENDER
preact.js:323, renderComponent: Calling setComponentProps NO_RENDER on AwesomeDude
preact.js:325, renderComponent: Calling renderComponent SYNC_RENDER on AwesomeDude
preact.js:323, renderComponent: Calling setComponentProps NO_RENDER on Dude
preact.js:290, setComponentProps: Calling ref of Dude
preact.js:325, renderComponent: Calling renderComponent SYNC_RENDER on Dude
If i, naively, ran the renderComponent method with SYNC_RENDER before the ref was called then it worked as expected. This of course isn't a fix but for some reason the ref is being called before the component is rendered. The ref is being called from setComponentProps, which in the case of NO_RENDER, runs the ref before the component has rendered.
Again, naively, calling the ref after the last render(preact.js:325, renderComponent: calling renderComponent SYNC_RENDER on Dude) and in setComponentProps adding a if (component.__ref && opts !== NO_RENDER) { component.__ref(component); } works as well.
I'm not sure where the root of the problem is though
Still looking into it. I think it might be a quick fix actually, just need to skip refs if a constant is set
Sweet! Getting closer to supporting a more seamless migration from React to Preact :+1:
Yup! Refs were a sore spot for sure, this is likely the last of the issues with them.
Update: preact-dnd is now a thing, might be able to close this one out.
@developit This looks promising. I haven't tested it though, but i will try to do it this week and get back to you!
Most helpful comment
@developit This looks promising. I haven't tested it though, but i will try to do it this week and get back to you!