React re-render via setState vs direct DOM manipulation? - javascript

I have been working with react for about 6 months now and something that always used to bother me is the way re-renders work.
Below is a traditional component that has one input box and sends data to the server to whose value is used by some other forms along with multiple almost static HTML elements that are never used or change very rarely. I am saying very rarely because static elements can be built and stored in a variable in the componentWillMount() method. But for this question to be a little more than that, render should contain a call to buildComplexHTMLFromData method.
buildComplexHTMLFromData = (data) => {
// Lot of javascript to build the boxes based on the initial or data
// that changes so rarely
// array.map.filter.find etc.
return (
<div>
//Complex HTML element 1
//Complex HTML element 2
//....
//....
//Complex HTML element n
</div>
)
}
sendDataToBackend = (event) => {
this.setState(
{ value: event.target.value },
() => this.functionThatSendsDataToBackend()
)
}
render() {
<div>
// Active input that sends data to the backend
<input
value={this.state.value}
onChange={this.sendDataToBackend}
/>
{this.buildComplexHTMLFromData()}
</div>
}
Now setting state upon input box change will trigger even the buildComplexHTMLFromData method that does complex javascript all over again. I heard React does something smart by diffing across DOM to efficiently re-render but this javascript is executed anyway.
On the other hand the same functionality can be achieved using two varieties of sendDataToBackend method as shown in the snippet below. This however ensures that only the target input element is changed without touching the already rendered elements or executing any javascript on buildComplexHTMLFromData method.
buildComplexHTMLFromData = (data) => {
// Lot of javascript to build the boxes based on the initial or data
// that changes so rarely
// array.map.filter.find etc.
return (
<div>
//Complex input box 1
//Complex input box 2
//....
//....
//Complex input box n
</div>
)
}
sendDataToBackend = (event) => {
//First strategy
var element = document.getElementById("persistable-input");
element && element.value = event.target.value
//Second strategy
this.persistableInput.value = event.target.value
}
render() {
<div>
// Active input that sends data to the backend or for other forms
<input
id="persistable-input"
ref={(elem) => { this.persistableInput = elem }}
value={this.state.value}
onChange={this.props.persistedValue}
/>
{this.buildComplexHTMLFromData()}
</div>
}
I don't know if I am missing something or if this is very minimal on performance but I feel it could be quite taxing for complex components. I looked multiple articles on React's reconciliation paradigm but it does not seem to address this.
I would really appreciate if anyone could shed some light into this area of React because I am looking for some cool tips and inputs on performant reconciliation in React in most cases.
Thanks in advance.

This is exactly what the shouldComponentUpdate lifecycle hook was created for. If you know that your component shouldn't always re-render, then you can add this lifecycle hook to detect which piece of state is changing. If it something that you don't care about, you can return false and the buildComplexHTMLFromData function won't ever run.
EDIT:
They also expose a base class called PureComponent that handles shouldComponentUpdate under the hood for you.

Related

Call function only after multiple states have completed updating

Logic:
I have a dialog for converting units. It has two stages of choice for the user: units to convert from and units to convert to. I keep this stage as a state, dialogStage, for maintainability as I'm likely going to need to reference what stage the dialog is in for more features in the future. Right now it's being used to determine what action to take based on what unit is clicked.
I also have a state, dialogUnits, that causes the component to rerender when it's updated. It's an array of JSX elements and it's updated via either foundUnitsArray or convertToUnitsArray, depending on what stage the dialog is at. Currently both states, dialogStage and dialogUnits, are updated at the same moment the problem occurs.
Problem:
When choosing the convertTo units, displayConversionTo() was still being called, as though dialogStage was still set to 'initial' rather than 'concertTo'. Some debugging led to confusion as to why the if (dialogStage == 'initial') was true when I'd set the state to 'convertTo'.
I believe that my problem was that the dialogStage state wasn't updated in time when handleUnitClick() was called as it's asynchronous. So I set up a new useEffect that's only called when dialogStage is updated.
The problem now is that the dialog shows no 'convertTo' units after the initial selection. I believe it's now because dialogUnits hasn't updated in time? I've swapped my original problem from one state not being ready to another state not being ready.
Question
How do I wait until both states are updated before continuing to call a function here (e.g. handleUnitClick()?).
Or have I mistaken what the problem is?
I'm new to react and, so far, I'm only familiar with the practice of state updates automatically rerendering a component when ready, unless overridden. Updating dialogUnits was displaying new units in the dialog until I tried to update it only when dialogStage was ready. It feels like an either/or situation right now (in terms of waiting for states to be updated) and it's quite possible I've overlooked something more obvious, as it doesn't seem to fit to be listening for state updates when so much of ReactJs is built around that already being catered for with rerenders, etc.
Component code:
function DialogConvert(props) {
const units = props.pageUnits;
const [dialogUnits, setDialogUnits] = useState([]);
const [dialogStage, setDialogStage] = useState('initial');
let foundUnitsArray = [];
let convertToUnitsArray = [];
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
useEffect(() => {
setDialogUnits(foundUnitsArray);
}, []);
useEffect(() => {
if (dialogStage == "convertTo") {
setDialogUnits(convertToUnitsArray);
}
}, [dialogStage]);
function handleClickClose(event) {
setDialogStage('initial');
props.callbackFunction("none");
}
function handleUnitClick(homogName) {
if (dialogStage == "initial") {
// getConversionChoices is an external function that returns an array. This returns fine and as expected
const choices = getConversionChoices(homogName);
displayConversionTo(choices);
} else if (dialogStage == "convertTo") {
// Can't get this far
// Will call a function not displayed here once it works
}
}
function displayConversionTo(choices) {
let canConvertTo = choices[0]["canconvertto"];
if (canConvertTo.length > 0) {
canConvertTo.forEach(element => {
convertToUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
setDialogStage('convertTo');
}
}
return (
<React.Fragment>
<div className="dialog dialog__convertunits" style={divStyle}>
<h2 className="dialogheader">Convert Which unit?</h2>
<div className='js-dialogspace-convertunits'>
<ul className="list list__convertunits">
{dialogUnits}
</ul>
</div>
<button className='button button__under js-close-dialog' onClick={handleClickClose}>Close</button>
</div>
</React.Fragment>
)
}
So, there are some issues with your implementations:
Using non-state variables to update the state in your useEffect:
Explanation:
In displayConversionTo when you run the loop to push elements in convertToUnitsArray, and then set the state dialogStage to convertTo, you should be facing the issue that the updated values are not being rendered, as the change in state triggers a re-render and the convertToUnitsArray is reset to an empty array because of the line:
let convertToUnitsArray = [];
thus when your useEffect runs that is supposed to update the
dialogUnits to convertToUnitsArray, it should actually set the dialogueUnits to an empty array, thus in any case the updated units should not be visible on click of the initial units list.
useEffect(() => {
if (dialogStage == "convertTo") {
// as your convertToUnitsArray is an empty array
// your dialogue units should be set to an empty array.
setDialogUnits(convertToUnitsArray)
}
}, [dalogStage]);
You are trying to store an array of react components in the state which is not advisable:
http://web.archive.org/web/20150419023006/http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#what-components-should-have-state
Also, refer https://stackoverflow.com/a/53976730/10844020
Solution: What you can do is try to save your data in a state, and then render the components using that state,
I have created a code sandbox example how this should look for your application.
I have also made some changes for this example to work correctly.
In your code , since you are passing units as props from parent, can you also pass the foundUnitsArray calculated from parent itself.
setDialogUnits(props.foundUnitsArray);
and remove the below operation,
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});

Generate large dynamic forms with React, which do not re-generate on each input

I'm rewriting a form generator from a class based to a functional based approach. However, in both approaches I'm running into the same problem.
The form receives a field template, and values, loops the field specifications, and renders the appropriate inputs. Each is given it's value and a handler to carry the value in a state object of the form (which later can be submitted).
This works fine while the form is small, but of course those forms are not small and can grow to be quite large and many types of elaborate fields in them. When the form field specification grows, the form slows down to the point where there is a delay between key press and visible input. Interestingly, that delay is very noticable while in development but is much better when compiled to a production build.
I would like to render the form elements as few times as possible and prevent the whole building of the form every time a key is pressed. However, if i pre-generate the fields, the event handlers don't retain the modified values. If I rebuild it on every render - it just slows things down.
A simplified example of this is here:
https://codesandbox.io/s/black-meadow-wqmzt
Note that this example starts by pre-rendering the form content into state and rendering it later. However, you can change the renders return line (in main.js) from :
return <div>{formContent}</div>;
to
return <div>{build()}</div>;
to have the form re-build on each render. You will notice in this case that the build process runs a lot.
Is it possible to pre-render a set of inputs with event handlers attached and retain the event handler's behaviour?
Edit: The slowness of a large form rendering is manifested in the input - typing some text into a text field for example sees a delay between keypress to rendering of the input because each key press triggers a rebuild of the form.
You can just use local state [and handler] to force item update/rerenderings. This of course duplicates data but can be helpful in this case.
export default function Text({ spec, value, onChange }) {
const [val, setVal] = useState(value);
const handleChange = ev => {
onChange(ev);
setVal(ev.target.value);
};
return (
<React.Fragment>
<label>{spec.label}</label>
<input type="text" name={spec.name} value={val} onChange={handleChange} />
</React.Fragment>
);
}
working example
BTW - use key (and not just index value) on outer element of item rendered from an array:
return (
<div key={spec.name}>
<FormElement
spec={spec}
value={values[spec.name] || ""}
onChange={handleChange}
/>
</div>
You should defer eventHandlers and all the behavior to React. I've simplified your code a bit here: https://codesandbox.io/s/solitary-tree-1hxd2. All the changes are in main.js file. Below I explain what I changed and why.
Removed useEffect hook and trigger of build() in there. Your hook was called only once on the first render and wasn't called on re-renders. That caused values don't update when you changed state.
Added unique key to each field. This is important for performance. It let's React internally figure out what field has updated and trigger DOM update only for that input. Your build() function is super fast and don't have side-effects. You shouldn't worry that it is being called more than once. React may call render multiple times and you have no control over it. For heavy functions you can use useMemo (https://usehooks.com/useMemo/) hook, but it isn't the case here, even if you have 50 fields on a form.
Inlined calls to handleChange and fields. That's minor and personal preference.
I don't see any delay in the code now and render called once or twice on each field update. You can't avoid render because it is controlled component: https://reactjs.org/docs/forms.html#controlled-components. Uncontrolled components isn't recommended when using React.
Final code for form component:
export default function Main({ template, data }) {
const [values, setValues] = useState(data);
console.log("render");
return (
<div className="form">
{template.fields.map((spec, index) => {
const FormElement = Fields[spec.type];
const fieldName = spec.name;
return (
<div>
<FormElement
spec={spec}
value={values[fieldName] || ""}
key={spec.name}
onChange={e => {
setValues({
...values,
[fieldName]: e.target.value
});
}}
/>
</div>
);
})}
</div>
);
}

Best practice for emitting an event to another component in the same container?

So I have a button that lives in a container and uses a callback on it's onClick event to update state in the container, fairly basic stuff. But now I want that container to let a different child know that the button was clicked so it can trigger the appropriate response. (and it should only let the child know once so the response isn't being triggered a million times)
The way I solved this looks and feels and quacks like a code smell, so I thought I'd ask you guys if there is a better way to do it. Here is what I did:
class myContainer extend Component {
constructor(){
super()
state= { triggered: false }
}
componentWillUpdate(nextProps, nextState){
this.hasTriggered = this.state.triggered !== nextState.triggered
}
triggerResponse = () => this.setState({...this.state, !this.state.triggered})
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.hasTriggered}/>
)
}
}
Now this seems to work perfectly fine, and maybe this is what I should do, but it just feels like there has to be a neater way of sending a simple message of "I have been clicked" to a component in the same container.
One major red flag for me is that "triggered" is a boolean, but it doesn't matter if it is true or false, so if triggered is false, it means nothing, all that matters if it was the other boolean last round. This seems like a violation of good practices to me.
*Summary: What I'm looking for is a snappy way to give state a value for just one update cycle and then go back to null or false without having to update it again. Or a different way to get the same result.
I came up with 2 different yet unsatisfying answers:
class myContainer extend Component {
constructor(){
super()
state= { hasTriggered: false }
}
shouldComponentUpdate(nextProps, nextState){
return (!nextState.hasTriggered && this.state.hasTriggered)
}
componentDidUpdate(nextProps, nextState){
if(nextState.hasTriggered)this.setState({hasTriggered: false})
}
triggerResponse = () => this.setState({hasTriggered: true})
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.state.hasTriggered}/>
)
}
}
This is unsatisfying because it is a lot of code for a very simple button click. I am setting state, sending it down, resetting state and then ignoring the next render call all to accommodate a lousy button click. The good news is that I'm no longer misusing a boolean, but this is definitely too much code.
class myContainer extend Component {
componentDidUpdate(){
this.hasTriggered = false
}
triggerResponse = () => {
this.hasTriggered = true
this.forceUpdate()
}
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.hasTriggered}/>
)
}
}
I find this method unsatisfying because I no longer have the shared state in state. Before I also did this by comparing new and old state and making a variable carry the result over to my component, but at least then I could look at my state and see that there is a state variable that has to do with this button. Now there is local state in my component that is in no way linked to the actual state, making it harder to keep track of.
After thinking about this all day I've come to the conclusion that #ShubhamKhatri was on the right track in his comment, I thought I was pulling up state to my container by using a callback and passing state down, but clearly there is too much logic being executed in my component if it's handling a click event. So my new rule is that in this kind of scenario you should just pull up whatever state you need to execute the onClick inside the container. If your dumb components are executing anything other than a callback it is a mistake.
The reason I was tempted to do the onClick logic in my presentational component is because I was using a third party library(d3) to handle the graphics and so I didn't consider that the state I wasn't pulling up was the d3 state, if I move that up, change it when the button is clicked and then pass it down to my component it works beautifully.
Now this means I need to import d3 in two places and I did have to write a bit more code than before, but I think the separation of concerns and the overall cleanliness of my code is well worth it. Also it made my child component a lot easier to maintain, so that's nice.

How to update a state object in the DOM from componentDidMount() (not componentDidUpdate())?

I'm trying to conditionally show or not show per say a button based on data that I receive from clicking on a point. I realized that regular jquery functions to add a class don't really work in React. So I figured I could store strings in the state like
this.state: {
hidden_components: {
add_comment: "hide"
}
}
This way I can conditionally show or hide a button by
<button className={this.state.hidden_components.add_comment}> Add Comment </button>
After the render() I have more or less:
componentDidMount() {
this.state.g = new Dygraph
this.state.modal = new Modal
this.state.modal.setContent(use some ID here to reference a div that is hidden but will show up in the modal)
const set_hidden_container = () => {
// I'm just going to use this = notation instead of setState()
// this is supposed to reset the
this.state.hidden_components = "hide"
if (check_comment(this.state.points[at some index].value)) {
this.state.hidden_components = "show"
}
}
this.state.g.updateOptions( {
pointClickCallback: (event, p) => {
console.log("i clicked a point on the graph")
this.setState({
currentPoint: p
})
set_hidden_containers()
// force update
this.setState({
currentPoint: p
})
// I want the modal to open a div of things that only show jsx based on logic in set_hidden_container()
this.state.modal.open()
}
}
componentDidUpdate() {
// logic goes here for like event listeners and anything that queries the DOM after initialization
}
Then in componentDidMount() I have a function that depending on the data received from clicking on a point I do the following:
1) reset all the classes stored in the state to "hide"
2) based on conditions set some of them to "show"
3) concatenate all the classes stored in the state with various styling classes
UPDATE:
I've long since found an easier solution to this problem, however, I'm guessing some people might have similar issues. Therefore, I'll update this question with more psuedocode and a workaround: maybe someone down the line can solve this. This component is particularly frustrating to work with because I haven't been able to make it as modular as I want because of the particular library I'm working with. There are actually about a 1000 lines in this component (I know I know not good).
WORKAROUND:
For those of you who are having trouble with a component's lifecycle in dynamically setting parts of the DOM but don't want to use global variables to set classNames, jquery functions, or use react syntax to show components containing the content I recommend you do the following.
You can still have a set_hidden_container() set content dynamically, you just have to set things based on an id with innerHTML instead of setting a state object to be a string "show". The important thing is, however, that for every time you need to dynamically change content you reset these references to be empty as well as force an update. You can simply change the state of anything and then in componentDidUpdate() you can insert 1) a conditional to check if the innerHTML was actually set or not (since you're not always going to be displaying everything) and 2) within that conditional you can set whatever logic you want associated with the content showing on the page.
componentDidMount is invoked immediately after a component is mounted. If you want to set classNames based on clicks, I would put that logic in componentDidUpdate, which is invoked after updating occurs.

How to 'reset' a ReactJS element?

I'm trying to 'reset' a ReactJS element.
In this case, the element is 90%+ of the contents of the page.
I'm using replaceState to replace the state of the element with with its initial state.
Unfortunately, sub-elements which have their own 'state' do not reset. In particular, form fields keep their contents.
Is there a way of forcing a re-render of an element, which will also cause sub-elements to re-render, as if the page had just loaded?
Adding a key to the element forces the element (and all its children) to be re-rendered when that key changes.
(I set the value of 'key' to simply the timestamp of when the initial data was sent.)
render: function() {
return (
<div key={this.state.timestamp} className="Commissioning">
...
The this.replaceState(this.getInitialState()) method doesn't actually reset children that are inputs, if that's what you're looking for. For anyone looking to just reset their form fields, there is a standard DOM reset() function that will clear all the inputs in a given element.
So with React, it'd be something like this:
this.refs.someForm.getDOMNode().reset();
Doumentation:
https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset
If it is a form you want to reset, you simply can use this
// assuming you've given {ref: 'form'} to your form element
React.findDOMNode(this.refs.form).reset();
While I don't personally think you should store local, interim component state (like in-progress input boxes) in a centralized location (like a flux store) in most cases, here it may make sense, depending on how many you have, especially since it sounds like the inputs already have some server interaction/validation around them. Pushing that state up the component hierarchy or into some other central location may help a lot in this case.
One alternative idea off the top of my head is to use a mixin in components that might need to reset local state, and do some kind of event triggering, etc. to make it happen. For example, you could use Node's EventEmitter or a library like EventEmitter3 with a mixin like this (warning: not tested, maybe best this as pseudocode :)
var myEmitter = new EventEmitter(); // or whatever
var ResetStateMixin = {
componentWillMount: function() {
myEmitter.on("reset", this._resetState);
},
componentWillUnmount: function() {
myEmitter.off("reset", this._resetState);
},
_resetState: function() {
this.replaceState(this.getInitialState());
},
triggerReset: function() {
myEmitter.emit("reset");
}
};
Then you could use it in components like so:
React.createClass({
mixins: [ResetStateMixin],
getInitialState: function() {
return { ... };
},
onResetEverything: function() {
// Call this to reset every "resettable" component
this.triggerReset();
}
});
This is very basic and pretty heavy handed (you can only reset all components, every component calls replaceState(this.getInitialState()), etc.) but those problems could be solved by extending the mixin a bit (e.g. having multiple event emitters, allowing component-specific resetState implementations, and so forth).
It's worth noting that you do have to use controlled inputs for this to work; while you won't need to push your state all the way up the component hierarchy, you'll still want all your inputs to have value and onChange (etc.) handlers.
You could also use document.forms[0].reset()

Categories