Refreshing Component after Route Change - reactjs

I have a row of buttons that all links to a chart being rendered, then the button pressed, it decides which data will be shown on the chart below.
<div>
<Route path="/" component={Main} />
<Route path="/chart/:topic" component={Chart} />
</div>
Button element:
<Link to={"/chart/" + collection.name}>
<Button key={index} data-key={index} style={this.btnStyle}>
{this.store.capitalizeFirstLetter(collection.name)}
</Button>
</Link>
This works fine when the button is pressed for the first time. However if the user tries to change the data by pressing a different button the chart component does not refresh at all, browser shows that the URL has changed however the component does not refresh at all.
I know this is because of, I've put a console.log in the chart component and it does not come up the second time a button is pressed.
componentDidMount = () => {
const { match: { params } } = this.props;
this.topic = params.topic;
console.log("chart topic", this.topic);
this.refreshData(true);
this.forceUpdate();
this.store.nytTest(this.topic, this.startDate, this.endDate);
};
As you can see I tried to put a forceUpdate() call but that did nothing. Any help is appreciated!

It's because your component is already rendered and didn't see any change so it don't rerender.
You have to use the componentWillReceiveProps method to force the refresh of your component
Example
componentWillReceiveProps(nextProps){
if(nextProps.match.params.topic){
//Changing the state will trigger the rendering
//Or call your service who's refreshing your data
this.setState({
topic:nextProps.match.params.topic
})
}
}
EDIT
The componentWillReceiveProps method is deprecated.
Now the static getDerivedStateFromProps is prefered when you're source data are coming from a props params
Documentation
This method shoud return the new state for trigger the remounting, or null for no refresh.
Example
static getDerivedStateFromProps(props, state) {
if (props.match.params.topic && props.match.params.topic !== state.topic) {
//Resetting the state
//Clear your old data based on the old topic, update the current topic
return {
data: null,
topic: props.match.params.topic
};
}
return null;
}

componentDidMount is only called once while mounting(rendering) the component.
You should use getDerivedStateFromProps to update your component

Related

Updating Parents state from Child without triggering a rerender of Child in React

So I'm trying to build a single page app in react.
What I want:
On the page you can visit different pages like normal. On one page (index) i want a button the user can click that expands another component into view with a form. This component or form should be visible on all pages once expanded.
The Problem:
The index page loads some data from an api, so when the index component gets mounted, an fetch call is made. But when the user clicks the "Expand form"-Button, the state of the Parent component gets updated as expected, but the children get rerendered which causes the index component to fetch data again, which is not what I want.
What I tried
// Parent Component
const App => props => {
const [composer, setComposer] = useState({
// ...
expanded: false,
});
const expandComposer = event => {
event.preventDefault();
setComposer({
...composer,
expanded: true
});
return(
// ...
<Switch>
// ...
<Route
exact path={'/'}
component={() => (<Index onButtonClick={expandComposer}/>)}
// ....
{composer.expanded && (
<Composer/>
)};
);
};
// Index Component
const Index=> props => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState([]);
useEffect(()=> {
// load some data
}, []);
if(isLoading) {
// show spinner
} else {
return (
// ...
<button onClick={props.onButtonClick}>Expand Composer</button>
// ...
);
};
};
So with my approach, when the button is clicked, the Index component fetched the data again and the spinner is visible for a short time. But I dont want to remount Index, or at least reload the data if possible
Two problems here. First, React will by default re render all child components when the parent gets updated. To avoid this behavior you should explicitly define when a component should update. In class based components PureComponent or shouldComponentUpdate are the way to go, and in functional components React.memo is the equivalent to PureComponent. A PureComponent will only update when one of it's props change. So you could implement it like this:
const Index = () =>{/**/}
export default React.memo(Index)
But this won't solve your problem because of the second issue. PureComponent and React.memo perform a shallow comparison in props, and you are passing an inline function as a prop which will return false in every shallow comparison cause a new instance of the function is created every render.
<Child onClick={() => this.onClick('some param')} />
This will actually create a new function every render, causing the comparison to always return false. A workaround this is to pass the parameters as a second prop, like this
<Child onClick={this.onClick} param='some param' />
And inside Child
<button onClick={() => props.onClick(props.param)} />
Now you're not creating any functions on render, just passing a reference of this.onClick to your child.
I'm not fully familiar with your style of React, I do not use them special state functions.
Why not add a boolean in the parent state, called "fetched".
if (!fetched) fetch(params, ()=>setState({ fetched: true ));
Hope this helps
Silly me, I used component={() => ...} instead of render={() => ...} when defining the route. As explained in react router docs, using component always rerenders the component. Dupocas' answer now works perfectly :)

Component render() triggered after history.push but page is blank

I want the user to be redirected to the resources list after he deleted an item on its show page. I 've read a lot of SO Q&A on the topic, but I think I have a different issue as my routes and component got hit the right way after history.push
I tracked code execution through debugger till component render and still don't understand why nothing is returned
Here are my routes in my App component (wrapped this way<Router><App /></Router>) component :
<Route component={AppHeader} />
{["/articles/:id/edit", "/articles/new"].map((path, index) =>
<Route key={index} exact path={path} component{ArticleForm}/>
)}
<Route exact path="/articles/:id" component={Article}/>
{["/", "/articles"].map((path, index) =>
<Route key={index} exact path={path} component{ArticlesList}/>
)}
I use redux so Router and ArticleList are exported this way :
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Component))
In AppHeader component, a delete button is provided, if user is on show or edit page. When clicking on the link, following method is triggered :
class AppHeader extends Component {
...
deleteArticle = async () => {
await ajaxHelpers.ajaxCall('DELETE',`/articles/${this.state.currentArticleID}`, {}, this.state.token)
this.props.history.push("/")
}
...
}
Then Route with ArticlesList is triggered and should render this component. Here is what happens (breakpoints all the way in render methods):
URL is updated
Router is rendered
App header is rendered
Article list is rendered
Article list re-rendered with fecth from API (state populated with list)
Article list re-rendered with componentDidUpdate
BUT page stays blank ... I am using this.props.history.push("/") in other components and it works fine (list get re-rendered and displayed). See blank page and console.logs :
If I manually reload the page, it renders normally.
What is preventing any component to be displayed (DOM is nearly empty, I only get my empty <div id="root"></div>) in this case ?
Do one thing ,to displaying Article list use filter to update list state
deleteArticle = async () => {
await ajaxHelpers.ajaxCall('DELETE',`/articles/${this.state.currentArticleID}`, {}, this.state.token)
this.setState({articleList:articleList.filter((list)=> list.id!==this.state.currentArticleID)})
}
Change :
this.props.history.push("/")
To :
this.setState({articleList:articleList.filter((list)=> list.id!==this.state.currentArticleID)})

State update on unmounting component - how to fix that?

I have some problems with updating state on an unmounted component as below:
All works as expected. Modal is closed after the comment is deleted. But I'm getting a warning:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in the
componentWillUnmount method
I know what causes the problem but I don't know how to fix that. Here's my data flow:
This function invokes an action responsible for deleting user posts:
async handleDelete() {
const { articleID, comment, deleteComment } = this.props;
await deleteComment({ articleID, idcomment: comment.idcomment });
//showModal flag is set to false - hide confirmation modal after clicking 'yes' button
this.setState({showModal: !this.state.showModal})
}
Action listed above 'deleteComment' looks like this:
export const deleteComment = data => async dispatch => {
const { idcomment, articleID } = data;
try {
await axios.post(`/api/comments/${idcomment}`, {_method: 'DELETE'});
//fetchling comments again to update a state and it looks like it causes the problem with updating a state on unmounted component because when I commented out that line, it didnt happen anymore.
await dispatch(fetchComments(articleID));
}
catch(e) {
throw new Error(e) }
finally {
dispatch(setCommentStatus(true));
dispatch(decCommentCount());
}
}
Not my question is, how to fix that? I want to close my confirmation modal after the comment is deleted from the database and the new sets of comments are already updated.
Here is how I use my modal:
<Modal
showModal={showModal}
accept={this.handleDelete}
denied={() => this.setState({showModal: !this.state.showModal})}
/>
And the last one is the modal itself:
return (
!showModal
? ''
: (
<Wrapper>
<ModalSection>
<Header>
<Title>Usunięcie komentarza</Title>
<ButtonExit onClick={denied}>❌</ButtonExit>
</Header>
<Text>Czy jesteś pewien, że chcesz usunąć ten komentarz? <br /> Nie będziesz mógł cofnąć tej operacji.</Text>
<Footer>
<div>
<Button onClick={denied}>Cofnij</Button>
<Button warning onClick={accept}>Usuń</Button>
</div>
</Footer>
</ModalSection>
</Wrapper>
)
)
Apparently it works after deleting this.setState({showModal: !this.state.showModal}) from handleDelete function. Weird thing is after finishing deleting comment my modal is closing and I don't know HOW.. Something changing showModal value to false... Still figuring out.
EDIT: IM DUMB. After deleting comment I'm fetching comment list again AND that component has state of showModal set to FALSE by default so when component is rerendering the state is default which is false.................. SORRY.
It is because you are trying to modify state of Modal component after its being closed. Instead you should maintain state of comment deletion in the Parent component which renders the Modal component instead. And update the state in Parent component using a callback. This callback can be passed to Modal component as a prop and called when a delete action occurs. This should fix the issue.
You shouldn't use tenary to render the modal (!showModal ? '' : <...>) You should pass a prop to your modal that only hides or shows the modal.
If you see some libraries they have a show prop or isOpen
e.g. react-bootstrap react-modal
They have that prop so you don't have errors like the one you are having.

How do you reload an iframe with ReactJS?

My ReactJS component contains an iframe. In response to an event in the outer page, I need to reload the iframe. If the user has navigated to another page in the iframe, I need to reset it to the URL that it had when I first loaded the page. This URL is available in this.props.
I've tried using forceUpdate(). I can see that this causes the render method to run, but the iframe doesn't get reset - presumably because React can't tell that anything has changed.
At the moment I'm appending some random text to the iframe's querystring: this URL change forces React to re-render the iframe. However this feels a bit dirty: the page within the iframe is beyond my control so who knows what this extra querystring value might do?
resetIframe() {
console.log("==== resetIframe ====");
this.forceUpdate();
}
public render() {
console.log("==== render ====");
// How can I use just this.props.myUrl, without the Math.random()?
let iframeUrl = this.props.myUrl + '&random=' + Math.random().toString();
return <div>
<button onClick={() => { this.resetIframe(); }}>Reset</button>
<iframe src={iframeUrl}></iframe>
</div>
}
(I'm using TypeScript too, if that makes a difference.)
I'd create a state variable with the random, and just update it on resetIframe:
state = {
random: 0
}
resetIframe() {
this.setState({random: this.state.random + 1});
}
public render() {
return <div>
<button onClick={() => { this.resetIframe(); }}>Reset</button>
<iframe key={this.state.random} src={this.props.myUrl}></iframe>
</div>
}
Here is a fiddle working: https://codesandbox.io/s/pp3n7wnzvx
In case of Javascript frameworks and technologies, If you update the key, then it'll automatically reload the iFrame. So you just want to increase (or Random number) the value of key for your action.
state = {
iframe_key: 0,
iframe_url: 'https://www.google.com/maps' //Your URL here
}
iframeRefresh() {
this.setState({iframe_key: this.state.iframe_key + 1});
}
public render() {
return (
<div>
<button onClick={() => { this.iframeRefresh(); }}>Reload Iframe</button>
<iframe key={this.state.iframe_key} src={this.state.iframe_url}>
</iframe>
</div>
);
}
<iframe key={props.location.key} />
use This "Router location Key" each click will get random value, whenever to navigate to here this value will get change then iframe will get the refresh
You could create a new component which just loads you iframe. In your main component you load it and give it a update prop which you ignore in you iframe component. If you set the prop with a state variable, your component will reload. You can take a look at Diogo Sgrillo's answer for the setState method.
You could also check with shouldcomponentupdate() if the prop changed and only update if it changed.
Maybe a bit late here :) Just a side note: When you update state based on previous state you should pass a function to setState:
resetIframe() {
this.setState(prevState => {random: prevState.random + 1});
}
Updating the source causes a reload without causing a rerender.
import React, { useState, useRef } from 'react'
function renderIframe({src}){
const iframeRef = useRef()
return <>
<iframe ref={iframeRef} src={src} >
<span onClick={e => (iframeRef.current.src += '')}>Reload</span
</>
}

Force remounting component when React router params changing?

I've written a simple app where the remote resources are fetched inside componentDidMount functions of the components.
I'm using React Router and when the route changes completely, the previous component is unmounted well then the new one is mounted.
The issue is when the user is staying on the same route, but only some params are changed. In that case, the component is only updated. This is the default behaviour.
But it's sometimes difficult to handle the update in all the children components where previously only componentDidMount was needed...
Is there a way to force the remounting of the component when the user is staying on the same route but some params are changing?
Thanks.
Do the following
the route shoul look like this.
<Route path='/movie/:movieId' component={Movie} />
When you go to /movie/:movieID
class Movie extends Component {
loadAllData = (movieID) => {
//if you load data
this.props.getMovieInfo(movieID);
this.props.getMovie_YOUTUBE(movieID);
this.props.getMovie_SIMILAR(movieID);
}
componentDidMount() {
this.loadAllData(this.props.match.params.movieId);
}
componentWillReceiveProps(nextProps){
if(nextProps.match.params.movieId !== this.props.match.params.movieId) {
console.log(nextProps);
this.loadAllData(nextProps.match.params.movieId);
}
}
render(){
return( ... stuff and <Link to={`/movie/${id}`} key={index}>...</Link>)
}
}

Resources