Where to use componentDidUpdate in React [duplicate] - reactjs

This question already has answers here:
When to use 'componentDidUpdate' method?
(9 answers)
Closed 2 years ago.
I am a complete newbie in the world of development. I am trying to make an app where we fetch movies. We can search and sort. I don't want to use any plugins as of now.
I have been successful in fetching the data and showing on the grid. Also, searching is working well. But I'm confused if I shall put the searching method in componentDidUpdate React lifecycle hook or not. If it shall be how to do it?
Following is my code:
class App extends Component
{
constructor(props)
{
super(props);
this.state = {
movies: [],
filteredMovies: []
}
}
componentDidMount()
{
axios.get('https://api.themoviedb.org/3/movie/top_rated?api_key={0}&language=en-US&page=1')
.then(response => {
this.setState({
movies : response.data.results,
filteredMovies : response.data.results
});
});
}
componentDidUpdate()
{
???
}
searchBy = (columnSearch, evt) =>
{
console.log(columnSearch);
let movie = this.state.movies;
if (evt!==undefined && evt.target.value !== undefined && evt.target.value.length > 0) {
movie = movie.filter(function(i) {
console.log(columnSearch);
return i[columnSearch].toString().match( evt.target.value );
});
}
this.setState({
filteredMovies: movie
});
}
render(){
const movieRows =( this.state.filteredMovies ===[] || this.state.filteredMovies === undefined)?[]:
this.state.filteredMovies.map( (rowData, index) => <Movie key ={index} {...rowData} />);
if(movieRows !== [] && movieRows !== undefined && movieRows !== null)
return (
<div className="table">
<div className="header row">
<div className="headercenter">Movie Id
<input type="text" onChange={(evt)=>this.searchBy('id',evt)}/>
</div>
<div className="headercenter"> Title <input type="text" onChange={(evt)=>this.searchBy('title',evt)}/></div>
{/* <div className="" onClick={() => this.sortBy('overview')}>OverView</div> */}
<div className="headercenter" >Popularity<input type="text" onChange={(evt)=>this.searchBy('popularity',evt)}/></div>
<div className="headercenter" >Rating<input type="text" onChange={(evt)=>this.searchBy('vote_average',evt)}/></div>
</div>
<div className="body">
{movieRows}
</div>
</div>
) ;
};
}
export default App;
const movie = (props) => (
<div className="row">
<div>{props.id}</div>
<div>{props.title}</div>
{/* <div>{props.overview}</div> */}
<div>{props.popularity}</div>
<div>{props.vote_average}</div>
</div>
);
export default movie;

ComponentDidUpdate called when any state update take places inside the component, so you can put search method inside ComponentDidUpdate or any method but normally we do things inside ComponentDidUpdate which do require some state change and also do not require user input. For any searching you must be requiring user input so I don't recommend that you should put search method inside ComponentDidUpdate instead put that method on onChange event handler.
Note: ComponentDidUpdate if not handled correctly i.e., you update any state inside this without any condition will cause Infinite Loop

IF you want to call your api in componentDidUpdate method. You should use if condition. Because when your dom is update this method will trigger.
It should look like something like this. I do not know your logic. Please change it for your use case.
componentDidUpdate(prevProps, prevState){
if(prevProps.movies.lenght != prevState.movies.lenght){
// call api
}
}

You don't need to put the searching function in componentDidUpdate, as it will be automatically called when the input field changes. The componentDidUpdate function will be called when the component rerenders (for example after state changes).
What you want, is to call the search function when the input in the input field changes, for which I would add keyup event like so:
<!DOCTYPE html>
<html>
<body>
<p>Write in the text field</p>
<input type="text" id="mySelect" />
<p>Down here will show what you write</p>
<p id="demo"></p>
<script>
document.getElementById('mySelect').addEventListener('keyup', search)
function search(event) {
console.log(event.target.value)
document.getElementById('demo').innerText = event.target.value
}
</script>
</body>
</html>

Related

setState second argument callback function alternative in state hooks

I made a code sandbox example for my problem: https://codesandbox.io/s/react-form-submit-problem-qn0de. Please try to click the "+"/"-" button on both Function Example and Class Example and you'll see the difference. On the Function Example, we always get the previous value while submitting.
I'll explain details about this example below.
We have a react component like this
function Counter(props) {
return (
<>
<button type="button" onClick={() => props.onChange(props.value - 1)}>
-
</button>
{props.value}
<button type="button" onClick={() => props.onChange(props.value + 1)}>
+
</button>
<input type="hidden" name={props.name} value={props.value} />
</>
);
}
It contains two buttons and a numeric value. User can press the '+' and '-' button to change the number. It also renders an input element so we can use it in a <form>.
This is how we use it
class ClassExample extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 1,
lastSubmittedQueryString: ""
};
this.formEl = React.createRef();
}
handleSumit = () => {
if (this.formEl.current) {
const formData = new FormData(this.formEl.current);
const search = new URLSearchParams(formData);
const queryString = search.toString();
this.setState({
lastSubmittedQueryString: queryString
});
}
};
render() {
return (
<div className="App">
<h1>Class Example</h1>
<form
onSubmit={event => {
event.preventDefault();
this.handleSumit();
}}
ref={ref => {
this.formEl.current = ref;
}}
>
<Counter
name="test"
value={this.state.value}
onChange={newValue => {
this.setState({ value: newValue }, () => {
this.handleSumit();
});
}}
/>
<button type="submit">submit</button>
<br />
lastSubmittedQueryString: {this.state.lastSubmittedQueryString}
</form>
</div>
);
}
}
We render our <Counter> component in a <form>, and want to submit this form right after we change the value of <Counter>. However, on the onChange event, if we just do
onChange={newValue => {
this.setState({ value: newValue });
this.handleSubmit();
}}
then we won't get the updated value, probably because React doesn't run setState synchronously. So instead we put this.handleSubmit() in the second argument callback of setState to make sure it is executed after the state has been updated.
But in the Function Example, as far as I know in state hooks there's nothing like the second argument callback function of setState. So we cannot achieve the same goal. We found out two workarounds but we are not satisfied with either of them.
Workaround 1
We tried to use the effect hook to listen when the value has been changed, we submit our form.
React.useEffect(() => {
handleSubmit();
}, [value])
But sometimes we need to just change the value without submitting the form, we want to invoke the submit event only when we change the value by clicking the button, so we think it should be put in the button's onChange event.
Workaround 2
onChange={newValue => {
setValue(newValue);
setTimeout(() => {
handleSubmit();
})
}}
This works fine. We can always get the updated value. But the problem is we don't understand how and why it works, and we never see people write code in this way. We are afraid if the code would be broken with the future React updates.
Sorry for the looooooooong post and thanks for reading my story. Here are my questions:
How about Workaround 1 and 2? Is there any 'best solution' for the Function Example?
Is there anything we are doing wrong? For example maybe we shouldn't use the hidden input for form submitting at all?
Any idea will be appreciated :)
Can you call this.handleSubmit() in componentDidUpdate()?
Since your counter is binded to the value state, it should re-render if there's a state change.
componentDidUpdate(prevProps, prevState) {
if (this.state.value !== prevState.value) {
this.handleSubmit();
}
}
This ensure the submit is triggered only when the value state change (after setState is done)
It's been a while. After reading React 18's update detail, I realize the difference is caused by React automatically batching state updates in function components, and the "official" way to get rid of it is to use ReactDOM.flushSync().
import { flushSync } from "react-dom";
onChange={newValue => {
flushSync(() => {
setValue(newValue)
});
flushSync(() => {
handleSubmit();
});
}}

How to listen to localstorage value changes in react?

I want to show a button when user is logged.If user is not logged then I m not showing button.When user logged i will set local storage values.when i set local storage in login Component,Header component must listen to that event and show the button.I m using addEventListener for listening.But its not listening.
I don't know where to listen in header Component.
// HeaderComponent(header.js):
class HeaderComponent extends Component {
componentDidMount(){
if(typeof window!='undefined'){
console.log(localStorage.getItem("token"));
window.addEventListener("storage",function(e){
this.setState({ auth: true});
})
}
}
render() {
return (
<div className="header">
<div className="container">
<div className="header-content">
<img src={logo} alt="logo"></img>
<div className="nav-links" >
<ul >
<li>Home</li>
<li>About</li>
<li>Services</li>
<li><NavLink activeClassName="active" to="/upload" >Upload</NavLink></li>
<li><NavLink activeClassName="active" to="/signup"> Sign Up</NavLink></li>
{ this.state.auth? <li onClick={this.onLogout}>Logout</li> :null}
</ul>
</div>
</div>
</div>
</div>
);
}
}
//loginComponent(login.js)
class LoginComponent extends Component {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
}
onSubmit(event) {
const data = {
username: document.getElementById('name').value,
password: document.getElementById('password').value
}
axios.post(`http://localhost:4000/user/login`, data).then(res => {
this.props.history.push("/");
localStorage.setItem("token",res.data.token);
localStorage.setItem("auth",true);
}).catch(err => console.log(err));
}
render() {
return (
<section class="log-in">
<div class="card-col">
<form>
<h3>LOG IN</h3>
<div class="form-controls">
<input id="name" type="text" placeholder="username" class="input"></input>
</div>
<div class="form-controls">
<input id="password" type="password" placeholder="password" class="input"></input>
</div>
<button type="submit" onClick={this.onSubmit} class="button" >Log in</button>
</form>
</div>
</section>
)
}
}
The current answers are overlooking a really simple and secure option: window.dispatchEvent.
Where you set your localStorage item, if you dispatch an event at the same time then the eventListener in the same browser tab (no need to open another or mess with state) will also pick it up:
const handleLocalStorage = () => {
window.localStorage.setItem("isThisInLocalStorage", "true");
window.dispatchEvent(new Event("storage"));
};
window.addEventListener('storage', () => {
console.log("Change to local storage!");
// ...
})
EDIT:
Because this seems to be helpful, I'd also recommended checking out the useLocalStorage hook from the usehooks-ts team. You don't need to install it as a package; you can just copy the hook wholesale. This hook makes use of the solution I originally shared, but adds a whole lot more sophisticated logic to it.
Please take note of two things
storage event works only when the same application opened in two browser tabs (it is used to exchange info between different tabs of the same app). Storage event will not fire when both components shown on the same page.
When adding event listerner, you're passing function(), not array function. function() doe not capture this so you should explicitly bind(this) or change it to arrow function.
For example
window.addEventListener("storage",(function(e){
this.setState({ auth: true});
}).bind(this));
Or do with arrow function
window.addEventListener("storage",(e) => {
this.setState({ auth: true});
});
Here is simple example.
Be sure to open it in two tabs (the same link). Store value in one tab and see this value in another tab.
I found a really bad hack to accomplish this:
I have a Toolbar and a Login Component where the Toolbar component listens to changes in localStorage and displays the logged-in user name when the Login Component updates local storage if authentication is successful.
The Toolbar Component
(similar to the Header component in your case)
const [loggedInName, setLoggedInName] = useState(null);
useEffect(() => {
console.log("Toolbar hi from useEffect")
setLoggedInName(localStorage.getItem('name') || null)
window.addEventListener('storage', storageEventHandler, false);
}, []);
function storageEventHandler() {
console.log("hi from storageEventHandler")
setLoggedInName(localStorage.getItem('name') || null)
}
function testFunc() {
console.log("hi from test function")
storageEventHandler();
}
Add a hidden button to your Toolbar component. This hidden button will call the testFunc() function when clicked which will update the logged-in user's name as soon as local storage is updated.
<button style={{ display: 'none' }} onClick={testFunc} id="hiddenBtn">Hidden Button</button>
Now, in your Login component
.
.
.
//login was successful, update local storage
localStorage.setItem("name",someName)
//now click the hidden button using Javascript
document.getElementById("hiddenBtn").click();
.

React form input won't let me change value

I have a component in a React class in my Laravel project which is a simple form with one input field. It houses a phone number which I have retrieved from the database and passed back through the reducer and into the component as a prop. Using this, I have passed it through to the module as a prop which then populates the field with the currently saved value:
<OutOfOfficeContactNumberForm
show={props.showOutOfOffice}
value={props.outOfOfficeNumber}
handleChange={console.log("changed")}
/>
I have a handleChange on here which is supposed to fire a console log, but it only ever displays on page load. Here is my form module class:
class OutOfOfficeContactNumberForm extends React.Component {
render() {
const { show, value, handleChange } = this.props;
if(!show) return null;
return (
<div>
<p>
Please supply an Out of Office contact number to continue.
</p>
<InputGroup layout="inline">
<Label layout="inline" required={true}>Out of Office Contact Number</Label>
<Input onChange={handleChange} value={value} layout="inline" id="out-of-office-number" name="out_of_office_contact_number" />
</InputGroup>
</div>
);
}
}
export default (CSSModules(OutOfOfficeContactNumberForm, style));
The form is embedded in my parent component, as follows:
return (
<SectionCategoriesSettingsForm
isSubmitting={this.state.isSubmitting}
page={this.props.page}
show={this.props.show}
categories={this.props.categories}
submitSectionCategoriesSettings={this._submit.bind(this, 'add')}
updateSelectedCategories={this._updateSelectedCategories.bind(this)}
selectedCategoryIds={this.state.selectedCategoryIds}
storedUserCategories={this.props.selectedCategories}
outOfOfficeNumber={this.state.outOfOfficeNumber}
onUpdateContactNumber={this._updateContactNumber.bind(this)}
/>
);
In my componentWillReceiveProps() function, I set the state as follows:
if (nextProps.selectedCategories && nextProps.selectedCategories.length > 0) {
this.setState({
outOfOfficeNumber: nextProps.outOfOfficeNumber,
selectedCategoryIds: nextProps.selectedCategories.map(c => c.id)
});
}
I'm pretty sure the reason it's not changing is because it's pre-loaded from the state which doesn't change - but if I cannot edit the field how can I get it to register a change?
EDIT: Just to clarify there are also checkboxes in this form for the user to change their preferences, and the data retrieved for them is set the same way but I am able to check and uncheck those no problem
Changes:
1- onChange expect a function and you are assigning a value that's why, put the console statement inside a function and pass that function toOutOfOfficeContactNumberForm component , like this:
handleChange={() => console.log("changed")}
2- You are using controlled component (using the value property), so you need to update the value inside onChange function otherwise it will not allow you to change means input values will not be not reflect in ui.
Check example:
class App extends React.Component {
state = {
input1: '',
input2: '',
}
onChange = (e) => this.setState({ input2: e.target.value })
render() {
return(
<div>
Without updating value inside onChange
<input value={this.state.input1} onChange={console.log('value')} />
<br />
Updating value in onChange
<input value={this.state.input2} onChange={this.onChange} />
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'))
<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' />
I think the best way is when you get data from database put it to state and pass the state to input and remember if you want to see input changes in typing, use a function to handle the change and that function should change state value.
class payloadcontainer extends Component {
constructor(props) {
super(props)
this.state = {
number:1
}
}
render() {
return (
<div>
<input value={this.state.number} onChange={(e)=>this.setState({number:e.target.value})}></input>
<button onClick={()=>this.props.buyCake(this.state.number)}><h3>buy {this.state.number} cake </h3></button>
</div>
)
}
}

Error 'ref is not a prop' when using ref on a div inside of a react component

So the main aim of me using refs is so that I can reset the scroll position of a scrollable div, this is an image of the div before adding content this is how it looks before dynamically adding divs to the scrollable container div
This is a screenshot of the div after adding boxes to it:
the box is created outside of viewport and is created at the top of the scrollable area
So to be able to maintain the viewport at the top of the scrollable area I am hoping to use refs to do ReactDOM.findDOMNode(this.songIdWrapper) and then manipulate the scrollTop or use scrollTo methods.
Please find the code snippet below:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
class AddPlaylist extends Component {
constructor(props){
super(props);
this.state = {
displaySearch: false,
id: '',
playlistName:'',
playlistTitle:'',
songs:[]
}
this.handleIdSubmit = this.handleIdSubmit.bind(this);
this.handleIdChange = this.handleIdChange.bind(this);
this.handleNamechange = this.handleNamechange.bind(this);
this.handleNameSubmit= this.handleNameSubmit.bind(this);
this.callback=this.callback.bind(this);
}
componentWillUpdate () {
console.log(ReactDOM.findDOMNode(this.songIdWrapper));
}
componentDidUpdate () {
}
callback (songId) {
this.songIdWrapper=songId;
}
render () {
return(
<div className='add-playlist-wrapper'>
<div className='form-wrapper container'>
<form onSubmit={this.handleNameSubmit} className='playlist-name-wrapper'>
<input className={this.state.submittedName ? 'hide-input' : ''} required onChange={this.handleNamechange} value={this.state.playlistName} placeholder='Playlist title'/>
{this.state.submittedName ? <p className='title'>{this.state.playlistTitle}</p> : null}
</form>
<form onSubmit={this.handleIdSubmit} className='add-id-wrapper'>
<div className='input-add-playlist'>
<input required onChange={this.handleIdChange} value={this.state.id} placeholder='Add song...'/>
<button type='submit' className='fabutton'>
<i className="add-button fa fa-plus-square-o fa-3x" aria-hidden="true"></i>
</button>
</div>
</form>
<div id='song-id-wrapper' ref={this.callback}>
{this.state.songs.map((song, i) => {
return (<div key={i} className='song'>
<p>{song}</p>
</div>
)
})}
</div>
</div>
</div>
)
}
handleIdSubmit (event) {
event.preventDefault();
const newState = this.state.songs.slice();
newState.push(this.state.id);
this.setState({
songs:newState
})
}
handleIdChange (event) {
this.setState({
id: event.target.value
})
}
handleNamechange (event) {
this.setState({
playlistName: event.target.value
})
}
handleNameSubmit (event) {
event.preventDefault();
this.setState({
playlistTitle: this.state.playlistName
})
}
}
export default AddPlaylist;
The error message I get is:
this is the error message stating that ref is not a prop
So I am quite new to react and as far as I'm aware this is an attribute on a div element not passed as a prop to a component. So I hope you can see my confusion as when I search google/stack-overflow I see a lot of comments relating to child components. I am fully aware string refs have been depreciated and that callbacks should be used but no matter what I try I cannot get rid of this error message.
Any help would be greatly appreciated.
I guess the issue is that you try to access the ref in the componentWillUpdate Hook. Because from the pure setup there is nothing wrong.
The componentWillUpdate Hook gets actually called before the next render cycle, which means you access the ref BEFORE your component gets rendered, which means that you always access the ref from the render cycle before. The ref gets updated with the next render cycle.
https://developmentarc.gitbooks.io/react-indepth/content/life_cycle/update/tapping_into_componentwillupdate.html
I think you should do the scroll position handling AFTER the component did update, not before it will update!

Reactjs: Callback for dangerouslySetInnerHTML complete

I'm currently having a React Component like this:
<div id="product-content" className="bz">
<a className="anchor" id="content-anchor"></a>
<h2 className="title-section">Thông tin sản phẩm</h2>
<div className="fixed-menu-content">
<div className="container">
<ul>
<li>Thông tin sản phẩm</li>
<li>Video sản phẩm</li>
<li>Đánh giá (19)</li>
<li>Hướng dẫn mua hàng</li>
</ul>
</div>
</div>
<div dangerouslySetInnerHTML={{__html: description}}></div>
</div>
It seems that dangerouslySetInnerHTML doesn't impact to Component Lifecycle. I put this line in componentDidMount, but it return wrong result:
let b = $("#product-content").height(); // wrong: 600, true: 6500
If I try to run above line in console dev tool, it returns true result because component has been rendered completely.
How can I trigger callback for dangerouslySetInnerHTML?
It looks like the DOMSubtreeModified event has been deprecated in favor of the Mutation Observer API.
You could refactor the code proposed by lustoykov to use the new API:
class ContentRenderer extends React.Component {
componentDidMount() {
this.observer = new MutationObserver(this.handleMutation);
this.observer.observe(this.myElement, {
// Check config in https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
childList: true,
attributes: true,
characterData: true
});
}
componentWillUnmount() {
this.observer.disconnect();
}
handleMutation() {
console.log('mutation');
}
render() {
return (
<div
dangerouslySetInnerHTML={{ __html: "<div>Test</div>" }}
ref={(myElement) => { this.myElement = myElement; }}
/>
);
}
}
There isn't a callback for completion of dangerouslySetInnerHTML yet, you'll have to resort to refs and DOMSubtreeModified.
// use ref to get the DOM Node
<div
dangerouslySetInnerHTML={{__html: description}}
ref={myElement => this.myElement = myElement}
>
</div>
// Listen for changes inside the DOM Node
componentDidMount() {
this.myElement.addEventListener('DOMSubtreeModified', () => {
// execute your logic here...
});
}
// Don't forget to clean up the listener
componentWillUnmount() {
this.myElement.removeEventListener('DOMSubtreeModified');
}
PS.
Be very careful with this event (DOMSubtreeModified) it is easy to cause an infinite loop if you decide to change the DOM inside the event handler.

Resources