Skip to content
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

Cannot have two HTML5 backends at the same time #186

Closed
prakhar1989 opened this issue Jun 8, 2015 · 75 comments
Closed

Cannot have two HTML5 backends at the same time #186

prakhar1989 opened this issue Jun 8, 2015 · 75 comments

Comments

@prakhar1989
Copy link

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?

@gaearon
Copy link
Member

gaearon commented Jun 8, 2015

Check out the source for DragDropContext. I think you should be able to reuse the existing manager if it is specified by a component above the tree. But it's still a tricky question.. Do you have any proposed solutions?

@prakhar1989
Copy link
Author

I think you should be able to reuse the existing manager if it is specified by a component above the tree.

Even if this is possible, the component that is exported should be able to work independently as well, and in cases where the backend exists (in the app where the component is being used) somewhere up the chain should be reused. I'll try to read the source and see if this can be pulled off.

Unfortunately, the only solution I can think of right now, is to export the final component as it is, and expect the user to add a DragDropContext with a backend of their choosing. What do you think?

@gaearon
Copy link
Member

gaearon commented Jun 9, 2015

the component that is exported should be able to work independently as well, and in cases where the backend exists (in the app where the component is being used) somewhere up the chain should be reused.

Yes, this is possible by not using DragDropContext altogether and instead manually using dragDropManager in the context. Your component may look into context, and either pass the existing dragDropManager down the context, or create its own. It seems somewhat fragile though.

Unfortunately, the only solution I can think of right now, is to export the final component as it is, and expect the user to add a DragDropContext with a backend of their choosing. What do you think?

I think this is the most flexible solution. You can also be opinionated there and export <MyComponentContext> that applies DragDropContext(HTML5Backend).

@prakhar1989
Copy link
Author

export <MyComponentContext> that applies DragDropContext(HTML5Backend)

I'm sorry, I don't quite understand this. Can you clarify this a bit more?

@gaearon
Copy link
Member

gaearon commented Jun 9, 2015

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.

@prakhar1989
Copy link
Author

Ah! How about this? Does this look too ugly?

// in main component file
module.exports = {
    WithContext: DragDropContext(HTML5Backend)(ReactTags),
    WithOutContext: ReactTags
};

The usage can then be something like

var ReactTags = require('react-tags').WithContext; // if your app doesn't use react-dnd
var ReactTags = require('react-tags').WithOutContext; // if your app already uses react-dnd.

@gaearon
Copy link
Member

gaearon commented Jun 9, 2015

I don't think this would work because each <ReactTags> would get its own copy of backend and lead to the invariant error you pasted above because those backends handle the same global window events.

@gaearon
Copy link
Member

gaearon commented Jun 9, 2015

What I think will work is you can manually create dragDropManager (just like DragDropContext does internally) and use the same instance of it for all ReactTag instances—with a fallback to the manager defined in context.

@gaearon
Copy link
Member

gaearon commented Jun 9, 2015

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} />
    }
}

@prakhar1989
Copy link
Author

Thanks a lot, Dan! I'll try this out and get back to you. Thank you for sharing the code 😀

@gaearon
Copy link
Member

gaearon commented Jun 9, 2015

No prob.. If you do it like this, just export that class instead of exporting ReactTags directly. It should be usable "as is", without any wrappers or decorators.

@prakhar1989
Copy link
Author

So Dan! For the heck of it, I was trying out the multiple exports solution above -

// in main component file
module.exports = {
    WithContext: DragDropContext(HTML5Backend)(ReactTags),
    WithOutContext: ReactTags
};

In my other app, I tried importing the component without the context and much to my delight, it seems to be working fine!

Do you think this is a hacky solution and I should go ahead with what you've proposed or should I let this be?

@gaearon
Copy link
Member

gaearon commented Jun 9, 2015

@prakhar1989 Are you sure this works with multiple <Tags /> on the page?

@prakhar1989
Copy link
Author

You had me for a second there! 😛

img

Thankfully it works!

@gaearon
Copy link
Member

gaearon commented Jun 9, 2015

Hmm, then maybe it's fine ;-). Let me know if you have any issues with this approach!

@gaearon gaearon closed this as completed Jun 9, 2015
@prakhar1989
Copy link
Author

Will do! Thanks a ton again for all your help.

PS: Do you have better naming ideas for WithContext and WithoutContext?

@gaearon
Copy link
Member

gaearon commented Jun 9, 2015

@prakhar1989 I'd probably just export WithContext version directly, and put NoContext as a static field on it.

@EvHaus
Copy link

EvHaus commented Jun 19, 2015

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;
    ...
  }

@prakhar1989
Copy link
Author

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?

@abobwhite
Copy link

Hi @gaearon - I am running into this issue as well except in my case, I have a page in which I've pieced together disparate react components within an angular template due to (angular) performance. What I have is a page which builds questions, choices, and more in a recursive tree structure. I also have a toolbar and a question library that use DnD to add things to the question tree. My problem is that I have now setup multiple react components that live within an angular context. Because of this, I am wrapping each of those with a DragDropContext which causes this error. I tried following the above thread but it's not entirely clear to me what I could do for these separate react components to share a context without converting everything else on my page to be React (not ideal). I am a bit familiar with the ES6 syntax but I am working on a project that is still using ES5. Is there a way to apply the shared DragDropManager concept from above? I've tried it so far and I don't seem to have access to DragDropManager since it's in dnd-core

Thanks for your help and for this awesome library!

P.S. If it matters, I'm using ngReact.

@abobwhite
Copy link

@prakhar1989 @globexdesigns @gaearon

I am wondering the same thing why HTML5 backend can't just re-use the backend if multiple are used? Per my previous comment, this is really making react-dnd unusable for me as I have multiple react areas within an angular page that need to be able to DnD between each other and am hitting a wall with this.

Anyone have any quick fix for this? I'm at a dead stop in my development.

@prakhar1989
Copy link
Author

@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,

@abobwhite
Copy link

Thanks, @prakhar1989 ! But I'm not following how the multiple export with one wrapped with context and one not solves the issue. My problem is not being potentially embedded into another application with react-dnd but rather not being able to wrap my entire dnd-enabled area (with several react and angular components/directives) in react so I was trying to wrap the context around just those react components in my page that support DnD...I'd love to try @gaearon 's approach from above but I don't have access to the DragDropManager in order to new one up...

@avk
Copy link

avk commented Feb 11, 2016

I'm having the exact same issue. I strongly agree with @abobwhite that this makes components less reusable.

@avk
Copy link

avk commented Feb 11, 2016

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.

@broncha
Copy link

broncha commented Jun 9, 2016

This is a weird problem. I am working on a nested reorderable component and I have DragDropContext nested inside the parent one. It seems to work standalone (but it still has nested DragDropContext).

But when I use that component inside another project which has DragDropContext initialised above the hierarchy, I get this error.

@jcblw
Copy link

jcblw commented Jun 13, 2016

I ran into this issue with an application I'm working on. I had full control over all the components so I ended up instead of using @DragDropContext(HTMLBackend) I ended up using something very close to @gaearon code in this comment to create a decorator that would give a shared drag drop context. It works really well.

@jchonde
Copy link

jchonde commented Nov 17, 2019

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>
)

@ttessarolo
Copy link

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/>
}

@cwtuan
Copy link

cwtuan commented Jan 8, 2020

My solution:
Make the children component don't import react-dnd directly.
Pass the DragDropContext and HTML5Backend from parent to children component.

@MJGang
Copy link

MJGang commented Jan 13, 2020

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

@milban
Copy link

milban commented May 29, 2020

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>
  );
}

@JSjump
Copy link

JSjump commented Jun 4, 2020

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

good job!

@qtnemo
Copy link

qtnemo commented Mar 2, 2021

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.

@adlerfaulkner
Copy link

adlerfaulkner commented Mar 8, 2021

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).

return this.optionsArgs?.rootElement || this.window

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.

public setup(): void {
const root = this.rootElement as RootNode | undefined
if (root === undefined) {
return
}
if (root.__isReactDndBackendSetUp) {
throw new Error('Cannot have two HTML5 backends at the same time.')
}
root.__isReactDndBackendSetUp = true
this.addEventListeners(root)
}
public teardown(): void {
const root = this.rootElement as RootNode
if (root === undefined) {
return
}

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

@annezhou920
Copy link

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>
  );
}

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

using a combination of @milban and @MJGang's solution worked for me. the drag and drop singleton needed the random key generation key={Math. random() to rid of the two HTML5 backend error

Related to: frontend-collective/react-sortable-tree#788

@nileshlande12
Copy link

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"

@darthtrevino
Copy link
Member

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

@kurochkinSergei
Copy link

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

@philipaarseth
Copy link

What is the modern approach to this?

Seems both createDndContext and DragDropContext are removed

@darthtrevino
Copy link
Member

The newer way is to use the react context API:

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

@arshKhachatryan
Copy link

arshKhachatryan commented Nov 24, 2021

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.

@jameskerr
Copy link

The recommended method is to attach in a stable container component that contains your drag/drop components

@darthtrevino Can you expand on this a little more? This seems like the solution to an error I'm having but it would help if I understood it more. Thank you.

@sputnikW
Copy link

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.

work for me, react-dnd@16.0.1

@kaydeniz
Copy link

The newer way is to use the react context API:

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

it worked for me, thank you

@shirleying-jing
Copy link

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.

That works for me! Really save my ass!! react-dnd@14.0.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests