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.
Related
Parent element (Board) creates list of children and passes them method to access this list, like this:
export default class Board extends React.Component {
constructor(props) {
super(props);
this.getList = this.getList.bind(this);
const nodes = this.props.data.map(item => (
<BoardItem key={item.id} next={item.next} accessSiblings={this.getList} />
));
this.state = {data: this.props.data, nodes: nodes}
}
getList() {
return this.state.nodes;
}
render() {
return (
<div>
{this.state.nodes}
</div>
);
} }
Then I call update() method and receive this list, filter it and correctly get the required object:
update() {
console.log(this.props.accessSiblings().find(x => x.key == this.props.next));
}
However it returns Symbol(react.element), and I am trying to get such properties as "offsetTop", "offsetHeight" of already rendered element.
Basically, I want to call an event in one element that gets some DOM properties of sibling element(e.g. offsetTop) and changes the state of this sibling.
Is this the correct approach? It feels very hacky and doesn't work at the moment.
You need to use "refs" (see also: how to access a dom element in react).
However, you should avoid working with DOM objects, if possible.
You do need a ref for accessing offsetTop etc., but apart from that you should not pass DOM or ReactElements, but you should only work with state (like "plain javascript" objects) as far as possible, and then render ReactElements (JSX, like <BoardItem ...) as the last step, and never care about DOM elements (React does it for you).
It is also usually not necessary to store ReactElements in variables or state, I suggest to try if you can focus a little bit more on state, and understand JSX more as a way to view the state.
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
}
I have a problem when I try to access parent's props value in the child class.
I am trying to initialize the child props with the parent's props.
But the following code shows me empty string of value.
export default class Child extends React.Component {
constructor(props) {
super(props);
this.state = { value: props.value };
}
...
In other class I call this Child as follows:
const issue = this.state.issue;
...
<Child value={issue.number} />
...
I checked the number value before Child class is constructed (it shows me correct values), while it is construced, the value becomes empty..
For test, I used primitive number value, and it works.
Seems like some kind of link to issue.number is disconnected when Child is contructed.
Any clue?
This could easily happen. For example, issue.number becomes a number after AJAX re-rendering. In this case you have to do follow (in your parent component):
render() {
const issue = this.state.issue
if (issue.number) {
return <Child value={issue.number} />
}
}
Ah, I just figured it out.
It is related to Lifecycle.
The number value is not loaded when I render Child.
So, I need to figure it out how to update the number in Child with my intention.
I have a third party library Im trying to use. It has a particular prop that allows you to pass in a string it uses to get a DOM element and return the bottom value.
<Sticky bottomBoundary="#some-id">
<MyChildComponentMightBeANavigationOrAnything />
</Sticky>
The component takes the id and determines the bottom value so it knows when to release itself from the sticky status. This id is basically another element in the DOM. So when that elements bottom value reaches the top of the view port the sticky component is allowed to move up as the user scrolls. The problem Im having is I need to add an offset. The Sticky component allows you to pass in a number value instead.
<Sticky bottomBoundary={1200}>
<MyChildComponentMightBeANavigationOrAnything />
</Sticky>
I need to add an offset of whatever the sticky elements height is. So lets say that "#some-id" element was 1200px and the height of the sticky element was 50, I need to to be able to get the "#some-id" and subtract 50 before passing the value into bottomBoundary={}. My calculated value would be bottomBoundary={1150}.
I tried the following. I created a component that wraps Sticky as follows:
export class WrapperSticky extends React.Component {
constructor(props) {
super(props);
this.boundary = null;
}
componentDidMount() {
const el = document.querySelector(this.props.bottomBoundary);
const rect: any = el.getBoundingClientRect();
this.boundary = rect.bottom - 50;
console.log(this.boundary);
}
render() {
return (
<Sticky innerZ={2000} bottomBoundary={this.boundary}>{this.props.children}</Sticky>
);
}
}
And I added the markup as follows:
<WrapperSticky bottomBoundary="#hero" offset={true}>
<MyChildComponentMightBeANavigationOrAnything />
</WrapperSticky >
Inside the WrapperSticky I attempted to do the calculations in the componentDidMount method and pass the results into the Sticky component. The obvious problem is the Sticky component tries to find the value before the wrapper component has completed the calculations.
Is there a way to do this elegantly. I am very new to react so any articles or documentation to learn from would be a plus.
Thanks.
You need to use the component state for this. And after calculation finished - update the state, so component re-renders with calculated values.
this.state.boundary vs this.boundary
Putting boundary value into component's state will help you by re-rendering on any of its change (i.e. setState call).
While plain class fields should be used only if a value should not affect render result.
Here is the code:
class WrapperSticky extends Component {
state = {
boundary: undefined,
}
componentDidMount() {
const el = document.querySelector(this.props.bottomBoundary)
const rect = el.getBoundingClientRect()
const boundary = rect.bottom - 50
this.setState({ boundary })
}
render() {
const { boundary } = this.state
return boundary === undefined
? null // or placeholder
: (
<Sticky innerZ={2000} bottomBoundary={boundary}>
{this.props.children}
</Sticky>
)
}
}
You could have a boolean flag in the state of WrapperSticky for whether you've done the calculation yet. It's initially false, and render returns <></>. In componentDidMount, after you do the calculation, you set the flag to true, which triggers a re-render that renders the child for real. This should at least work (I've done something similar here using the subProps state field), though there might be a better way using some of the more advanced lifecycle hooks.
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.