React Reconciliation Of Component Instances - javascript

I've been trying to understand react reconciliation and am getting really confused by some of the details of how the diffing algorithm works. So far, I understand that whenever an update is made, we create a new react element tree and compare it with our previous react element tree. The diffing algorithm manages finding the difference between the new and old react element trees. The 2 assumptions of the algo. are that elements of the same level and type don't need to be unmounted and re-mounted and that keys provide a way of identifying child elements not by index.
The part that confuses me is how comparisons are made between 2 react instances. For example, when comparing <Comp1/> in the old react element tree and <Comp2/> in the new react element tree (assume that <Comp2> replaced <Comp1> in the creation of the new tree), does the diffing algorithm simply compare both react elements' "type" attributes? So if both have the same "type", then the diffing algorithm doesn't consider un-mounting and mounting into the DOM?

does the diffing algorithm simply compare both react elements' "type"
attributes?
Yes, from the docs:
Whenever the root elements have different types, React will tear down
the old tree and build the new tree from scratch. Going from <a> to
<img>, or from <Article> to <Comment>, or from <Button> to <div> - any
of those will lead to a full rebuild.
Another your question:
So if both have the same "type", then the diffing algorithm doesn't
consider un-mounting and mounting into the DOM?
Yes, in that case React just updates the existing instance. During component update instance remains the same, and state is maintained across renders.
You can see in below example:
The first place where we rendered A, it doesn't get unmounted when we replace it also with another A (because of same type).
The second place where we used A, as soon as we replace it with B, react unmounts A.
let A = (props) => {
React.useEffect(() => {
console.log('Mounted A', props);
return () => {
console.log('Unmounted A', props);
};
}, []);
return <div>This is A: {props.tmp}</div>;
};
let B = (props) => {
React.useEffect(() => {
console.log('Mounted B', props);
return () => {
console.log('Unmounted B', props);
};
}, []);
return <div>This is B</div>;
};
function App() {
let [tmp, setTemp] = React.useState(0);
return (
<div
onClick={() => {
setTemp(tmp + 1);
}}
>
{tmp % 2 == 0 ? <A id="first A"/> : <A id="second A"/>}
{tmp % 2 == 0 ? <A id="third A"/> : <B />}
<p>Start editing to see some magic happen :)</p>
</div>
);
}
ReactDOM.render(<App />,document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Related

Is there a way to manipulate rendered text in react component children?

I am trying to write a component that highlights text inside it's children recursively.
What I have been able to achieve, is to highlight the text only if it's explicitly provided in the body component, but I can't find a way to change the text of the component's render part.
Let's say I have the following HighlightText component:
(Note, that this is a concept component. The real component is much more complicated)
const HighlightText = ({highlight, children}) => {
const regex = new RegExp(`(${regexEscape(highlight)})`, 'gi');
return React.Children.map(children, child => {
// Found a text, can highlight
if (typeof child === 'string') {
const text = child.trim();
if (text) {
return text.split(regex).filter(p => p).map((p, i) =>
regex.test(p) ? <mark key={i}>{p}</mark> : <span>{p}</span>;
);
}
}
// If child is a react component, recurse through its children to find more text to highlight
if (React.isValidElement(child)) {
if (child.props && child.props.children) {
return HighlightText({children: child.props.children, highlight});
}
}
// Here I believe, should be another handling that handles the result of the render function to search for more text to highlight...
// For any other cases, leave the child as is.
return child;
})
}
And some component that renders something:
const SomeContent = () => <div>content</div>;
Now, I want to use the HighlightText component the following way:
ReactDOM.render(
<HighlightText highlight="e">
<SomeContent />
<p>some paragraph</p>
nude text
</HighlightText>
,document.body);
The resulted DOM of the the above code is:
<div>content</div>
<p><span>som</span><mark>e</mark><span> paragraph</span></p>
<span>nud</span><mark>e</mark><span> t</span><mark>e</mark><span>xt</span>
But I expect it to be:
<div><span>cont</span><mark>e</mark><span>nt</span></div>
<p><span>som</span><mark>e</mark><span> paragraph</span></p>
<span>nud</span><mark>e</mark><span> t</span><mark>e</mark><span>xt</span>
Any suggestions on how to handle the rendered part of the child component?
Eventually I managed to solve this problem using React.Context.
Not exactly as I expected, but I think it's even a better approach, because now I can decide what text to highlight.
It's similar to i18n and themes techniques in React. React.Context is best approach for these kind of text manipulations.

Component's forwardRef children all point to the same ref in an array

I have a function component that takes a variable amount of child (forwardRef) function components. What I would like to achieve is having a ref to each of the child components for animations when a child component is clicked. I have a semi-working solution by creating an array of refs and then cloning all the children and passing an indexed ref to each of them. The only issue is that all of the refs in the index point to the same (last) child.
Here are my components:
const Navbar = ({children}) => {
const [activeLink, setActiveLink] = useState(0);
const linkRefs = Array(children.length).fill(React.createRef(null));
const handleActiveLinkChange = (index) => {
setActiveLink(index);
console.log(linkRefs[index].current);
}
return (
<nav>
{React.Children.map(children, (child, index) => React.cloneElement(child, {index: index, active: index === activeLink, handleActiveLinkChange, key: "child" + index, ref: linkRefs[index]}))}
</nav>
)
}
const Link = React.forwardRef(({children, active, index, handleActiveLinkChange}, ref) => {
return (
<a href="#" style={linkStyle} onClick={() => handleActiveLinkChange(index)} ref={ref}>
{active ? <b>{children}</b> : children}
</a>
)
});
And assuming I use the components in the following way:
<Navbar>
<Link>One</Link>
<Link>Two</Link>
<Link>Three</Link>
<Link>Four</Link>
<Link>Five</Link>
</Navbar>
I expect the refs to be:
Ref array index
Ref current
0
One
1
Two
2
Three
3
Four
4
Five
But the refs I get are:
Ref array index
Ref current
0
Five
1
Five
2
Five
3
Five
4
Five
I'm assuming it's something to do with variable scope but I just can't figure out the cause of the issue. I've tried many variations of loops and functions but I'd rather understand the cause than blindly try to find a solution.
The issue is with the following line. It creates only one ref and all the array indices refer to that single ref.
const linkRefs = Array(children.length).fill(React.createRef(null));
Instead of the above use the following line which creates new refs for each child as you expect.
const linkRefs = Array.from({ length: children.length }, () =>
React.createRef(null)
);

How do I delete dynamically rendered component that is part of a list?

I currently dynamically render the same component when clicking a button and the latest component is rendered on the top of the list.
Now, I want to delete the component. Each component has a cancel button to delete the rendered component. So I should be able to delete the component no matter where it is in the list.
Here's what I have so far:
local state:
state = {
formCount: 0
}
add and cancel:
onAddClicked = () => {
this.setState({formCount: this.state.formCount + 1});
}
onCancelButtonClicked = (cancelledFormKey: number) => {
const index = [...Array(this.state.formCount).keys()].indexOf(cancelledFormKey);
if (index > -1) {
const array = [...Array(this.state.formCount).keys()].splice(index, 1);
}
}
Parent component snippet:
{ [...Array(this.state.formCount).keys()].reverse().map(( i) =>
<Form key={i}
onCancelButtonClicked={() => this.onCancelButtonClicked(i)}
/>)
}
The only thing is I'm not sure how to keep track of which form was cancelled/deleted. I think I would need to create a new object in my local state to keep track but how do I know which index of the array I deleted as part of state? I'm not sure how do that? As I am using the count to make an array above.
Usually, this isn't how you'd generate a list of items. You're not storing the form data in the parent, and you're using index based keys which is a no-no when you're modifying the array. For example, I have an array of size 5 [0, 1, 2, 3, 4], when I remove something at position 2, the index of all the items after it changes causing their key to change as well, which will make react re-render them. Since you're not storying the data in the parent component, you will lose them.
Just to humor you, if we want to go with indexed based keys, we may have to maintain a list of removed indexes and filter them out. Something like this should do the trick:
state = {
formCount: 0,
deletedIndex: []
}
onCancelButtonClick = (cancelledIndex: number) => setState((prevState) => ({
deletedIndex: [...prevState.deletedIndex, cancelledIndex]
});
And your render would look like:
{
[...Array(this.state.formCount)].keys()].reverse().map((i) => (
if (deletedIndex.includes(i) {
return null;
} else {
<Form key={i} ... />
}
))
}
As a rule of thumb though, avoid having index based keys even if you don't care about performance. It'll lead to a lot of inconsistent behavior, and may also cause the UI and the state to be inconsistent. And if you absolutely want to for fun, make sure the components that are being rendered using index based keys have their data stored at the parent component level

React renders again the same div elements - Reconciliation question

In React documentation I was reading about reconciliation. The following interesting scenario just came to my mind.
In my examples the code uses a button element to toggle a boolean value on click event. Based on that the code decides with ternary operator which element should React render.
Let's have two components to represent my example:
const First = () => {
return <div>element - no difference</div>
}
const Second = () => {
return <div>element - no difference</div>
}
There are no difference in the rendered elements at the end.
First example
Have First and Second functional components in the first example as the following:
const YourComponent = () => {
const [renderFirst, setRenderFirst] = useState(true);
return <>
<button onClick={() => setRenderFirst(!renderFirst)}>Toggle</button>
{renderFirst ? <First /> : <Second /> }
</>
}
Second example
In the second example just using div elements but ending with the same results:
const Contact = () => {
const [renderFirst, setRenderFirst] = useState(true);
return <>
<button onClick={() => setRenderFirst(!renderFirst)}>Toggle</button>
{renderFirst ? <div>element - no difference</div> : <div>element - no difference</div> }
</>
}
Question
My understanding as per the documentation states:
Whenever the root elements have different types, React will tear down the old tree and build the new tree from scratch.
At the end in either way the rendered result will end up <div>element - no difference</div>. The Second example is not rendering again the DOM element obviously.
Why is React rendering the First example then? Are those considered different types in this case?
Thank you!
By rendering I assume you mean changes to be "rendered" aka committed to the DOM. The reconciliation process will still be triggered in both examples.
The answer is simple. In your first example you are returning a React component (<First /> or <Second />) whereas in the second example you are returning a React element (one of the two div's).
React cannot know beforehand what each of your two components will do (they could have their own logic), so in the latter case, React will just see that you want to replace First with Second and just re-render. In the former case, you are only returning elements which can be objectively compared.
In addition to #Chris answers, I made a small test approving the answer.
My main consideration was if JSX will generate a new instance although the components may unmounted due to the condition.
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
// You can check either
const Component = <div>element - no difference</div>;
const Contact = () => {
const [renderFirst, setRenderFirst] = useState(true);
const componentRef = useRef();
const first = useRef();
useEffect(() => {
console.log(componentRef.current);
const [child] = componentRef.current.children;
if (!first.current) {
first.current = child;
}
}, []);
useEffect(() => {
const [child] = componentRef.current.children;
console.log(child !== first.current ? 'different' : 'same');
});
return (
<>
<button onClick={() => setRenderFirst(prev => !prev)}>Toggle</button>
<div ref={componentRef}>
{renderFirst ? (
<div>element - no difference</div>
) : (
<div>element - no difference</div>
)}
{/* {renderFirst ? Component : Component} */}
</div>
</>
);
};
ReactDOM.render(<Contact />, document.getElementById('root'));

What are the benefits of immutability?

I'm using React to render long scrollable list of items (+1000). I found React Virtualized to help me with this.
So looking at the example here I should pass down the list as a prop to my item list component. What's tripping me up is that in the example the list is immutable (using Immutable.js) which I guess makes sense since that's how the props are supposed to work - but if I want to make a change to a row item I cannot change its state since the row will be rerendered using the list, thus throwing out the state.
What I'm trying to do is to highlight a row when I click it and have it still be highlighted if I scroll out of the view and back into it again. Now if the list is not immutable I can change the object representing the row and the highlighted row will stay highlighted, but I'm not sure that's the correct way to do it. Is there a solution to this other than mutating the props?
class ItemsList extends React.Component {
(...)
render() {
(...)
return(
<div>
<VirtualScroll
ref='VirtualScroll'
className={styles.VirtualScroll}
height={virtualScrollHeight}
overscanRowCount={overscanRowCount}
noRowsRenderer={this._noRowsRenderer}
rowCount={rowCount}
rowHeight={useDynamicRowHeight ? this._getRowHeight : virtualScrollRowHeight}
rowRenderer={this._rowRenderer}
scrollToIndex={scrollToIndex}
width={300}
/>
</div>
)
}
_rowRenderer ({ index }) {
const { list } = this.props;
const row = list[index];
return (
<Row index={index} />
)
}
}
class Row extends React.Component {
constructor(props) {
super(props);
this.state = {
highlighted: false
};
}
handleClick() {
this.setState({ highlighted: true });
list[this.props.index].color = 'yellow';
}
render() {
let color = list[this.props.index].color;
return (
<div
key={this.props.index}
style={{ height: 20, backgroundColor: color }}
onClick={this.handleClick.bind(this)}
>
This is row {this.props.index}
</div>
)
}
}
const list = [array of 1000+ objects];
ReactDOM.render(
<ItemsList
list={list}
/>,
document.getElementById('app')
);
If you only render let's say 10 out of your list of a 1000 at a time, then the only way to remember highlighted-flag, is to store it in the parent state, which is the list of 1000.
Without immutability, this would be something like:
// make a copy of the list - NB: this will not copy objects in the list
var list = this.state.list.slice();
// so when changing object, you are directly mutating state
list[itemToChange].highlighted = true;
// setting state will trigger re-render
this.setState({ list: list });
// but the damage is already done:
// e.g. shouldComponentUpdate lifecycle method will fail
// will always return false, even if state did change.
With immutability, you would be doing something quite similar:
// make a copy of the list
var list = this.state.list.slice();
// make a copy of the object to update
var newObject = Object.assign({}, list[itemToChange]);
// update the object copy
newObject.highlighted = true;
// insert the new object into list copy
list[itemToChange] = newObject;
// update state with the new list
this.setState({ list : list );
The above only works if the object does not contain more nested objects.
I am not familiar with immutable.js, but I'm sure they have excellent methods to deal with this more appropriately.
The argument for immutability in react is that you can reliably and transparently work with state changes (also react's life-cycle methods expect them). There are numerous questions on SO with a variant of "why is nextState == this.state", with answers coming down to "not keeping state and props immutable screwed things up"

Categories