Tooltip5.0.0-alpha.4umd, not full version16.2.04.0.0-beta.3Sometimes, 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
It should render my custom tooltip properly, with no errors.
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.
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 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 tried creating the element separately, to try to referencing the target directly (am I just misunderstanding? The docs don't have an example), but the error suggests that it's still looking for an element attached to the DOM at render-time: https://codepen.io/anon/pen/WdBBMW
@k-funk I waited for a ref set on the target to !== undefined, then I was able to refer to the target DOM id.
EDIT ... but this approach doesn't seem to play well with my DOM router and back-buttoning history, perhaps due to a component lifecycle issue,
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
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 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.
Ah, I see, thanks! So that's how @Cretezy's workaround functions. That definitely makes sense. In that case, I think this part of the issue might be considered fixed.
Thanks a lot for your super-quick response, and again, thanks for your work on reactstrap!
@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
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.
Hey @TheSharpieOne , I tried this but it still throws the same error
it('test', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const wrapper = mount(<Component {...props} />);
});
Any idea why?
@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 have the same problem. I am using tooltip for a button. At first, everything seems ok but when I hover over the button the error is being thrown: Uncaught Error: The target 'tooltip-cftoce14f' could not be identified in the dom, tip: check spelling. Here is my code for the tooltip:
class FxTooltip extends React.Component {
constructor(props) {
super(props)
this.toggle = this.toggle.bind(this)
this.state = {
tooltipOpen: false
}
}
toggle() {
this.setState({
tooltipOpen: !this.state.tooltipOpen
})
}
render() {
const { body, placement = 'top', children } = this.props
if (body) {
const id = Math.random().toString(36).substr(2, 9)
return <div>
<div id={`tooltip-${id}`}>
<Button>Some button</Button>
</div>
<Tooltip
placement={placement}
toggle={this.toggle}
isOpen={this.state.tooltipOpen}
target={`tooltip-${id}`}
autohide={false}
>
<span style={{ fontSize: 12, padding: '0 8px', lineHeight: '20px' }}>{body}</span>
</Tooltip>
</div>
}
return <div>
{children}
</div>
}
}
Version details:
Am I doing something wrong?
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.
@TheSharpieOne thanks a lot it worked! Completly missed this one 馃槃
I have the same problem as @prodevelsol had 2 days ago but I can't figure it out... 馃槥 Can you help me find what is wrong in my code? It returns this error message :
The target 'rfmygrx07' could not be identified in the dom, tip: check spelling
Where 'rfmygrx07' is the random id
import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import { Row, Col, Tooltip } from 'reactstrap';
import s from './BadgeBenchmarkTooltip.css';
import Badge from '../../atoms/Badge/Badge';
class BadgeBenchmarkTooltip extends React.Component {
constructor(props) {
super(props);
const generateID = Math.random().toString(36).substr(2, 9);
this.toggle = this.toggle.bind(this);
this.state = {
tooltipOpen: false,
id: generateID,
};
}
toggle() {
this.setState({
tooltipOpen: !this.state.tooltipOpen,
});
}
render() {
return (
<span id={this.state.id}>
<Badge deltaYear={this.props.deltaYear} />
<Tooltip
className={s.tooltipAll}
innerClassName={s.tooltip}
placement="auto"
isOpen={this.state.tooltipOpen}
target={this.state.id}
toggle={this.toggle}
>
<Row>
<Col>
<span className={s.tooltipTitle}>{this.props.title}</span>
</Col>
</Row>
<Row>
<Col xs={12}>
<span className={s.tooltipSubTitle}>Year -1</span>
</Col>
<Col xs={12}>
<span className={s.tooltipDate}>
From June 2016 to November 2016
</span>
</Col>
<Col xs={12}>
<Badge deltaYear={12} />
</Col>
<Col xs={12}>
<span className={s.tooltipNote}>{this.props.deltaYear}</span>
</Col>
</Row>
</Tooltip>
</span>
);
}
}
export default withStyles(s)(BadgeBenchmarkTooltip);
Note : As you can notice, my project integrates https://github.com/kriasoft/isomorphic-style-loader so classes are decorated, but not IDs, so i don't think it is the source of my problems (but I prefer to notice, just in case...)
Thanks ! 馃槂
@Thoum I think this can be caused by the fact that you put the id on the root level component so the component is being re-rendered and the id is not in the state. You can try to assign it to the Badge component instead.
<Badge deltaYear={this.props.deltaYear} id={this.state.id} />
is it the badge that has to be tooltip-ed?
Hope this is the issue 馃檪
@prodevelsol :
Sadly, this doesn't work... 馃槥 ...
const generateID = Math.random().toString(36).substr(2, 9);
this.toggle = this.toggle.bind(this);
this.state = {
tooltipOpen: false,
idtool: this.props.idTooltip,
// idtool: `tooltip${generateID}`,
};
Full code with modifications operated :
import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import { Row, Col, Tooltip, Button } from 'reactstrap';
import s from './BadgeBenchmarkTooltip.css';
import Badge from '../../atoms/Badge/Badge';
class BadgeBenchmarkTooltip extends React.Component {
constructor(props) {
super(props);
const generateID = Math.random().toString(36).substr(2, 9);
this.toggle = this.toggle.bind(this);
this.state = {
tooltipOpen: false,
id: this.props.idTooltip,
// id: `tooltip${generateID}`,
};
}
toggle() {
this.setState({
tooltipOpen: !this.state.tooltipOpen,
});
}
render() {
return (
<span className={s.tooltipBench}>
<Badge id={this.state.id} deltaYear={this.props.deltaYear} />
<Tooltip
className={s.tooltipAll}
innerClassName={s.tooltip}
placement="auto"
isOpen={this.state.tooltipOpen}
target={this.state.id}
hideArrow
toggle={this.toggle}
>
<Row>
<Col>
<span className={s.tooltipTitle}>{this.props.title}</span>
</Col>
</Row>
<Row className="mt-3">
<Col xs={12}>
<span className={s.tooltipSubTitle}>Year -1</span>
</Col>
<Col xs={12}>
<span className={s.tooltipDate}>
{this.props.datesForYear}
</span>
</Col>
<Col xs={12} className="my-3">
<Badge deltaYear={this.props.deltaYear} />
</Col>
<Col xs={12}>
<span className={s.tooltipNote}>{this.props.noteYear}</span>
</Col>
</Row>
<Row>
<Col>
<hr />
</Col>
</Row>
<Row>
<Col xs={12}>
<span className={s.tooltipSubTitle}>Period -1</span>
</Col>
<Col xs={12}>
<span className={s.tooltipDate}>
{this.props.datesForPeriod}
</span>
</Col>
<Col xs={12} className="my-3">
<Badge deltaYear={this.props.deltaPeriod} />
</Col>
<Col xs={12}>
<span className={s.tooltipNote}>{this.props.notePeriod}</span>
</Col>
</Row>
</Tooltip>
</span>
);
}
}
export default withStyles(s)(BadgeBenchmarkTooltip);
Thank you for your help ! 馃槂
I removed isomorphic-style-loader (and the classNames with references to the imported styles) and replaced Badge with reactstrap's badge (just to get it to render) and it works fine:
https://stackblitz.com/edit/reactstrap-v5beta-3-9cj9dj?file=Example.js
Can you inspect and confirm that the ID on the rendered element matches what the error is saying cannot be found? I see some stuff in that isomorphic-style-loader project which manipulates IDs, but I am not sure if that is the case.
Yes i can confirm the ID is correct.
I made a console log of the state.id value and it's the same ID on the rendered element (no decoration added).
As soon as i reimport my tooltip component, the error shows :(
NB : no problem with 'static / non-generetad'. The problem is only when i generate the id randomly :/
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
The rules of HTML still apply, but I suppose we can add a note to explicitly point it out.
Typically when I generate an ID or something, I try to prefix it with the type of thing for which it was generated. Looking at his code, @Thoum is doing the same thing: tooltip${generateID} (at least at one point as it is commented out in the snippet posted).
The example I posted before had randomly generated ids and it worked fine 馃槙
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:
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.
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.
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>
)
}
@soywod , that's a very good solution! Using reference and to make sure that Tooltip will render after reference is applied.
I had the same problem and decided to wrap @soywod's solution in a component I can reference.
Here is how I use my component:
<TooltipContext>
<TooltipTarget>
{...arbitrary children here...}
</TooltipTarget>
<UncontrolledTooltip ...all props except target...>
{... tooltip contents...}
</UncontrolledTooltip>
</TooltipContext>
And here is the component that does the wiring up of target, and uses the Ref trick to work around the render order.
import React, { createContext, useRef, useContext, ReactNode, RefObject } from "react";
import { UncontrolledTooltip as BaseUncontrolledTooltip, UncontrolledTooltipProps } from "reactstrap";
const Context = createContext<RefObject<HTMLDivElement>>({ current: null });
export const TooltipContext = ({ children }: { children: ReactNode }) => {
const ref = useRef<HTMLDivElement>(null);
return (
<Context.Provider value={ref}>
{children}
</Context.Provider>
);
}
export const TooltipTarget = ({ children }: { children: ReactNode }) => {
const ref = useContext(Context);
return (
<div ref={ref}>
{children}
</div>
);
}
export const UncontrolledTooltip = ({ children, target, ...props }: Partial<UncontrolledTooltipProps>) => {
const ref = useContext(Context);
return !ref.current ? null : (
<BaseUncontrolledTooltip target={ref.current} {...props}>
{children}
</BaseUncontrolledTooltip>
);
}
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).
@TheSharpieOne What if you're using react-test-renderer instead of enzyme's mount? How would you address the issue? Actually I used @soywod 's solution for it. Putting a ref to the target and checking if it's ready, but I don't think it's a good solution because you need to modify the code just to make the test work.
This is only happening for me on snapshot testing and not in production.
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;
}
@AsadSaleh I would use a UUID instead of generating a random number manually. Also, I think a useRef is more appropriate, since it's exactly what we need: a mutable reference of the tooltip.
useRef is less efficient for holding a random ID because it needs to generate randomness every call, there's no lazy version like with memo. If you're suggesting holding a "ref", then it might be different.
Yes sorry, I wasn't clear enough. The @AsadSaleh solution looks good, but I would use a UUID instead. Personally I would not use this method, I prefer to keep a tooltip ref as I shared in my previous comment.
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.
Thanks all. FYI that this is still an 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
You can still use @TheSharpieOne solution for the meantime:
const btnRef = React.useRef(null);
const [ready, setReady] = React.useState(false);
React.useEffect(() => {
if (btnRef.current) {
setReady(true);
}
}, [btnRef]);
<button
ref={btnRef}
type="button"
id={id}
aria-describedby="tooltip-id"
>
Show tooltip
</button>
{ready && <Tooltip id="tooltip-id">some text</Tooltip>}
Unfortunately we are using an ancient version of React without hooks 馃槥 , I will have to find a way to do without them.
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>}
</>
);
}
}
@k-funk hey, thanks a bunch. I needed to make a little fix to the setReady method:
setReady() {
if (this.myRef.current && this.state.ready === false) {
this.setState({
ready: true,
});
}
}
Dammit some of my tests are still failing, I think for the time being I will have to remove it.
@yidingalan enzyme's
mounttakes a second parameter;options. Options has a property,attachTowhich 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
This worked perfectly when using mount(). I had to make a couple adjustments that hopefully might help if you're stuck; first of which is setting the actual id with setAttribute(), and you set this to the tooltip id that react can't locate. Second, I had multiple tooltips in a component, so I had to nest one inside another, append the outer to the DOM, but reference the inner one when attaching to mount as follows:
it('test', () => {
const tooltipDiv = document.createElement('div');
const innerTooltipDiv = document.createElement('div');
tooltipDiv.setAttribute("id", "some-tooltip-id");
innerTooltipDiv.setAttribute("id", "another-tooltip-id");
tooltipDiv.appendChild(innerTooltipDiv);
document.body.appendChild(tooltipDiv);
const wrapper = mount(<Component {...props} />, { attachTo: innerTooltipDiv});
});
Most helpful comment
@yidingalan enzyme's
mounttakes a second parameter;options. Options has a property,attachTowhich 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.More information about mount and it's options: https://github.com/airbnb/enzyme/blob/master/docs/api/mount.md#mountnode-options--reactwrapper