react-dnd: Cannot have two HTML5 backends at the same time

Hi Dan,

Just a quick one - I’m trying to use my own component that has react-dnd as a dependency in another app which itself uses react-dnd so the error above expected. In this case, what would be the best way to fix this?

Since the other component is my own, I can remove the DragDropContext call from while exporting the component but then that sacrifices the re-usability of the component. What do you advise?

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 75 (13 by maintainers)

Commits related to this issue

Most upvoted comments

Another approach that is a bit cleaner is to create a module that generates the decorator for a particular backend, and then use the decorator where needed:

lib/withDragDropContext.js

import {DragDropContext} from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';

export default DragDropContext(HTML5Backend);

components/MyComponent.js

import { Component } from 'react';
import withDragDropContext from '../lib/withDnDContext';

class MyComponent extends Component {
  
  render() {
    return (
     <div>
       // other children
     </div>
   );
}

export default withDragDropContext(MyComponent);

To solve my problem I did a singleton with this code:

import { DragDropManager } from 'dnd-core';
import HTML5Backend from 'react-dnd-html5-backend';

let defaultManager;

/**
 * This is singleton used to initialize only once dnd in our app.
 * If you initialized dnd and then try to initialize another dnd
 * context the app will break.
 * Here is more info: https://github.com/gaearon/react-dnd/issues/186
 *
 * The solution is to call Dnd context from this singleton this way
 * all dnd contexts in the app are the same.
 */
export default function getDndContext() {
  if (defaultManager) return defaultManager;

  defaultManager = new DragDropManager(HTML5Backend);

  return defaultManager;
}

And then in all the components that have a child that had DragDropContext(HTML5Backend) I remove it from those child and in their parents I do this:

import getDndContext from 'lib/dnd-global-context';

const ParentComponent = React.createClass({

  childContextTypes: {
    dragDropManager: React.PropTypes.object.isRequired,
  },

  getChildContext() {
    return {
      dragDropManager: getDndContext(),
    };
  },

  render() {
    return (<ChildComponentWithDndContext />);
  },

I think the key is that I only initialize dnd context once. What do you think?

Old issue, but in case somebody else ends up here from Google:

I had only 1 DND provider on my page, I was not integrating any other library that has DND, but I still ended up running into this error, somewhat randomly.

My problem was that the DragDropContextProvider was inside ReactRouter’s BrowserRouter element, which made the HTML5Backend to be reconstructed on every navigation, and if both the old page (that was navigated away from) and the new one (the one navigated to) had DND elements, the above error would occur.

The solution was to move the DragDropContextProvider out of BrowserRouter.

A better solution using Hooks (thanks @jchonde):

import { DndProvider, createDndContext } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import React, { useRef } from "react";

const RNDContext = createDndContext(HTML5Backend);

function useDNDProviderElement(props) {
  const manager = useRef(RNDContext);

  if (!props.children) return null;

  return <DndProvider manager={manager.current.dragDropManager}>{props.children}</DndProvider>;
}

export default function DragAndDrop(props) {
  const DNDElement = useDNDProviderElement(props);
  return <React.Fragment>{DNDElement}</React.Fragment>;
}

so then you can use elsewhere:

import DragAndDrop from "../some/path/DragAndDrop";

export default function MyComp(props){
   return <DragAndDrop>....<DragAndDrop/>
}

Probably a bit too late but I’ve come up with a similar but slightly different solution. I implemented a higher order component and simply use it in all my DragDropContext aware components.

Code looks like this (TypeScript):

import * as React from 'react';
import {DragDropContext} from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';

// context singleton
let context: Function;

export function withDragDropContext<P>(
    Component: React.ComponentClass<P> | React.StatelessComponent<P>,
): React.ComponentClass<P> {
    // ensure a singleton instance of the context exists
    if (!context) {
        context = DragDropContext<P>(HTML5Backend);
    }

    return context(Component);
}

And then use it as follows in your components:

import * as React from 'react';
import {withDragDropContext} from 'components/WithDragDropContext';

class MyClass extends React.Component<IMyClassProps, {}> {
    // ...
}

export default withDragDropContext<IMyClassProps>(MyClass);

N.B. I’ve not tried yet but you should probably be able to populate the context variable during the declaration:

const context = DragDropContext(HTML5Backend);

and then skip the if (!context) {... part.

DragDropContextProvider标签里加一个不重复的key就解决了<DragDropContextProvider backend={HTML5Backend} key={Math. random()}></DragDropContextProvider>

Using hooks

import { createDndContext } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";

const manager = useRef(createDndContext(HTML5Backend));

return (
  <DndProvider manager={manager.current.dragDropManager}>
      ....
  </DndProvider>
)

why not simply do this inside the HTML5Backend code

This is a great point. If the component is able to detect multiple backends can’t it directly have the logic of falling back to the existing backend in scope?

In the typescript, i made the below component. (thanks @jchonde @ttessarolo )

import { DndProvider, createDndContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import React, { PropsWithChildren, useRef } from 'react';

const RNDContext = createDndContext(HTML5Backend);

function DragAndDrop({ children }: PropsWithChildren<{}>): JSX.Element {
  const manager = useRef(RNDContext);
  return <DndProvider manager={manager.current.dragDropManager}>{children}</DndProvider>;
}

export default DragAndDrop;

And used a component like this

function SomeComponent(): JSX.Element {
  return (
    <DragAndDrop>
      ...
    </DragAndDrop>
  );
}

DndProvider has an options prop in where you can set rootElement which bounds DnD to that specified context, and unfortunately it isn’t documented well. This approach solved all my issues, as I had other component which was using DnD and they were out of my boundary and I wasn’t able to make HTML5Backend singleton. I tried this approach with "react-dnd": "^14.0.2"

const myFirstId = 'first-DnD-Containier';
const mySecondId = 'second-DnD-Containier';

export const DndWrapper = React.memo((props) => {
  const [context, setContext] = useState(null);

  useEffect(() => {
    setContext(document.getElementById(props.id))
  },[props.id])

  return context ? (
      <DndProvider backend={HTML5Backend} options={{ rootElement: context}}>
          {props.children}
      </DndProvider>
  ) : null;
});

export default function App() {
  return (
    <div>
      <div id={myFirstId}>
        <DndWrapper id={myFirstId}>
               <MyOtherComponents /> 
        </DndWrapper>
      </div>
      <div id={mySecondId}>
          <DndWrapper id={mySecondId}>
            <MyOtherComponents />
          </DndWrapper>
      </div>
    </div>
  );
}

P.S. Make sure by the time you call document.getElementById the DOM exist with ids.

export default function MyTagControlContext(DecoratedClass) {
  return DragDropContext(HTML5Backend)(DecoratedClass);
}

and you can tell users to either wrap their top-level component into MyTagControlContext or use DragDropContext directly if they already use React DnD.

This thread led me to solving my invariant problem by moving the HTML5Backend and DragDropContext higher in my component hierarchy, just like the docs recommend.

I mean exporting something like this from your library:

let defaultManager;
function getDefaultManager() {
    if (!defaultManager) {
        defaultManager = new DragDropManager(HTML5Backend);
    }
    return defaultManager;
}

class ReactTagContext {
    static contextTypes = {
        dragDropManager: PropTypes.object.isRequired
    };

    static childContextTypes = {
        dragDropManager: PropTypes.object.isRequired
    };

    getChildContext() {
        return {
            dragDropManager: this.context.dragDropManager || getDefaultManager()
        };
    }

    render() {
        return <ReactTag {...props} />
    }
}

This one is for those mortals who tried out ReactDnD for the first time, followed the tutorial and ended up with a 64 square chessboard. I could drag my knight around as I pleased. Problem was when I tried to drop it in one of the BoardSquare, it threw up with the 2 HTML5 backend issue.

The Fix

As others have mentioned in comments before, move the DragDropContextProvider rendering outside the app rerender cycle. As in, don’t directly do a ReactDOM.render as a callback to the observe function. Instead do this:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { DragDropContextProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import './index.css';
import App from './App';

ReactDOM.render(
    <DragDropContextProvider backend={HTML5Backend}>
        <App />
    </DragDropContextProvider>,
    document.getElementById('root')
)

App.js

import React, { Component } from 'react';
import Board from './Board';
import { observe } from './Game';

class App extends Component {
    state = {
        knightPosition: [0, 0]
    }

    componentDidMount = () => {
        observe(knightPosition => {
            this.setState(prevState => ({
                ...prevState,
                knightPosition
            }));
        });
    }

    render() {
        return (
            <div className="App">
                <Board knightPosition={this.state.knightPosition} />
            </div>
        );
    }
}

export default App;

I’m running into a similar issue and I’d like to better understand why this limitation exists in the first place because it’s making writing re-usable components quite difficult. As it is, each component that uses react-dnd needs to be aware of various contexts that may exist in the application and deal with them accordingly. It would be preferable if each component could manage its own drag behaviour/context regardless of what else may be going on in the rest of the application.

For example, I may want to have an application screen which has several File Upload components, a sortable menu and a game with draggable elements. Each of those components have very different ways of dealing with drag events, and should really be in charge of their own context.

My first question, is why not simply do this inside the HTML5Backend code?

setup() {
    ...

    // Events already setup - do nothing
    if (this.constuctor.isSetUp) return;

    // Don't throw an error, just return above.
    //invariant(!this.constructor.isSetUp, 'Cannot have two HTML5 backends at the same time.');

    this.constructor.isSetUp = true;
    ...
  }

What is the modern approach to this?

Seems both createDndContext and DragDropContext are removed

I tried some of the suggestion above but get the following error: Module ‘“…/node_modules/react-dnd/dist/types”’ has no exported member ‘createDndContext’.t Version: “react-dnd”: “^13.1.1”, “grep -R -i createDndContext .” does not find any such string in the source files.

@gaearon I know this is old, but how do I actually get the hold of DragDropManager in your example? It’s not exported anywhere.

defaultManager = new DragDropManager(HTML5Backend);

My problem is that I’m rendering a few widgets on a page via some 3rd party API, and I can’t move DragDropContextProvider higher than my actual widget.

createDndContext was removed some time ago. The recommended method is to attach <DndProvider> in a stable container component that contains your drag/drop components

I tried lots of the suggestions above but for my use case none of them worked for me. I have a chrome extension which adds my widget directly into the DOM of webpages. This error occurs when the webpage uses react-dnd (for example Discord) and my widget tries to load react-dnd itself. I can’t use the options above because I wouldn’t be able to control the webpage’s DndContext singleton.

The error can be solved by changing rootElement from it’s default value, window (see below). https://github.com/react-dnd/react-dnd/blob/339dd7aade81477af46becf4a67a2f65d6cfeb74/packages/backend-html5/src/OptionsReader.ts#L36

I did the following:

....

const el =  document.getElementById('my-widget-root-element')

<DndProvider backend={HTML5Backend} options={{ rootElement: el }}>
    {props.children}
</DndProvider>

....

This makes it so that root.__isReactDndBackendSetUp is false when react-dnd is instantiated in my widget because root is my element, not window.

https://github.com/react-dnd/react-dnd/blob/339dd7aade81477af46becf4a67a2f65d6cfeb74/packages/backend-html5/src/HTML5BackendImpl.ts#L97-L114

Through my inspection, this rootElement option is not documented, though this page https://react-dnd.github.io/react-dnd/docs/api/dnd-provider states that the options prop to DndProvider is dependent on the backend in use. There is no mention of the options available to the HTML5Backend here https://react-dnd.github.io/react-dnd/docs/backends/html5

@martinnov92 I think this approach solves the problem you mention where two codebases are both using reactDnd. If possible, they should both update the rootElement option to only listen to the part of the DOM that they are associated with instead of the entire window.

Hello, does anybody know how to solve when you have one component wrapped in DragDropContext and then another component which uses DragDropContext as well, but this component (react big calendar) is an npm package, so remove it from there is not a solution and they are next to each other, not parent-child…

Likewise to @mauron85 and @szopenkrk

Fortunately, I could find the (hidden) answer of @gcorne (https://github.com/react-dnd/react-dnd/issues/186#issuecomment-282789420). It solved my problem - which was assumed to be tricky - instantaneously. @prakhar1989 I have the feeling it is a real party answer and that it should be highlighted somehow, so maybe link it in the bug description?

The newer way is to use the react context API:

<DndProvider backend={HTML5Backend}>
   {/* app */}
</DndProvider>

The solution I figured out that works for me and allows me to use either HTML5 or Touch backend is:

Create a singleton HOC component:

import {DragDropContext} from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import TouchBackend from "react-dnd-touch-backend";

const DragAndDropHOC = props => {
    return <React.Fragment>
        {props.children}
    </React.Fragment>
};

export default {
    HTML5: DragDropContext(HTML5Backend)(DragAndDropHOC),
    Touch: DragDropContext(TouchBackend)(DragAndDropHOC),
}

Then a provider component:

const DragDrop = props => {
    if (props.isTouch) {
        return <DragDropContext.Touch>{props.children}</DragDropContext.Touch>
    } else {
        return <DragDropContext.HTML5>{props.children}</DragDropContext.HTML5>
    }
};

And use <DragDrop isTouch={props.isTouch} /> wherever I need it.

@abobwhite This is how I’ve solved it. It’s definitely not a great solution but it seems to work as of now.

Hope this helps,

In the typescript, i made the below component. (thanks @jchonde @ttessarolo )

import { DndProvider, createDndContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import React, { PropsWithChildren, useRef } from 'react';

const RNDContext = createDndContext(HTML5Backend);

function DragAndDrop({ children }: PropsWithChildren<{}>): JSX.Element {
  const manager = useRef(RNDContext);
  return <DndProvider manager={manager.current.dragDropManager}>{children}</DndProvider>;
}

export default DragAndDrop;

And used a component like this

function SomeComponent(): JSX.Element {
  return (
    <DragAndDrop>
      ...
    </DragAndDrop>
  );
}

This approach worked for me. react-dnd version ^11.1.3

I tried everything from above comment but typescript throwing below error: Module ‘“react-dnd”’ has no exported member ‘createDndContext’. Version: “react-dnd”: “14.0.0”, “react-dnd-html5-backend”: “14.0.0”

@guang2013 if you are using a library that depends on DragDropContext from react-dnd, this technique won’t work because the library won’t use your unified dndContext. Some libraries like react-sortable-tree will allow you to use the components without a context so you can wrap them yourself though.

I met the same situation as @andresgutgon , but could not solve with his method and all the method here I tried and no one works, is there anybody can help me? Thank you.

What if there are 2 DrapDropContext in the app but they are not parent and child?

My case:

  1. Intialize first dnd Context
  2. Initialize as sibbling a second dnd Context. 💣

I can’t check on childContext because second component is not a child of first dnd component/context