React - How do I give each child component a unique ref? - javascript

I have a parent component that renders one of several child components:
class Parent extends React.Component {
...
render() {
const activeChild = this.state.activeChild; // 0, 1, 2, etc.
return (
<div>
{this.props.children[activeChild]}
</div>
);
}
}
Each of these child components needs to have a ref to its own unique DOM node. In my case, I need to pass each child's node to a third-party library to draw something on it (for example: a visualization, a canvas, or a map).
The problem is that, when the child components are rendered to the same 'point' in the React tree, at different times, they all end up sharing the same ref. (I think)
Here's a JSFiddle demonstrating the problem, using Leaflet.js.
When all of the children are rendered separately, they each get their own DOM node to draw their map onto. But when they are shown one-at-a-time, only the first one is able to draw onto the ref'd DOM node.
Can I tell React to use (and display) separate nodes for each individual child's ref, even if the children are being mounted at the same point?
If not, is there a better approach to this problem?

If you mean ref the ref and not the key, try something like this:
class Parent extends React.Component {
...
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
const activeChild = this.state.activeChild; // 0, 1, 2, etc.
const ChildComponent = this.props.children[activeChild];
return (
<div>
<ChildComponent ref={this.myRef} />
</div>
);
}
}
I did not test this.

Related

React findDOMNode alternative for text nodes

I'm trying to create a text editor such as Draft.js, which entails using contentEditable. Anyways, I'm using MutationObserver to detect changes and I want to be able to reflect changes to the DOM in the state.
When a component renders to a string, findDOMNode returns a text DOM node containing that value.
I want to compare the mutation target to the rendered dom and reflect those changes in the app state. It would most likely work if I used findDOMNode, however it's deprecated. Is there an alternative? Or, is there a way to use refs to make this happen?
As an example, I have a PElement class:
class PElement extends React.Component {
constructor (props) {
super(props);
this.state = {
children = []
}
/*Parse content props, etc, for children*/
this.ref = React.createRef();
this.compare = this.compare.bind(this);
}
compare (node) {
//This will allow me to check if the target of the mutation was this component.
return this.ref.current === node;
}
render () {
return (<p ref={this.ref}>{this.state.children}</p>);
}
}
As for the Text class:
class Text extends React.Component {
constructor (props) {
super(props);
//No state. This will be lowest level and controlled.
this.ref = React.createRef();
this.compare = this.compare.bind(this);
}
compare (node) {
return this.ref.current === node;
}
render () {
//I don't know how to create a ref to the rendered text node.
return this.props.content;
}
}
}
I tried to create a Text node and use React.cloneElement, though it didn't work.
In the end, I settled on inferring the location of the Text component by mutating the state with each mutation, ie childList by adding or removing components and checking the child nodes of the parent. This way I can know which Component renders which node.
Though, on second thought, I might just prevent input and modify the state in the way which it should be, based on the captured input. It's more in line with the philosophy of React.

ref issues due to React Portals and componentDidMount

Background:
I'm trying to figure out the best way to implement a Portal component that wraps React's native portal utility. The component would simply handle creating the portal's root element, safely inserting it into the DOM, rendering any of the component's children into it, and then safely removing it again from the DOM as the component is unmounting.
The Problem:
React strongly advises against side effects (like manipulating the DOM) outside of React's safe life cycle methods (componentDidMount, componentDidUpdate, etc...) since that has the potential to cause problems (memory leaks, stale nodes, etc...). In React's examples of how to use Portals, they mount the portal's root element into the DOM tree on componentDidMount, but that seems to be causing other problems.
Issue number 1:
If the Portal component 'portals' it's children into the created root element during it's render method, but waits until it's componentDidMount method fires before appending that root element into the DOM tree, then any of the portal's children which need access to the DOM during their own componentDidMount life cycle methods will have issues, since at that point in time they will be mounted to a detached node.
This issue was later addressed in React's docs which recommend setting a 'mounted' property to true on the Portal component's state once the Portal component had finished mounting and successfully appended the portals root element to the DOM tree. Then in the render, you could hold off on rendering any of the Portal's children until that mounted property was set to true, as this would guarantee that all of those children would be rendered into the actual DOM tree before their own respective componentDidMount life cycle methods would fire off. Great. But this leads us to...
Issue number 2:
If your Portal component holds off on rendering any of it's children until after it itself has mounted, then any of the componentDidMount life cycle methods of it's ancestors will also fire off prior to any of those children being mounted. So any of the Portal component's ancestors that need access to refs on any of those children during their own componentDidMount life cycle methods will have issues. I haven't figured out a good way to get around this one yet.
Question:
Is there a clean way to safely implement a portal component so that it's children will have access to the DOM during their componentDidMount life cycle methods, while also allowing the portal component's ancestors to have access to refs on those children during their own respective componentDidMount life cycle methods?
Reference Code:
import { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
export default class Portal extends Component {
static propTypes = {
/** This component uses Portals to dynamically render it's contents into
* whatever DOM Node contains the **id** supplied by this prop
* ('portal-root' by default). If a DOM Node cannot be found with the
* specified **id** then this component will create one and append it
* to the 'Document.Body'. */
rootId: PropTypes.string
};
static defaultProps = {
rootId: 'portal-root'
};
constructor(props) {
super(props);
this.state = { mounted: false };
this.portal = document.createElement('div');
}
componentDidMount() {
this.setRoot();
this.setState({ mounted: true });
}
componentDidUpdate( prevProps, prevState ) {
if( this.props.rootId !== prevProps.rootId ) this.setRoot();
}
componentWillUnmount() {
if( this.root ) {
this.root.removeChild(this.portal);
if( !this.root.hasChildNodes() ) this.root.parentNode.removeChild(this.root);
}
}
render() {
this.portal.className = this.props.className ? `${this.props.className} Portal` : 'Portal';
return this.state.mounted && ReactDOM.createPortal(
this.props.children,
this.portal,
);
}
setRoot = () => {
this.prevRoot = this.root;
this.root = document.getElementById(this.props.rootId);
if(!this.root) {
this.root = document.createElement('main');
this.root.id = this.props.rootId;
document.body.appendChild(this.root);
}
this.root.appendChild(this.portal);
if( this.prevRoot && !this.prevRoot.hasChildNodes() ) {
this.prevRoot.parentNode.removeChild(this.prevRoot);
}
}
}
The constructor is a valid lifecycle method in which you can perform side effects. There's no reason you can't create/attach the root element in the constructor:
class Portal extends Component {
constructor(props) {
super();
const root = document.findElementById(props.rootId);
this.portal = document.createElement('div');
root.appendChild(portal);
}
componentWillUnmount() {
this.portal.parent.removeChild(this.portal);
}
render() {
ReactDOM.createPortal(this.props.children, this.portal);
}
// TODO: add your logic to support changing rootId if you *really* need it
}

Updating Parent Component state from multiple child components' componentDidMount() synchronously

Ok so this question is a bit tricky. I have been thinking about whether this is even correct concept wise, considering React is supposed to be a one-way flow of data, from parent to children, and not viceversa. But I would like to post the question anyway so I get different opinions and even possibly a way to get this to work.
In my app, I have a pretty large component that accepts forms as its children, and does some nifty React magic to pass its methods to the children so when the children elements are changed, they trigger the parent components methods that store the data in state and handles the form submissions. It works very nicely, however it is not so good at catching "defaultValues".
In a nutshell, I'm trying to trigger my parent method on the chilren's componentidMount() method, and it works, however, if there's more than one child trying to do this, the method gets called twice but it only uses the second child's dataset.
I have created a simplified version of my issue in the following code:
import React from 'react'
export class Parent extends React.Component {
constructor(props){
super(props)
this.state = {
data : {name:'james'}
}
this.updateData = this.updateData.bind(this)
}
updateData(key,data){
console.log('updating data')
this.setState({
data : {...this.state.data,[key]:data}
})
}
render(){
console.log(this.state)
return (
<div>
<Child1 updateData={this.updateData}/>
<Child2 updateData={this.updateData}/>
</div>
)
}
}
class Child1 extends React.Component {
componentDidMount(){
this.props.updateData('child1','myData')
}
render(){
return (
<div>
I am Child 1
</div>
)
}
}
class Child2 extends React.Component {
componentDidMount(){
this.props.updateData('child2','myData2')
}
render(){
return (
<div>
I am Child 2
</div>
)
}
}
This code will render 'updating data' twice on the console, but it will only update the state with the data sent in child2. Again, I can see how this may not be the best approach considering that im setting the state of a parent from its children, but it would be a good solution for setting default values on a parent component that gets reused a lot with different children.
Im all ears stack overflow
I think the problem is that setState does both updates at the same time (batches them) meaning the same initial state is used when merging both partial states. You need to use updater function as shown by kind user:
this.setState((prevState) => ({ data: { ...prevState.data, [key]: data } }));

React ref inside of a loop breaks on re render

I have a gallery component that takes in an array of components. In each of the child components I am assigning a ref. The reason for this is because within the child component there are many other children components and I am attempting to access some functions on a component that is about 5 component deep. The below code shows the initial setup:
export class Gallery extends React.Component {
render() {
const galleryItems = data.map((item, index) => {
return (
<GalleryItem
ref={React.createRef()}
/>
);
});
return (
<div >
<Gallery
items={heroGalleryItems}
/>
</div>
);
}
}
When the Gallery component renders all the refs in the array of GalleryItem component are correct. But as soon as the Gallery component re renders for any reason the refs in the GalleryItem components become null values.
I have tried several things in the children components but nothing I do fixes the issue. I believe the reason is because something is happening in the code above.
I have also tried to change up the code after reading the following:
Issue storing ref elements in loop
However its not really clear to me what the person is saying to do when I look at my own implementation.
You need to move out React.createRef() from the loop (and also render) as it is creating a new ref on every render.
Depending on your code/usage, you'd need to do this in constructor and CWRP methods (basically whenever data changes).
Then creating galleryItems would be like
...
<GalleryItem ref={item.ref} />
...

Dynamically adding children of arbitrary types to React components

I'm using react-art to make a little app where a number of different shapes can be added/removed at runtime by the user.
The way I'm imagining doing it is outlined below. Note: ES6 syntax.
Top-level canvas, with a list of objects representing the shapes and their current states (e.g. position):
class Canvas extends React.Component {
constructor(props) {
super(props); // includes list of shapes
}
render() {
return (
<Surface>
// my shapes here
</Surface>
);
}
}
A number of React components for different shapes:
class SimpleShape extends React.Component {
constructor(props) {
super(props); // includes e.g. position of shape
}
render() {
<Circle />
<Rectangle />
}
}
I'm thinking of doing something like this in the Canvas component to render the shapes:
render() {
return (
<Surface>
this.props.shapes.map(function(shape) {
return React.createElement(shape.component, shape.props);
});
</Surface>
);
}
I've been unable to find an example of a situation where the number, and type, of children for a React component is dynamic. It almost feels like I'm fighting against the React way by passing in child components as props, but I can't think of another way of doing it.
My question is, what is the idiomatic React way of doing this? Is there a better way to achieve what I'm trying to do?
I hope this isn't too much of a discussion question!
I think your way is React's way, based in this React documentation:
render: function() {
var results = this.props.results;
return (
<ol>
{results.map(function(result) {
return <li key={result.id}>{result.text}</li>;
})}
</ol>
);
}
For this you have to keyed your child elements, like vkurchatkin says:
When React reconciles the keyed children, it will ensure that any child with key will be reordered (instead of clobbered) or destroyed (instead of reused).

Categories