dangerouslySetInnerHtml doesn't update during render - reactjs

So I made a component for including content-editable components in my app. I copied it from some gist I believe, then edited to what i needed.
The code is below. When I edit it, it triggers updates on the parent just fine, but when I attempt to set props.html in the parent, it doesn't reflect in the UI.
FURTHER, the console.log shows that this.props.html is equal to '' a blank string, yet the UI doesn't update, and maintains the text that was originally in there.
I don't understand how this is possible... dangerouslySetInnerHtml = {__html: ''} should make it so the UI reflects an empty string... it feels like it should be impossible for it to show the old text.
var React = require('react');
var ContentEditable = React.createClass({
render: function(){
//TODO: find where html=undefined and fix it! So I can remove this? Maybe I should keep this safety.
var html = this.props.html || '';
console.log('content editable render, html: ', this.props.html);
return <div id="contenteditable"
onInput={this.emitChange}
onBlur={this.emitChange}
contentEditable
dangerouslySetInnerHTML={{__html: html}}></div>;
},
shouldComponentUpdate: function(nextProps){
return nextProps.html !== this.getDOMNode().innerHTML;
},
emitChange: function(){
var html = this.getDOMNode().innerHTML;
if (this.props.onChange && html !== this.lastHtml) {
this.props.onChange({
target: {
value: html
}
});
}
this.lastHtml = html;
}
});
module.exports = ContentEditable;
(A little background, I'm trying to clear my input after submitting it to be saved. The clearing isn't working, hence this question.)

I got a very similar problem using contentEditable and shouldComponentUpdate, it looks like there is a bug when resetting the innerHTML to the same previous value using dangerouslySetInnerHTML function (or prop) (I think it does not work even if you insert the code without using it) ... I suspect (this is just an idea) that React compares the last value set through dangerouslySetInnerHTML with the new one you are trying to send and decides not to update the component because it is the same (even if the real innerHtml has changed due to user interactions, because those interactions does not trigger any state or props update on React).
Solution: The easiest solution I found was to use a different key each time I needed it to re-render. for example using key={Date()}.
Example: Here you can find your code (I changed some of it to make it work), when you type '?' into the div, the text inside the ContentEditable component should become an empty string (i.e. ''), it works only once, the second time you type '?' won't work because the innerHTML for react will be the same to the one you're setting (i.e. an empty string so it won't update the component).
And here, I added the key={Date()} (this is the easiest way to show you that this work, but it is not the best way to set a unique key each time it re-render) to the editable component, now you can type any number of '?' and it will work.

I found another solution that is probably better than generating random keys. Putting a key specifically on the div that calls #dangerouslySetInnerHtml, and not just on the component itself
<div class='wrapper'>
<div key={this.props.thing.id} dangerouslySetInnerHtml={this.props.thing.content} />
</div>

This is not the case here but make sure you always dangerouslyset html on div tag and never on span, p ... because if span element child is div, there will be a problem.
Solved rerender bug to me

My (very simple) React (version 16) app: It has a contentEditable <div>.
It successfully re-renders this <div> upon a progression of submit button clicks. Instead of dangerouslySetInnerHtml, I used ref={el => this.myRefElem = el} with componentWillUpdate(nextProps) { this.myRefElem.innerHTML = nextProps.myInputText; } For me, nextProps was important for the proper value to re-render. See my app's project files, to see the rest of the required code.
CLICK-HERE​ to see my React app. It has a button to download its (development mode) project files. It (basically) only has an index.js file. - - - This app was initiated by mlbrgl, who asked me for an alternative technique.

I ran into the same issue (React 16) and used an approach suggested by MLR which consists in dropping dangerouslySetInnerHTML and using componentDidMount() instead for the initial render and componentDidUpdate() for any subsequent renders.
Solution here, adapted to React 16: https://codepen.io/mlbrgl/pen/PQdLgb
These hooks would perform the same update, directly updating innerHTML from props:
componentDidMount() {
this.updateInnerHTMLFromProps();
}
componentDidUpdate() {
this.updateInnerHTMLFromProps();
}
updateInnerHTMLFromProps() {
this.refEl.innerHTML = this.props.html;
}
This makes it clearer (for me at least) to see what is really going on, without having the false expectation that dangerouslySetInnerHTML would keep the DOM in sync in all circumstances, as suggested by Mike Woodcock here https://stackoverflow.com/a/38548616/9408759.
For a complete view of the problem and both solutions outlined here, please check https://codepen.io/mlbrgl/pen/QQVMRP.

Adding key property for an element with dangerouslySetInnerHTML did resolve my issue.
As a key I used
key={new Date().getTime()} // timestamp

Related

React component getting wrong value from a wrong field field in the state object when I use "defaultValue" to assign values

My code is as follows, and you can see how it works in https://codepen.io/rongeegee/pen/BaVJjGO:
const { useState } = React;
const Counter = () => {
const [data, setData] = useState({
displayData: "data_one",
data_one: {
text: ""
},
data_two:{
text:""
}
})
function handleOnChange(event){
event.preventDefault();
const new_data = {...data};
if (event.target.name == "displayData"){
new_data.displayData = event.target.value;
setData(new_data);
}
else{
new_data[event.target.name]["text"] = event.target.value;]
setData(new_data);
}
}
return (
<div>
<form onChange={handleOnChange}>
<select name="displayData" value={data.displayData}>
<option value="data_one">data_one</option>
<option value="data_two">data_two</option>
</select>
<br/>
{
data.displayData == "data_one"
?
<>data One: <input name="data_one" defaultValue={data.data_one.text} /></>
:
<>data two: <input name="data_two" defaultValue={data.data_two.text} /></>
}
</form>
</div>
)
}
ReactDOM.render(<Counter />, document.getElementById('app'))
If I type something in the input of data_one, toggle between the values between "data_one" and "data_two", the data_two input field will have the same value inside. If I change the value in data_one toggle the dropdown to "data_one", data_one will have the same value again.
This shouldn't happen since data_one input uses the value of the text field in data_one field in the data state while data_two input uses the one in data_two field. One should not take the value from another field in the state.
React has a way to determine if/which elements/components have changed and which haven't. This is neccesary, because DOM manipulation is expensive, so we should try to limit it as much as possible. That's why React has a way to determine what changed between rerenders and what didn't; and only changes what changed in the DOM.
So if we in your case swith from dataOne to dataTwo, React goes something like: "Oh nice input element stays input element. Nice I don't have to completely destroy that DOM node and render it rom scratch, I can just check what changed and change that. Let's see: The name has changed ... so let's change that, but apart from that everything stayed the same." (This means your input element won't get destroyed and the other one initialy rendered, but the one input element get's a name change and React calls it a day - and since default Value only works on initial creation of an element/DOM node, it won't be shown)
The way React rerenders stuff and compares/modifies the DOM is quite complicated. For futher information I can recomend the following video: https://youtu.be/i793Qm6kv3U (It really helped me understand the whole React Render process way better).
A possible fix to your problem, would be to give each input element a key. So your input elements could look something like:
<input key="1" name="data_one" defaultValue={data.data_one.text} />
<input key="2" name="data_two" defaultValue={data.data_two.text} />
So yeah, the fix is fairly easy; understanding the reason to why we need this fix however is everything but easy.
regarding your comment (my answer would be to long for a comment, and formatting is nicer in an answer)
nope you aren't changing state on input-field change ... you are changing/manipulating the state variable data ... but you are not updating your state. Hence no rerender gets triggered, and in case of a rerender triggered by something else the data will be lost. So you aren't actually changing state.
Changes to your state can only be made by calling the callback Funcnction provided by the useState-Hook. (In your case the callback provided by the useState-Hook is setData. In your else statement you are not calling the provided callback, just manipulating the data object (since you shallow clone and therefor manipulate the original data object)
When changing state ALWAYS use the provided callback const [stateVariable, thisIsTheCallback] = useState(initVal). If you don't use this callback and just manipulate the stateVariable you will sooner or later run into problems (and debugging that issue is particularly tedious, since it seems like you are changing state (because the stateVariable changes) but you aren't changing state since only the callback can do this)
In place of defaultValue attribute to your <input/> replace that with value attribute and everything will work.
And to know why defaultValue won't get updated after state change, read the first answer by #Lord-JulianXLII

cannot render a complex React Element in order to replace a DOM element by id

I am a beginner with react so please excuse me if this question does not make sense.
I have a complex div element in the html document with an id, it looks like this:
aaavote: I am comingChange My voteDelete Me
and it was originally created from rendering a react element that was returned from a component, like this:
let content = visitors_list.map((visitor) =>
)
{content} was returned from the react component.
At some stage I need to replace that div (with id="MyDiv2") with a different content.
I have a function that creates this new "content" exactly the same way I created the original one, however, now instead of return it from the component and have react do the rendering, I need to do it manually and call:
document.getElementById("MyDiv2").innerHTML = something
I cannot just pass content as something because : component is an array of [object Object]
I cannot pass it as a json structure because JSON.stringify(content) gives me:
[{"type":"div","key":"1","ref":null,"props":{"className":"flex-no-shrink w-full md:w-1/4 md:px-3","children":{"key":null,"ref":null,"props":{"visitor":{"id":"1","name":"aaa","vote":0,"imageUrl":"http://www.stone-guitar-picks.com/stoneguitarpicksshop/images/large/GP2046_LRG.JPG"}},"_owner":null,"_store":{}}},"_owner":null,"_store":{}}]
so I tried: ReactDOM.render(content, document.getElementById("MyDiv2"))
but ReactDOM.render does nothing, infact it breaks and exists the function.
I also tried using React.createElement(content) - also did not work...
How can I solve this problem. the only solution that I found to work is forcing the refreshing of the page which I do not want to do.
Dont try to handle the dom manually, react does this for you!, imagine you have your component MyInput that renders an input with some characteristics that may be passed by properties as well, then you could just change your component props and react will change your component automatically. i'll try to post a hardcoded example:
const myComp = ({text, visible}) = return (
<input text={text} visibility={visible}>
)
then, when you change your component text it will render it automatically.

How to control a non-React component (BokehJS) in React?

Backstory
I want to include a BokehJS plot in my React component. The process for this is to render <div id="my_plot_id" className="bk-root"/> and call window.Bokeh.embed.embed_item(plotData, 'my_plot_id') which injects needed HTML into the DOM.
Because I want to control the BokehJS plot using the React component's state (i.e replace the plot with new generated plot data), I don't want to just call embed_item() in componentDidMount(). I've instead placed embed_item() in render() and added some code to remove child nodes of the container div prior to this call.
Problem
My React component renders 3 times on page load and although by the final render I have only one plot displayed, there is a brief moment (I think between the 2nd and 3rd/final render) where I see two plots.
Code
render()
{
let plotNode = document.getElementById('my_plot_id');
console.log(plotNode && plotNode.childElementCount);
while (plotNode && plotNode.firstChild) {
//remove any children
plotNode.removeChild(plotNode.firstChild);
}
const { plotData } = this.state;
window.Bokeh.embed.embed_item(plotData, 'my_plot_id');
return(
<div id="my_plot_id" className="bk-root"/>
)
}
In console I see:
null
0
2
Question
So it seems embed_item executes twice before the my_plot_id children are correctly detected.
Why is this happening and how can I resolve it? While the triple render may not be performance optimized I believe my component should be able to re-render as often as it needs to (within reason) without visual glitching like this, so I haven't focused my thought on ways to prevent re-rendering.
Interaction with DOM elements should never happen inside the render method. You should initiate the library on the element using the lifecycle method componentDidMount, update it based on props using componentDidUpdate and destroy it using componentWillUnmount.
The official React documentation has an example using jQuery which shows you the gist of how to handle other dom libraries.
At start plotNode is unable to reach 'my_plot_id'.
You can render null at start, when plotData is unavailable.
You can use componentDidUpdate().
In this case I would try shouldComponentUpdate() - update DOM node and return false to avoid rerendering.

How can I trigger a click on an element inside a child component (that I don't have access to)?

I have installed react-file-base64 into my React JS project and have implemented it like so:
import FileBase64 from 'react-file-base64';
import FileUpload from '../../forms/FileUpload'
...
class MyComponent extends Component {
render() {
return (
<FileUpload buttonText='Upload New Image'>
<FileBase64
multiple={ false }
onDone={ this.changeProfileImage }
/>
</FileUpload>
)
}
}
The code is obviously condensed for brevity.
As you can see, I've wrapped the FileBase64 component inside a custom FileUpload component - to do the old JS/CSS trick of hiding the file upload and triggering it via a different button press.
Given that I do not have direct access to edit the FileBase64 component, since it's been installed by NPM (and will possibly be updated by it in the future), and given that it is not a direct input element but rather a custom component that renders one - how can I trigger a click on the input element rendered by the FileBase64 component, from inside my FileUpload component?
You have a few options.
Reconsider using react-file-base64
This is a pretty minor NPM module, so ask yourself: is it worth using a few dozen lines of someone else's code instead of writing the functionality myself? Open source is amazing and leveraging other people's work can be a lifesaver, but learn to recognize when to lean on it and when not to.
Fork react-file-base64
Fork the original project and add whatever functionality you need to meet your requirements. Ideally do it in a well-written, well-documented way so that you can later open a pull request and contribute back to the project in a meaningful way.
Hack it a bit
It's good to stay inside of React as much as possible, but there are ways around it. You can, for example, still select DOM elements using plain old JavaScript. Remember that stuff? ;P
This would probably work fine - wrap the <FileBase64 /> component in a <div> that you can use to select any nested child <input> elements.
class MyComponent extends Component {
...
onBtnClick() {
this.inputWrapper.getElementsByTagName("input")[0].click();
}
render() {
return (
<FileUpload buttonText='Upload New Image' callback={this.onBtnClick} >
<div ref={(el) => this.inputWrapper = el} >
<FileBase64
multiple={ false }
onDone={ this.changeProfileImage }
/>
</div>
</FileUpload>
)
}
}
I dunno how exactly you're handling <FileUpload /> click callbacks but you get the idea. After a component renders, its DOM elements are laid bare for you to access. The trick is figuring out how to select those elements in the first place, and being careful that you don't break React in the process. But selecting an element and triggering a "click" event is pretty benign.
There are several triggers for this component that maybe suits your needs. Some of them are:
beforeUpload: Triggered before uploading. return true to continue or false to stop uploading.
doUpload: Triggered after the request is sent(xhr send | form submit).
onabort:riggered after you aborting a xhr.
uploadSuccess: Callback when upload succeed (according to the AJAX simply).
If you see the plugin documentation you can be how they work in detail, as well as more different events to interact with your input element inside your FileUpload component.

Angular 2 setting a new value does not trigger an input change event

I'm running into a weird case that only seems to happen upon first loading a component on a heavily based component page (loading 30+ components).
#Component{
selector: <parent-component>
template: `<child-component [myObject]=myObject>
}
export class ParentComponent {
private myObject:DTOValue;
constructor(service:MyService){
service.getDTOValue().subscribe((dtoValue:DTOValue) => {
this.myObject = dtoValue;
});
}
}
#Component{
selector: <child-component>
template: `<div></div>
}
export class ChildComponent {
#Input set myObject(value:DTOValue) => {//do something};
constructor(){
}
}
In this code, the Parent is going to get a value to a child as an input. This value comes from a request at a later time, so when the child is first initialized, the input could be undefined. When the value does get returned from the request and is set on the variable myObject, I'd expect that the child component would receive an input event being triggered. However, due to the timing, it seems like this is not always the case, especially when I first load a page that contains a lot of files being loaded.
In the case that the child component doesn't receive the input, if I click else where on my page, it seems to now trigger the change detection and will get the input value.
The 2 possible solutions I can think of that would require some large code changes so I want to make sure I choose the right now before implement them.
Change the input to be an Subject, so that I push the input value which should ensure that a correct event is triggered(this seems like overkill).
Use the dynamic loader to load the component when the request as return with the correct value (also seems like overkill).
UPDATE:
Adding a plnker: http://plnkr.co/edit/1bUelmPFjwPDjUBDC4vb, you can see in here that the title seems to never get its data bindings applied.
Any suggestions would be appreciated.
Thanks!
If you can identify where the problem is and appropriate lifecycle hook where you could solve it, you can let Angular know using ChangeDetectorRef.
constructor(private _ref: ChangeDetectorRef)
method_where_changes_are_overlooked() {
do_something();
// tell angular to force change detection
this._ref.markForCheck();
}
I had a similar issue, only with router - it needed to do redirect when/if API server goes offline. I solved it by marking routerOnActivate() for check...
When you trigger change detection this way a "branch" of a component tree is marked for change detection, from this component to the app root. You can watch this talk by Victor Savkin about this subject...
Apologize, the issue ended up being my interaction with jQuery. When I triggered an event for a component to be loaded, inside of the jQuery code, it wouldn't trigger the life cycle. The fix was after the code was loaded to then call for a change detection.

Resources