React only re-renders modified contenteditable once - javascript

This is a bit of a weird one to reproduce, had to use timeouts to hack it but it does emulate the issue I'm having.
https://jsfiddle.net/mwqyub1v/13/
1) React renders the div with abc123. Manipulate this string (add a 4 or something)
2) After the first timeout, React will remove the content -- type something in to the box manually now, before the second timer triggers..
3) At the second timer, React does run the render method (note the logging fires) with this.props.value as null, but the div does not update (it won't remove any text you've typed in since abc123 went away).
I know playing with the contents of a contenteditable can prevent React from being able to manage that content (hence me suppressing the warning). But why does the component update correctly after I've modified text in step 1 above? Furthermore, if the second timer sets value to be some other text, as opposed to null, then once again the component updates properly.
How can I get this component to always reset the contenteditable contents, even when value is null?

The reason that it's not updated with the second timeout because the virtual dom didn't detect any difference. This will set the innerHTML based on the props: https://jsfiddle.net/mwqyub1v/35/.
class App extends React.Component {
componentDidMount() {
this.node = React.findDOMNode(this);
}
componentDidUpdate() {
this.node.innerHTML = this.props.value;
}
render() {
console.log('value', this.props.value);
return React.createElement('div', {
contentEditable : true,
suppressContentEditableWarning : true
}, this.props.value);
}
}

Related

console.log of editorState on unfocus display a stack of all the changes

Please check in first place this codesandbox.
I wrote a TextEditor component using draftjs that get 3 props:
text: html as a string
onChange: a callback that get a unique parameter (the updated html string)
onUnfocus: a callback that is called when you click outside of the TextEditor
If you look in the App component where I use my TextEditor component, I store the text in a state that I update using onChange.
Clicking outside the TextEditor (with the blue background) will trigger the onUnfocus function that console.log the text state.
But If I write a word (let's say Hello) and then click outside the TextEditor I will get so many console.log as changes:
> H
> He
> Hel
> Hell
> Hello
The expected behavior would be to have a single console.log with the latest changes:
> Hello
Has someone an idea why I get so many console.log?
The issue is that you forgot to return your useEffect helper, and also didn't pass in text as a dependency, so the cleanup code is never called, so each time it renders it adds another document.addEventListener without ever removing the previous.
React.useEffect(() => {
return onClickOutsideHook( // add return here
editorRef,
(): void => {
if (isFocus && onUnfocus !== undefined) {
onUnfocus();
}
setIsFocus(false);
}
);
}, [editorState, isFocus, onUnfocus, text]); // add text as dependency

How to reset state on props change in an already mounted component?

I have a <BlogPost> component which could've been a Stateless Function Component, but turned out as a Class Stateful Component because of the following:
The blogPost items that it renders (receiving as props) have images embedded in their html marked content which I parse using the marked library and render as a blog post with images in between its paragraphs, h1, h2, h3, etc.
The fact is that I need to preload those images before rendering the post content to my client. I think it's a UX disaster if you start reading a paragraph and all of a sudden it moves down 400px because the image that was being loaded has been mounted to the DOM during the time you were reading it.
So I prefer to hold on by rendering a <Spinner/> until my images are ready. That's why the <BlogPost> is a class component with the following code:
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.state={
pending: true,
imagesToLoad: 0,
imagesLoaded: 0
};
}
preloadImages(blogPostMedia) {
this.setState({
pending: true,
imagesToLoad: 0,
imagesLoaded: 0
});
... some more code ...
// Get images urls and create <img> elements to force browser download
// Set pending to false, and imagesToLoad will be = imagedLoaded
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props !== nextProps) {
this.preloadImages(nextProps.singleBlogPost.media);
}
}
componentDidMount() {
this.preloadImages(this.props.singleBlogPost.media);
}
render() {
return(
this.state.pending ?
<Spinner/>
: (this.state.imagesLoaded < this.state.imagesToLoad) ?
<Spinner/>
: <BlogPostStyledDiv dangerouslySetInnerHTML={getParsedMarkdown(this.props.singleBlogPost.content)}/>
);
}
}
export default BlogPost;
At first I was calling the preloadImages() only inside the componentDidMount() method. And that works flawlessly for the first post I render with it.
But as soon as I would click on the next post link; since my <BlogPost>component is already mounted, componentDidMount() doesn't get called again and all the subsequent posts I would render by clicking on links (this is a Single Page App) wouldn't benefit from the preloadImages() feature.
So I needed a way to reset the state and preload the images of the new blogPost received as props inside an update cycle, since the <BlogPost> component it's already mounted.
I decided to call the same preloadImages() function from inside the UNSAFE_componentWillReceiveProps() method. Basically it is reseting my state to initial conditions, so a <Spinner/> shows up right away, and the blog post only renders when all the images have been loaded.
It's working as intended, but since the name of the method contains the word "UNSAFE", I'm curious if there's a better way to do it. Even though I think I'm not doing anything "unsafe" inside of it. My component is still respectful to its props and doesn't change them in anyway. It just been reset to its initial behavior.
RECAP: What I need is a way to reset my already mounted component to its initial state and call the preloadImages() method (inside an update cycle) so it will behave as it was freshly mounted. Is there a better way or what I did is just fine? Thanks.
I would stop using componentWillReceiveProps()(resource). If you don't want the jarring effect, one way you can avoid it is to load the information from <BlogPost/>'s parent, and only once the information is loaded, to pass it into <BlogPost/> as a prop.
But anyway, you can use keys to reset a component back to its original state by recreating it from scratch (resource).
componentWillReceiveProps is deprecated, it's supposed to be replaced with either getDerivedStateFromProps or componentDidUpdate, depending on the case.
Since preloadImages is asynchronous side effect, it should be called in both componentDidMount and componentDidUpdate:
componentDidMount() {
this.preloadImages(this.props.singleBlogPost.media);
}
componentDidUpdate() {
this.preloadImages(this.props.singleBlogPost.media);
}

React Component Flickers on Rerender

I'm pretty new to react and have been working on this new page for work. Basically, there's a panel with filter options which lets you filter objects by color. Everything works but I'm noticing the entire filter panel flickers when you select a filter.
Here are the areas functions in the filter component I think bear directly on the filter and then the parent component they're inserted into. When I had originally written this, the filter component was also calling re render but I've since refactored so that the parent handles all of that - it was causing other problems with the page's pagination functionality. naturally. and I think that's kind of my problem. the whole thing is getting passed in then rerendered. but I have no idea how to fix it. or what's best.
checks whether previous props are different from props coming in from parent and if so, creates copy of new state, calls a render method with those options. at this point, still in child component.
componentDidUpdate(prevProps, prevState) {
if (prevState.selectedColorKeys.length !== this.state.selectedColorKeys.length ||
prevState.hasMatchingInvitation !== this.state.hasMatchingInvitation) {
const options = Object.assign({}, {
hasMatchingInvitation: this.state.hasMatchingInvitation,
selectedColorKeys: this.state.selectedColorKeys
});
this.props.onFilterChange(options);
}
}
handles active classes and blocks user from selecting same filter twice
isColorSelected(color) {
return this.state.selectedColorKeys.indexOf(color) > -1;
}
calls to remove filter with color name so users can deselect with same filter button or if its a new filter, sets state by adding the color to the array of selected color keys
filterByColor(color) {
if (this.isColorSelected(color.color_name)) {
this.removeFilter(color.color_name);
return;
}
this.setState({
selectedColorKeys:
this.state.selectedColorKeys.concat([color.color_name])
});
}
creating the color panel itself
// color panel
colorOptions.map(color => (
colorPicker.push(
(<li className={['color-filter', this.isColorSelected(color.color_name) ? 'active' : null].join(' ')} key={color.key} ><span className={color.key} onClick={() => { this.filterByColor(color); }} /></li>)
)
));
parent component
callback referencing the filter child with the onFilterChange function
<ThemesFilter onFilterChange={this.onFilterChange} />
onFilterChange(filters) {
const { filterThemes, loadThemes, unloadThemes } = this.props;
unloadThemes();
this.setState({
filterOptions: filters,
offset: 0
}, () => {
filterThemes(this.state.filterOptions.selectedColorKeys, this.state.filterOptions.hasMatchingInvitation);
loadThemes(this.state.offset);
});
}
when I place break points, the general flow seems to be :
filterByColor is triggered in event handler passing in that color
active classes are added to the color, a filter tag for that color is generated and appended
componentDidMount takes in the previous props/state and compares it to the new props/state. if they don't match, i.e state has changed, it creates a copy of that object, assigning the new states of what's changed. passes that as props to onFilterChange, a function in the parent, with those options.
onFilterChange takes those options, calls the action method for getting new themes (the filtering actually happens in the backend, all I really ever need to do is update the page) and passes those forward. its also setting offset to 0, that's for the page's pagination functionality.
It looks like the problem might be around the componentDidUpdate function which, after setting breakpoints and watching it go through the steps from filterByColor to componentDidMount, that componentDidMount loops through twice, checking again if the colorIsSelected, and throughout all that the color panel pauses to re-render and you get a flicker.
Is it possible creating the copy is causing it? since it's being treated, essentially, as a new object that isColorSelected feels necessary to double check? any help you guys have would be much appreciated, this shit is so far over my head I can't see the sun.
Can you change
componentDidUpdate(prevProps, prevState)
with
componentWillUpdate(nextProps, nextState)

ReactJs how to know when a component is removed from DOM

I'm currently facing a problem with React. I need to know when a component is removed from DOM.
All I find in component lifecycle is componentWillUnmount which gets called before the component is removed from DOM.
Is there any way to achieve this with React ?
In plain javascript ?
Thanks.
[EDIT]
#jpdelatorre
"Plain javascript" === Not using a library ;-)
My use case is the use of jsPlumb within a react component.
Basically jsPlumb is a library that draws svg in the DOM with position calculation.
In my main component there is a list of items.
Each item is a component.
On each rendered item I use JsPlumb to draw on it.
But... When I remove an item of the list the items are changing there positions in the DOM so I need to ask to jsPlumb to redraw things based on new positions. So that's why I need to know when component is fully removed from DOM.
componentWillUnmount is the correct lifecycle method. As you say, it is triggered prior to the component being removed. If you have something you need to wait to to until after it's been removed, you can use setTimeout with a short timeout value to schedule a callback once the current task completes.
componentWillUnmount is the life cycle method which can be hooked.
But as mentioned if you want to know in component1 about unmounting of component2 then you need to trigger an action in component2 in its lifecycle method, which should be subscribed in component1 and do some action in component1 listener.
As other already mentioned you need componentWillUnmount.
Here is a simple example in React(I added there some comment to get what is going one):
var Button = React.createClass({
componentWillUnmount: function(){
// console will show this message when compoent is being Unmounted("removed")
console.log('Button removed');
},
render() {
return <h1 ref='button_node'>
<ReactBootstrap.Button bsStyle="success">Red</ReactBootstrap.Button>
</h1>;
}
});
var RemoveButton = React.createClass({
getInitialState: function() {
// this state keep tracks if Button removed or not
//(you can use it for some redrawing or anything else in your code)
return {buttonMounted: true}
},
mountRedButton: function(){
ReactDOM.render(<Button/>, document.getElementById('button'));
this.setState({buttonMounted: true});
},
unmountRedButton: function(){
ReactDOM.unmountComponentAtNode(document.getElementById('button'));
this.setState({buttonMounted: false});
},
render() {
return <h1>
//based on condition if Button compoennt removed or not we show/hide different buttons
{ this.state.buttonMounted ? <ReactBootstrap.Button onClick={this.unmountRedButton } bsStyle="danger">Remove Red Button!</ReactBootstrap.Button> : null}
{ this.state.buttonMounted ? null :<ReactBootstrap.Button onClick={this.mountRedButton } bsStyle="success">Add Red Button!</ReactBootstrap.Button> }
</h1>;
}
});
// mount components
ReactDOM.render(<Button/>, document.getElementById('button'));
ReactDOM.render(<RemoveButton/>, document.getElementById('remove'));
Here is a full working example on JSFiddle
As about "plain javascript" - you are already using React JS,my example is based on React and ReactDom, nothing more(actually there is also react-bootstrap, I added it only for pretty buttons, it is not required at all)
Update:
What about use of MutationObserver? If you need a time when Node in DOM is removed but componentWillUnmount is fired before node removal(which seems unsutable for you) you can use it. Following my example with buttons:
var removalWatcher = new MutationObserver(function (e) {
var removalTimeStamp = '[' + Date.now() + '] ';
if (e[0].removedNodes.length) {
console.log('Node was removed', e[0].removedNodes, 'timestamp:', removalTimeStamp)
};
});
here is a JsFiddle with updated example. You can compare timestamps that are printed into console both for MutationObserver and React ComponentWillUnmount.
Ok thanks to all ! I found a solution that fits my needs and is (IMHO) clean...
In my parent component I handle componentDidUpdate lifecycle event and in there I can know (with prevProps and props) if the changes that are made need to fire my action...
class MyComponent extends Component {
// this one is called each time any props is changed
componentDidUpdate(prevProps, prevState) {
// if condition is matched then do something
}
}

ReactJs: change state in response to state change

I've got a React component with an input, and an optional "advanced input":
[ basic ]
Hide Advanced...
[ advanced ]
The advanced on the bottom goes away if you click "Hide Advanced", which changes to "Show Advanced". That's straightforward and working fine, there's a showAdvanced key in the state that controls the text and whether the advanced input is rendered.
External JS code, however, might change the value of advanced, in which case I want to show the [advanced] input if it's currently hidden and the value is different than the default. The user should be able to click "Hide Advanced" to close it again, however.
So, someone external calls cmp.setState({advanced: "20"}), and I want to then show advanced; The most straightforward thing to do would just be to update showAdvanced in my state. However, there doesn't seem to be a way to update some state in response to other state changes in React. I can think of a number of workarounds with slightly different behavior, but I really want to have this specific behavior.
Should I move showAdvanced to props, would that make sense? Can you change props in response to state changes? Thanks.
Okay first up, you mention that a third party outside of your component might call cmp.setState()? This is a huge react no-no. A component should only ever call it's own setState function - nothing outside should access it.
Also another thing to remember is that if you're trying change state again in response to a state change - that means you're doing something wrong.
When you build things in this way it makes your problem much harder than it needs to be. The reason being that if you accept that nothing external can set the state of your component - then basically the only option you have is to allow external things to update your component's props - and then react to them inside your component. This simplifies the problem.
So for example you should look at having whatever external things that used to be calling cmp.setState() instead call React.renderComponent on your component again, giving a new prop or prop value, such as showAdvanced set to true. Your component can then react to this in componentWillReceiveProps and set it's state accordingly. Here's an example bit of code:
var MyComponent = React.createClass({
getInitialState: function() {
return {
showAdvanced: this.props.showAdvanced || false
}
},
componentWillReceiveProps: function(nextProps) {
if (typeof nextProps.showAdvanced === 'boolean') {
this.setState({
showAdvanced: nextProps.showAdvanced
})
}
},
toggleAdvancedClickHandler: function(e) {
this.setState({
showAdvanced: !this.state.showAdvanced
})
},
render: function() {
return (
<div>
<div>Basic stuff</div>
<div>
<button onClick={this.toggleAdvancedClickHandler}>
{(this.state.showAdvanced ? 'Hide' : 'Show') + ' Advanced'}
</button>
</div>
<div style={{display: this.state.showAdvanced ? 'block' : 'none'}}>
Advanced Stuff
</div>
</div>
);
}
});
So the first time you call React.renderComponent(MyComponent({}), elem) the component will mount and the advanced div will be hidden. If you click on the button inside the component, it will toggle and show. If you need to force the component to show the advanced div from outside the component simply call render again like so: React.renderComponent(MyComponent({showAdvanced: true}), elem) and it will show it, regardless of internal state. Likewise if you wanted to hide it from outside, simply call it with showAdvanced: false.
Added bonus to the above code example is that calling setState inside of componentWillReceiveProps does not cause another render cycle, as it catches and changes the state BEFORE render is called. Have a look at the docs here for more info: http://facebook.github.io/react/docs/component-specs.html#updating-componentwillreceiveprops
Don't forget that calling renderComponent again on an already mounted component doesn't mount it again, it just tells react to update the component's props and react will then make the changes, run the lifecycle and render functions of the component and do it's dom diffing magic.
Revised answer in comment below.
My initial wrong answer:
The lifecycle function componentWillUpdate will be ran when new state or props are received. You can find documentation on it here: http://facebook.github.io/react/docs/component-specs.html#updating-componentwillupdate
If, when the external setState is called, you then set showAdvanced to true in componentWillUpdate, you should get the desired result.
EDIT: Another option would be to have the external call to setState include showAdvanced: true in its new state.

Categories

Resources