Does React need to Re-Render Textbox at its OnChange Event - reactjs

I've been working on a React project recently. I've gotten to the point where I know that when typing in a textbox React updates its UI state and re-renders the textbox and brings focus back to it. The whole process happens so fast that user doesn't notice that textbox has been re-rendered while typing.
Now here my question is that why doesn't React just update UI state of textbox at onChange event and doesn't re-render the textbox and hence doesn't require to put focus on it.
Why isn't the re-render process saved in this, or other similar, cases. What were we missing if we dont re-render?

React doesn't re-render elements in the sense that it creates a new one which replaces the old. In your example it simply updates the value.
React will only ever remove an element if it was replaced with one of another type. If it's of the same type, it will simply update the missing or altered attributes. You should have a read about Reacts Reconciliation on the official docs.
Elements Of Different Types
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.
DOM Elements Of The Same Type
When comparing two React DOM elements of the same type, React looks at the attributes of both, keeps the same underlying DOM node, and only updates the changed attributes.
Demo
I have prepared a demo below. It's a combination of React with jQuery just to demonstrate this.
When you type some text into the textarea, the state is updated and the element value is updated to show what you typed. This is done with React.
The button is outside the React scope completely. There is an eventlistener written with jQuery which randomly alters the elements css background color.
If the component was replaced each time React updated the state value of the component (i.e when we type inside the textarea), the background color would be lost since we would be talking about a newly created DOM element.
However, as you can see, this is not the case -- the background stays.
class Greeting extends React.Component {
constructor() {
super();
this.state = {txt: ""};
}
updateTextarea = (e) => {
this.setState({txt: e.target.value});
}
render() {
return <textarea onChange={this.updateTextarea}>{this.state.txt}</textarea>;
}
}
ReactDOM.render(<Greeting />, document.getElementById('app'));
$("#btn").on("click", function() {
var colors = ["red", "green", "lightblue", "grey", "pink", "orange"];
var rand = Math.floor(Math.random() * 6);
$("#app textarea").css("background", colors[rand]);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<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>
<button id="btn">Randomize color!</button>

Related

Is the rule of ReactJS re-rendering just: if the state of component changes, then the whole sub-tree is re-rendered?

First of all, by "re-render", here it means either
the render() method of any class component is called, OR
the function of the function component is called.
Let's called the element in the actual DOM changing a "refresh", to distinguish it from "re-render".
Is the rule of "re-render" as simple as:
When any state of a component is changed, then the component and all the subtree down from this component is re-rendered
and that's it? For example:
function A() {
console.log("Component A re-render");
return <div>Component A says Hello World</div>;
}
function App() {
const [counter, setCounter] = React.useState(0);
console.log("Component App re-render");
function increaseCount() {
setCounter(c => c + 1);
}
return (
<div>
{counter}
<button onClick={increaseCount} >Increment</button>
<A />
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react#16.12.0/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16.12.0/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Component A is so simple: it doesn't even take any props and is just outputting static text, yet it is still called every time (by looking at the console.log() output).
But even though it is "re-rendered", the actual DOM element is not "refreshed", as seen in Google Chrome's Inspect Element that the DOM element is not flashing for Component A, but is only flashing for the counter number.
So is this how it works?
Whenever any state of a component is changed, that component and the whole subtree will be "re-rendered".
But ReactJS will "reconcile" the content of the "Virtual DOM" that it built using those JSX with the content of the actual DOM, and if the content is different, "refresh the actual DOM".
But having said that, it seems ReactJS doesn't actually reconcile with the actual DOM, but reconcile with probably the "previous virtual DOM". Why? Because if I use a setTimeout() to change the actual DOM of Component A to some other content after 3 seconds, and click the button, ReactJS doesn't change the content of Component A back to "Hello World". Example:
function A() {
console.log("Component A re-render");
return <div id="foo">Component A says Hello World</div>;
}
function App() {
const [counter, setCounter] = React.useState(0);
console.log("Component App re-render");
function increaseCount() {
setCounter(c => c + 1);
}
return (
<div>
{counter}
<button onClick={increaseCount} >Increment</button>
<A />
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#root"));
setTimeout(function() {
document.querySelector("#foo").innerText = "hi"
}, 3000);
<script src="https://unpkg.com/react#16.12.0/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16.12.0/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
After rendering react gets a json of how the view should look like, calculates the changes and then changes the actual dom. So even though A is rerendered the output json will be same as virtual dom's and hence react does not touch the dom
This is to answer the WHY part of your question -
As explained in this link - https://gist.github.com/paulirish/5d52fb081b3570c81e3a, many things can trigger reflow of the DOM. Not just setting, even accessing the DOM element properties can cause reflow of DOM which is a very costly process.
So, if React starts comparing against the actual DOM, then it will bring down the performance of rendering rather than improving it. This is why react compares changes with previous copy and update the changes to the Actual DOM if required.
After reading the React docs on Reconciliation, it seems a simplified way to think about it is:
Whenever the props provided to the component COMPO1 or the state of this component changes, then all the render() of class components and the function components under COMPO1 are called, to form a virtual DOM tree, and the whole subtree is compared to the previous subtree -- not to the actual DOM but to a previous virtual DOM subtree
It is compared to see if the nodes are different and recursively do that for all children.
Only the minimum subtrees from the component and down that are different would cause a refresh of content to the actual DOM -- meaning if node A has children B and C, and B stayed the same while node C became different, then only C will cause that part of the actual DOM to be refreshed. (of course if node C has node D and E and D stayed the same while E became different, then only E is refreshed to actual DOM).

React - How to notify popper to reposition my popover whenever react updates any DOM element

Relevant versions: React 16.4.2, Bootstrap 4.1.3, popper.js 1.14.4, Typescript 3.0.3
I use the Bootstrap Popover functionality in my react app.
The Popover works well if the rest of the page is static. When the page is changed (at the browser level), the Popover gets repositioned very quickly and smoothly so it stays visible while the content it's anchored to is visible:
when scrolling if it bumps up against the windows edges
if the screen is rotated on a phone
if the window is resized
This all works well because popper.js is apparently watching the window.scroll and window.resize events, as per this answer: Bootstrap 4 - how does automatic Popover re-positioning work?
The problem comes when my react application starts showing/hiding DOM elements. Because popper.js doesn't know about react, it doesn't know the DOM changed, so it doesn't know that the Popovers might need to be repositioned.
I know calling popover("update") on each Popover anchor works, because I've added code like this to do it intermittently:
window.setInterval(()=> $(this.selfRef).popover("update"), 100);
But that's yucky and wasteful, and a little janky.
Is there a way to have react tell me when it updates any node in the DOM, so I can then tell popper.js to update the position of the popovers?
Note that the react component that causes the DOM change isn't necessarily located near the component that uses the Popover. It could be something in a completely separate part of the hierarchy that happens to be displayed before the component with the popover - so the I don't think the solution is componentWillReceiveProps() or methods like that on the Popover component, because it's probably not the component that's causing the movement.
Note that I'm aware of projects like react-bootstrap, reactstrap or react-popper - but I don't want to use them.
EDIT: it seems like MutationObserver might be a non-react way to do this. I just figured since React is already doing all that reconciliation work, maybe there's a way to get it to notify me when it actually does edit the DOM.
"The react Component that causes the DOM change isn't necessarily
located near the Component that uses the Popover. It could be
something in a completely separate part of the hierarchy"
If both the Component that changes the DOM, and the Component that creates the Popover are in the same parent, you could share a method in the parent that does the .popover('update'). The Component that changes the DOM would need to trigger this event, but it doesn't need to be specifically "aware" of the Popover Component. The Popover Component doesn't need to be aware of the DOM changing Component.
class ChangeDom extends React.Component {
constructor(props) {
super(props);
this.changeDom = this.changeDom.bind(this);
}
changeDom () {
this.props.domChanged();
}
render() {
return (
<div>
<button className="ml-2 btn btn-primary" onClick={this.changeDom}>Change Dom
</button>
</div>)
}
}
class Pop extends React.Component {
constructor(props) {
super(props);
this.togglePopover = this.togglePopover.bind(this);
}
togglePopover() {
$('[data-toggle="popover"]').popover('toggle');
}
render() {
return (
<div class="position-relative">
<button className="mt-4 btn btn-primary" onClick={this.togglePopover} data-toggle="popover"
</button>
</div>)
}
}
class Parent extends React.Component {
domChanged(){
$('[data-toggle="popover"]').popover("update");
}
render() {
return (
<div>
<ChangeDom domChanged={this.domChanged} />
<Pop />
</div>)
}
}
Demo: https://www.codeply.com/go/NhcfE8eAEY
This is my current attempt at a MutationObserver based solution.
UserApp is a component placed toward the top of the application hierarchy.
The Popover class is (over) used in various places in my application for a bunch of stuff.
The possibility of infinite recursion caused by firing popover("update") from a MutationObserver event makes me wary of using this solution long term.
It seems to do the job for now, but this is one of the things uni-directional binding is meant to avoid.
On the plus side, this works even when you have non-react components in your application (like for example, the Bootstrap navbar).
export class UserApp extends React.Component<any, AppState> {
public domChangeObservers = $.Callbacks();
public mutationObserver = new MutationObserver(
(mutations: MutationRecord[])=>{
// premature optimisation?
// I figure I don't care about each individual change, if the browser
// batched em up, just fire on the last one.
// But is this a good idea given we have to inspect the mutation in order
// to avoid recursive loops?
this.domChangeObservers.fire(mutations[mutations.length-1]);
}
);
constructor(props: any) {
super(props);
this.mutationObserver.observe(document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
}
componentWillUnmount(){
this.mutationObserver.disconnect();
}
...
}
const DefaultTrigger = "click";
export interface PopoverProps{
popoverTitle: string | Element | Function;
popoverContent: string | Element | Function;
/** Set to "focus" to get "dismiss on next click anywhere" behaviour */
popoverTrigger?: string;
/** Leaving it empty means that the popover gets created
* as a child of the anchor (whatever you use as the child of the popover).
* Setting this to "body" means the popover gets created out on the body
* of the document.
* "body" can help with stuff like when the popover ends up
* being clipped or "under" other components (because of stuff like
* `overflow:hidden`).
*/
container?: string;
allowDefaultClickHandling?: boolean;
ignoreDomChanges?: boolean;
id?: string;
}
export class Popover
extends PureComponent<PopoverProps, object> {
// ! to hack around TS 2.6 "strictPropertyInitialization"
// figure out the right way... one day
selfRef!: HTMLSpanElement;
onDomChange = (mutation:MutationRecord)=>{
/*
- popover("update") causes DOM changes which fire this handler again,
so we need to guard against infinite recursion of DOM change events.
- popover("update") is async, so we can't just use an "if not currently
handling a mutation" flag, because the order of events ends up being:
onDomChange() -> flag=true -> popover("update") -> flag=false ->
popper.js changes DOM -> onDomChange() called again -> repeat forever
- Can't just detect *this* popover. If DOM event occurs because popovers
overlay each other they will recurse alternately - i.e. pop1 update
call makes DOM changes for pop2, pop2 update makes changes for pop1,
repeat forever.
*/
if( Popover.isPopoverNode(mutation) ){
return;
}
/*
- tell popper.js to reposition the popover
- probably not necessary if popover is not showing, but I duuno how to tell
*/
$(this.selfRef).popover("update");
};
private static isPopoverNode(mutation: MutationRecord){
/*
Had a good attempt that used the structure of the mutation target to
see if it's parent element was defined as `data-toggle="popover"`; but
that fails when you set the `container` prop to some other element -
especially, "body", see the comments on the Props .
*/
if( mutation.target.nodeType != 1 ){
return false;
}
// Is Element
let element = mutation.target as Element;
/*
Is the mutation target a popover element?
As defined by its use of the Bootstrap "popover" class.
This is dodgy, it relies on Bootstrap always creating a container
element that has the "popover" class assigned.
BS could change their classname, or they could
change how they structure their popover, or some other
random widget could use the name.
Actually, this can be controlled by overriding the popover template,
which I will do... later.
*/
let isPopoverNode = element.classList.contains("popover");
// very helpful when debugging - easy to tell if recursion is happening
// by looking at the log
// console.log("target", isPopoverNode, mutation, mutation.target );
return isPopoverNode;
}
componentDidMount(): void{
// the popover() method is a "JQuery plugin" thing,
// that's how Bootstrap does its stuff
$(this.selfRef).popover({
container: this.props.container || this.selfRef,
placement: "auto",
title: this.props.popoverTitle,
content: this.props.popoverContent,
trigger: this.props.popoverTrigger || DefaultTrigger,
});
if( !this.props.ignoreDomChanges ){
UserApp.instance.domChangeObservers.add(this.onDomChange);
}
}
componentWillUnmount(): void {
if( !this.props.ignoreDomChanges ){
UserApp.instance.domChangeObservers.remove(this.onDomChange);
}
// - without this, if this component or any parent is unmounted,
// popper.js doesn't know that and the popover content just becomes
// orphaned
$(this.selfRef).popover("dispose");
}
stopClick = (e: SyntheticEvent<any>) =>{
if( !this.props.allowDefaultClickHandling ){
// without this, if the child element is an <a> or similar, clicking it
// to show/dismiss the popup will scroll the content
e.preventDefault();
e.stopPropagation();
}
};
render(){
let popoverTrigger = this.props.popoverTrigger || DefaultTrigger;
// tabIndex is necessary when using "trigger=focus" to get
// "dismiss on next click" behaviour.
let tabIndex = popoverTrigger.indexOf("focus")>=0?0:undefined;
return <span id={this.props.id}
tabIndex={tabIndex}
ref={(ref)=>{if(ref) this.selfRef = ref}}
data-toggle="popover"
onClick={this.stopClick}
>{this.props.children}</span>;
}
}

React: empty placeholder div gets re-rendered and thereby removes children (asynchr attached chart)

I am working on a widget dashboard which has DC.JS charts as content for each widget.
Widgets are created/ removed using react-grid-layout which creates an empty placeholder node like this:
<div id={"content_" + this.props.id} className="widgetContent"> /* chart is later drawn here */ </div>)
DC.JS later selects the div by Id and attaches its SVG chart as a child.
The problem is that for some events (like toggling static or changing Ids of the widgets), react re-renders the widgets and thereby "overwrites" the existing charts (children) with a brand new empty placeholder div as above.
My question is if that issue can be solved by React-techniques (can I prevent a div from ever being re-rendered?) or if this is an issue with the library itself.
Very similar code can be found here. The code in action is here. Imagine the snippet line above (the empty chart placeholder where a chart is attached later) in line 44.
The common solution here is to wrap this chart in a component where shouldComponentUpdate is set to false. That way react will never alter the element which your charting library modifies. An example wrapper component can be found here (including below)
var React = require('react/addons');
var ReactIgnore = {
displayName: 'ReactIgnore',
shouldComponentUpdate (){
return false;
},
render (){
return React.Children.only(this.props.children);
}
};
module.exports = {
Class: ReactIgnore,
Component: React.createClass(ReactIgnore)
};

Appending value using React.js

I made todo app using jquery and now using react making same app. I made general layout of the app using react but facing problem in appending element by clicking button. The code i made in jquery was.
function entervalue()
{
var text = $('#inputext').val();
$("#list").append('<li class="item"> <input type="checkbox" class="option-input checkbox"> ' + text + '<button class="destroy"></button><li>');
$("#inputext").val('');
}
Here the value of input field is stored in var text.
List is appended in having checkbox, 'text' and a button.
I want same code to be written in react, so that when i click on button the following list gets appended.
Use state to store the new value. Save it as an array. React does a re-render when the state changes. During that render, map() through the value in the state and generate the HTML out of it.
this.setState({ //put this inside the 'onChange' handler of <input/>
value: e.target.value
})
This will set the new value to the state.
var todo = this.state.value.map(function(value){
return <li class="item"> <input type="checkbox" class="option-input checkbox">{value}<button class="destroy"></button><li>
})
todo will have the latest list of user inputs.
[UPDATE]
Check the example https://jsfiddle.net/Pranesh456/1veb7bdg/1/
jQuery is fundamentally different than React... you need to think of how you store data in state.. not actual DOM elements (jQuery like). You'll want to define your DOM elements in your render method and have it iterate through your state to generate the elements.
I made this repo to help teach a developer the transition from jQuery to React.. it might be helpful to checkout: https://github.com/bradbumbalough/react-tunes.
I also highly reccomend this doc if you're new to React: Thinking in React.

Manipulating DOM elements within a React component

My code fetches a blob of HTML and renders it on the page. When the user selects some text within this blob, I want the selected text to be wrapped in its own span. (This is a "highlighting" feature similar Google Docs' comments system.)
If I were doing this with plain Javascript, I'd mutate the DOM on my own. But I'm not sure how to do this safely in React or where in the component lifetime I'd be able to do so.
Ideally, I could directly manipulate the Element corresponding to my HTML blob and use that directly within render(), but I don't know if this would play well with React's bookkeeping.
How can I do this in React without shooting myself in the foot?
EDIT:
Per request, some sample code. Let's say that our component receives these props:
{
id: 1337,
content: "<p>This is a paragraph with some <strong>markup</strong></p>",
highlights: [],
}
And renders something accordingly:
const Widget = React.createClass({
render() {
return <div dangerouslySetInnerHTML={{__html: this.props.content}} />
},
});
Now the component is updated with highlights: [{ start: 5, end: 10 }]. I want to have chars 5 through 10 wrapped in some <span>.
Is the right way to do this to just parse this.props.content as an Element, add the <span> in the right place, and dangerouslySetInnerHTML at the end?
Maybe you can store your <p> element with the text you want to highlight in your state. Like
this.state = {
willHighlight: <p>This is a <span>paragraph</span> with some <strong>markup</strong></p>
}
And then you can conditionaly render it. I think this is a better approach

Resources