reactstrap: Tooltip cannot find ID of a React element rendered next to it

Issue description

  • components: Tooltip
  • reactstrap version 5.0.0-alpha.4
  • import method umd, not full version
  • react version 16.2.0
  • bootstrap version 4.0.0-beta.3

What is happening?

Sometimes, when I render the attached code, I get this error: The target <target id> could not be identified in the dom, tip: check spelling. However, I can’t get the codepen to reproduce this, but it consistently happens in my larger application base, which uses the same data flow. The bug occurs when the button in the codepen is pressed, but perhaps React functions differently in my case. I made a small change in the codepen, the bug is now reproduceable.

The full application is here: https://github.com/kenzierocks/OurTube/blob/master/client/js/navbar.tsx#L78

I can help set this up to run if needed, but it’s still in the middle of initial development

What should be happening?

It should render my custom tooltip properly, with no errors.

Code

https://codepen.io/anon/pen/xpWJzN

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 20
  • Comments: 42 (7 by maintainers)

Commits related to this issue

Most upvoted comments

@yidingalan enzyme’s mount takes a second parameter; options. Options has a property, attachTo which is used to tell it where to mount the component. It’s not enough to add a div to the body, you have to then tell it to mount the component to that div.

it('test', () => {
  const div = document.createElement('div');
  document.body.appendChild(div);
  const wrapper = mount(<Component {...props} />, { attachTo: div });
});

More information about mount and it’s options: https://github.com/airbnb/enzyme/blob/master/docs/api/mount.md#mountnode-options--reactwrapper

I was having this issue in my tests, which originally was:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

describe("App", () => {
	test("renders without crashing", () => {
		const div = document.createElement("div");
		ReactDOM.render(<App />, div);
	});
});

This would give the error discussed.

To solve it (at least for my tests), I had to add the div to the body, since the tooltip tries to query the document. This was the fix:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

describe("App", () => {
	test("renders without crashing", () => {
		const div = document.createElement("div");
		document.body.appendChild(div);
		ReactDOM.render(<App />, div);
	});
});

I hope this can bring any insights to what you are experiencing.

I had the same problem using react hooks. I solved it with a useEffect. My component is a Link component (a wrapper around <a>), I share the code in case someone needs:

import React, {Fragment, useEffect, useRef, useState} from 'react'
import classNames from 'classnames'
import Tooltip from 'reactstrap/lib/Tooltip'

export default function({className, to, tooltip, onClick, children}) {
  const linkRef = useRef()
  const [ready, setReady] = useState(false)
  const [open, setOpen] = useState(false)

  function handleClick(event) {
    event.preventDefault()
    onClick(event)
  }

  function toggle() {
    setOpen(!open)
  }

  useEffect(() => {
    if (linkRef.current) {
      setReady(true)
    }
  }, [linkRef.current])

  return (
    <Fragment>
      <a
        className={classNames(className)}
        ref={linkRef}
        target="_blank"
        rel="noopener noreferrer"
        href={to || '#'}
        {...(onClick ? {onClick: handleClick} : {})}
      >
        {children}
      </a>

      {tooltip && ready && (
        <Tooltip
          placement="bottom"
          isOpen={open}
          target={linkRef.current}
          toggle={toggle}
        >
          {tooltip}
        </Tooltip>
      )}
    </Fragment>
  )
}

You are generating random ID values each render. So when you open the tooltip, it will create a new ID and when the tooltip tries to find an element with the new ID, it will not have been put into the DOM yet. If you cache the IDs (created them once in your constructor and reference them from state) you should be able to avoid the issue.

I’m having an issue that I think is similar to this.

In at least two places, the code checks for the target during render:

(note: commit ID used is just what the current master is, not related to issue)

This means that a component that renders the target and the tooltip (UncontrolledTooltip in my case) will only work on the subsequent render.

I hit this issue in the following scenario:

  • component renders
  • a state change (router) causes it to re-render with new parameters
  • leave mouse hovering over tooltip (the part that makes it difficult to test)
  • I was generating the id using an id unique to that row
  • on the render with the new parameters, the new element does not exist yet and cannot be found

I fixed my issue by adding a key={row.id} to the UncontrolledTooltip instance. This prevented react from re-using the component when the basis for the target id changed.

I wrote a crude test where I modified reactstrap and moved the getTarget calls into componentDidUpdate and it seemed to also fix the issue.

If there is interest, I can clean up my code a little and submit a PR.

@TheSharpieOne Seeing this issue on UncontrolledTooltip at render time where the isOpen prop should be false (there is no way for the tips to be hovered before rendering).

FOLLOW UP EDIT So, in my case, it turned out that I had some dynamic complex id names like field.subprop-1. When I removed the . from the names the tips started working again. Hopefully this helps

The JSDOM issue is because you are not mounting your application/component to the document during testing. Simply mount your application/component to the document JSDOM creates and it will work. This is needed because the reactstrap code looks in the document for the target and if the target is mounted outside of the document it cannot find it. If you would like to investigate a better way for reactstrap to locate the target or something to help the tests not need to mount the application/component in the document, go for it.

The issue is that it is trying to find your target element before it is rendered to the DOM. This happens when the tooltip is open when it is initialized. Not too sure how to address this, but the workaround it is either toggle it after it initializes or provide a ref or function to get a ref/DOM node to target

I’ve managed to reduce the problem even further: it seems that the problem is much simpler than I first thought. New codepen here has the issue.

Same issue here and none of the solutions above have been working so far =( We are also having: TypeError: Cannot read property 'removeEventListener' of undefined

Thanks all. FYI that this is still an issue.

Worth mentioning that the same issue exists with popovers and continues to be a problem that we have to work around in hacky ways (for both tooltips and popovers). The useRef solution in this thread, which we’ve used until recently has its own issues and introduces console warnings - in addition to a bunch of confusing code to our components that exists entirely to accommodate this quirk in tests.

reproduction: https://codesandbox.io/s/144zyk1zjq

Modifying reactstrap to get the target in the appropriate lifecycle methods appears to fix this.

EDIT: a workaround/fix is to add key={counterId} to the UncontrolledTooltip to prevent react from re-using the element when the id changes.

The issue is the way React handles parameters for IDs with strings and ints. You can’t use an numeric starting value as the ID. (Hence why your Math.randoms() will break and your test values work) It must start with a string. We should add a note to this to the documentation portion for tooltips.

/cc @TheSharpieOne

Yeah, the target is a css selector, so the . indicates classname. We may want to initially search for just id (via getElementById) and fallback to css selector.

Unfortunately we are using an ancient version of React without hooks 😞 , I will have to find a way to do without them.

React hooks are just a shortcut for what react already does. It’s worth reading through Using the State Hook and Using the Effect Hook.

Using classes, this would be something like:

export default class MyComponent extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      ready: false,
    };
    this.myRef = React.createRef();
  }

  componentDidMount() {
    this.setReady();
  }
  
  componentDidUpdate() {
    this.setReady();
  }
  
  setReady() {
    if (this.myRef.current) {
      this.setState({
        ready: true,
      });
    }
  }


  render() {
    const { ready } = this.state;
    return (
      <>
        <button
          ref={this.myRef}
          type="button"
          id={id}
          aria-describedby="tooltip-id"
        >
          Show tooltip
        </button>
        {ready && <Tooltip id="tooltip-id">some text</Tooltip>}
      </>
    );
  }
}

My current solution is by using generated ID, but I make sure that it only created once by using zero-dependencies useMemo, thus successfully generated a random ID that will not change in subsequent re-render. Hope this helps someone. Any suggestions if this solution has a downside are welcomed.

Code:

import React, { useState, useMemo } from "react";
import { Tooltip as BsTooltip } from "reactstrap";

const Tooltip = ({ description, children }: TooltipProps) => {
  const [tooltipOpen, setTooltipOpen] = useState(false);
  const toggle = () => setTooltipOpen(!tooltipOpen);
  const tooltipId = useMemo(
    () => "tooltip" + Math.floor(Math.random() * 1000),
    []
  );

  return (
    <>
      <BsTooltip
        placement="top"
        isOpen={tooltipOpen}
        autohide={false}
        target={tooltipId}
        toggle={toggle}
      >
        {description}
      </BsTooltip>
      <div id={tooltipId}>{children}</div>
    </>
  );
};

export default Tooltip;

interface TooltipProps {
  description?: string;
  children: React.ReactChild;
}

Key workaround suggested by @pmacmillan works for me.

The issue seems to happen if id is generated dynamically, and then the component is re-rendered (so id will change upon re-rendering).

@soywod , that’s a very good solution! Using reference and to make sure that Tooltip will render after reference is applied.

Hi everyone,

just wanted to drop in quickly, and say thanks for all of your work on reactstrap, and specifically on this issue! I would like to confirm that this still occurs with 5.0.0-beta; as for @Cretezy, in-browser production code works fine but CI builds via jsDom fail for me on the most basic tests; their workaround fixes things successfully.

I’ve been able to isolate the issue to UncontrolledTooltip components (I think) added to the page after the initial render, would be happy to investigate further if this is useful – please let me know.

All the best, and thanks again to you all for this awesome library, it makes my work easier every day!

-Felix