Get Root DOM element from a child component - reactjs

I have a loop that reads multiple elements from the document and I render it with ReactDOM.render, and a component very low in the component tree, creates a custom event, to that event I would like to pass the element that was rendered in the DOM (i.e. Root Element), I have to go passing from the top the element through Props, or React provides some API that can tell me which Root element we are?
Rather, in the child component, I would like to make: rootElement.dispatchEvent(myCustomEvent);
What options do I have to do this?

The root node looks to be given a property that starts with __reactContainer, so you can search through parent elements until you find an element with such a property.
const Child = () => <div><span onClick={(e) => {
let element = e.target;
while (element) {
element = element.parentElement;
if (Object.keys(element).some(key => key.includes('reactContainer'))) {
console.log('Found', element);
break;
}
}
}}>click</span></div>;
const App = () => {
return <section><Child /></section>
};
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>
That's almost certainly not part of the deliberate outward-facing design, though. A better way would be to use useContext to save the root element at the top component via a ref, and to consume it in the descendant component.
const Child = () => {
const { ref } = React.useContext(Context);
return (
<div>
<span
onClick={() => { console.log(ref.current.parentElement); }}
>click</span>
</div>
);
};
const Context = React.createContext();
const App = () => {
const ref = React.useRef();
return (
<Context.Provider value={{ref}}>
<section ref={ref}><Child /></section>
</Context.Provider>
);
};
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>

Related

How to implement a React equivalent of Vue's IS attribute?

I want to be able to implement:
<div is='a' href='https://stackoverflow.com'>go to Stack Overflow</div>
To render into:
<a href='https://stackoverflow.com'>go to Stack Overflow</a>
What would be the most sensible approach to implement it?
There are a number of ways to do that, but here's one of them:
const CustomComponent = (props) => {
const {is, children, ...rest} = props
return React.createElement(is, {...rest}, children)
}
const App = () => {
return (
<CustomComponent
is={'a'}
href={'https://stackoverflow.com'}
>
go to Stack Overflow
</CustomComponent>
)
}
ReactDOM.render(
<App /> ,
document.getElementById('root')
);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
More on React.createElement() here.

React container props child passing

afraid that question is simple stupid but im stuck(
I have a Box component with some nested ones:
<Box.Content anyProp="prop">
<Box.Text...
<Box.Links
How I can pass ALL the props form Box.Content to ALL child containers (Box.Text, Box.Links, anything else)?
Trying with no luck:
BoxComponent.Content = (...props) => (
<BoxContent {...props}>{props.children}</BoxContent>
);
Then trying to catch any prop from Parent inside child container - no props listed(
You can use React.cloneElement to dynamically set a parent's props on its children.
Run the following code snippet. prop1 and prop2 on the Parent component are passed to the Child1 and Child2 components.
const Child1 = (props) => <p>Child 1: {JSON.stringify(props)}</p>;
const Child2 = (props) => <p>Child 2: {JSON.stringify(props)}</p>;
const Parent = ({ children, ...props }) => {
children = children.map(v => React.cloneElement(v, props));
return <div id="parent">{children}</div>;
};
const App = () => (
<Parent prop1="foo" prop2="bar">
<Child1 />
<Child2 />
</Parent>
);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>

How to test interaction with Ant Design's Popover content?

I have a React Component that wraps an Ant Design Popover. This component gets a callback that is being called by user interaction (say click) in dynamically generated content. Something similar to this:
const { Popover, Button } = antd;
const PopoverExtended = ({ onWhatever, children }) => {
const handleClick = (event) => {
if (event.target.className === 'some-class') {
onWhatever(event.target.dataset.value);
}
};
const dynamic = () => '<span class="some-class" data-value="42">Click this text</span>';
const content = () => {
return (
<div>
<p>Some HTML</p>
<div dangerouslySetInnerHTML={{ __html: dynamic() }} onClick={handleClick}></div>
</div>
);
};
return (
<Popover content={content()} placement="right" trigger="click">
{children}
</Popover>
);
};
ReactDOM.render(
<PopoverExtended onWhatever={(x) => console.log(x)}>
<Button>Click me</Button>
</PopoverExtended>,
document.getElementById('root')
);
<link href="https://cdnjs.cloudflare.com/ajax/libs/antd/3.26.20/antd.css" rel="stylesheet"/>
<div id="root" style="margin: 2em 0 0 2em"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/3.26.20/antd-with-locales.js" crossorigin="anonymous"></script>
Everything works as expected, but using Jest and Enzyme I'm trying to test that the onWhatever callback is being called, but so far I haven't been able to target the dynamic content neither as a ShallowWrapper nor ReactWrapper. I've tried:
describe(`<PopoverExtended /> interaction`, () => {
const mockChildren = <Button>Mock me</Button>;
const mockCallback = jest.fn();
const wrapper = mount(<PopoverExtended onWhatever={mockCallback}>{mockChildren}</PopoverExtended>);
// Try 1
const trigger = wrapper.find('.some-class[data-value="42"]'); // Nothing found.
// Try 2
const content = mount(<>{wrapper.find(Popover).prop('content')}</>);
console.log(content.html()); // Is apparently the correct Popover content HTML
const trigger = wrapper.find('.some-class[data-value="42"]'); // Nothing found.
// Try 3
const content = mount(<>{wrapper.find(Popover).prop('content')}</>);
const rendered = content.render();
const trigger = wrapper.find('.some-class[data-value="42"]'); // Node found, but
// it's a CheerioWrapper, so I cannot call trigger.simulate('click');
});
Any ideas on how to properly test that the callback is being called?
Enzyme does not see the dynamic content and hence there's no way for you to simulate click on the elements inside of dynamic content. You can verify this by doing console.log(wrapper.debug()) which will show you what Enzyme sees. After trying:
const mockCallback = jest.fn();
const wrapper = mount(<PopoverExtended onWhatever={mockCallback}>{<Button>Mock me</Button>}</PopoverExtended>);
const trigger = wrapper.find("button");
trigger.simulate("click");
Enzyme only goes as far as the hosting div:
...
<Content trigger={{...}} prefixCls="ant-popover" id={[undefined]} overlay={{...}}>
<div className="ant-popover-inner" id={[undefined]} role="tooltip">
<div>
<div className="ant-popover-inner-content">
<div>
<p>
Some HTML
</p>
<div dangerouslySetInnerHTML={{...}} onClick={[Function: handleClick]} />
</div>
</div>
</div>
</div>
</Content>
...
Now calling wrapper.html() actually returns the full DOM including the dynamic content, but that's pretty useless for our case as you've mentioned. On defense of Enzyme, the dynamic content uses html flavor instead of JSX (class instead of className) making it even more difficult to wrap.
With that out of the way, I don't see why you need to include the dynamic content in your test scenario. Simply simulate the click on the host div. In fact I'd argue that's the right way of doing it since you define the event handler on the host div and not inside the dynamic content:
it(`<PopoverExtended /> interaction`, () => {
const mockCallback = jest.fn();
const wrapper = mount(<PopoverExtended onWhatever={mockCallback}>{<Button>Mock me</Button>}</PopoverExtended>);
const mockEvent = {
type: "click",
target: {
dataset: { value: 42 },
className: "some-class"
}
};
const trigger = wrapper.find("button");
trigger.simulate("click");
const hostDiv = wrapper.find("div.trigger-wrapper");
hostDiv.simulate("click", mockEvent);
expect(mockCallback.mock.calls.length).toBe(1);
expect(mockCallback.mock.calls[0][0]).toBe(42);
});
Option 2
Interestingly the react testing library has no problem including your dynamic content so you may want to use it instead of Enzyme:
import React from "react";
import { render, fireEvent, screen } from '#testing-library/react'
import { Button } from "antd";
import PopoverExtended from "./PopOverExtended";
it(`<PopoverExtended /> interaction`, async () => {
const mockCallback = jest.fn();
render(<PopoverExtended onWhatever={mockCallback}>{<Button>Mock me</Button>}</PopoverExtended>);
fireEvent.click(screen.getByText('Mock me'))
fireEvent.click(screen.getByText('Click this text'))
expect(mockCallback.mock.calls.length).toBe(1);
expect(mockCallback.mock.calls[0][0]).toBe("42");
});
Argument for this is that the dataset is defined by the dynamic content and so you have to consider it in your test.

Basics implementation example of useState Hooks in ReactJS

We know that the useState hook is used for managing state within functional components in ReactJs.
So, for learning purposes I was implementing the example of the useState hook (snippet is given below) wherein I have taken an array with some initial value and I need to update the array and display the whole updated array in the browser whenever I clicked on the button. I tried with the below snippet but didn't get the expected result.
Problem: When I click the button for first time it will add the new element in the array but after clicking the button for two or more times it only overrides the last element.
Expected Result: New elements should be added in the array rather than overriding the last element in the array.
I definitely missing any logic or any important concept of useState hook here in this example. Please help me to understand more on react Hooks.
const {useState} = React;
const Example = () => {
const array = [1,2,3] ;
const [newArray,setNewArray] = useState(array);
const [newElement,setElement]= useState(array[array.length-1]);
const handleBoth = () => {
setElement(prev => prev + 1);
setNewArray([...array,newElement]);
}
const mapping = newArray.map(element => <li> No. {element}</li>)
return (
<div>
<ul>
{mapping}
</ul>
<button onClick={handleBoth}>Add</button>
</div>
);
};
ReactDOM.render(
<Example />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Use the state newArray not the array for the map implementation. array will reinitialize to [1,2,3] on every render. In fact, you should just move the array constant outside of the component
const {useState} = React;
const array = [1,2,3];
const Example = () => {
const [newArray,setNewArray] = useState(array);
const [newElement,setElement]= useState(array[array.length-1]+1);
const handleBoth = () => {
setElement(prev => prev + 1);
setNewArray([...newArray,newElement]);
}
const mapping = newArray.map(element => <li> No. {element}</li>)
return (
<div>
<ul>
{mapping}
</ul>
<button onClick={handleBoth}>Add</button>
</div>
);
};
ReactDOM.render(
<Example />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Calling ReactDOM.render more than once

Why does the following only render a single button?
const b = <button>this is a button</button>;
ReactDOM.render(b,mountNode)
ReactDOM.render(b,mountNode)
ReactDOM.render(b,mountNode)
If mountNode is a reference to a DOM element, calling ReactDOM.render(b, mountNode) means that React will insert your React Component as the innerHTML to that node.
Calling it several times effectively means that you just keep replacing the previously mounted node.
If you want 3 buttons, try creating a component that wraps them. For example:
var mountNode = document.getElementById("app");
const b = <button>this is a button</button>;
const myApp = <div>{b}{b}{b}</div>;
ReactDOM.render(myApp, mountNode);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
<div id="app"></div>
Alternatively:
var mountNode = document.getElementById("app");
const B = () => <button>this is a button</button>;
const MyApp = () => <div><B /><B /><B /></div>;
ReactDOM.render(<MyApp />, mountNode);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
In react it creates virtual DOM. Every time render method is called the previous DOM is replaced by new created DOM. It only looks for difference between previous DOM and new DOM. That's why it renders single button.

Resources