Force remounting component when React router params changing? - reactjs

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>)
}
}

Related

Change in the optional parameter is not reloading the component

Below is the route :
<Route exact path="/products/version/:id/:version" component={component} />
I am trying to redirect from : http://localhost:8080/products/version/600178221/3
to
http://localhost:8080/products/version/600178221/5
using :
this.props.history.push('/products/version/600178221/5');
Issue : URL is updating, but component is not re-loading.
React won't re-render your component because only the props changed but the component that has to be rendered is still the same. React only updates what is necessary to update.
If you get your product data from a remote backend (using ajax) I recommend you use componentDidUpdate. This is also recommended in the official documentation as an alternative for getDerivedStateFromProps for side effects:
If you need to perform a side effect (for example, data fetching or an animation) in response to a change in props, use componentDidUpdate lifecycle instead.
React documentation
componendDidUpdate(prevProps) {
const { match } = this.props;
if (prevProps.match.params.id !== match.params.id || prevProps.match.params.version !== match.params.version) {
this.fetchDataFromApi();
}
}
Along with componentDidMount, You also need to implement the componentWillReceiveProps or use getDerivedStateFromProps(from v16.3.0 onwards) in Products page since the same component is re-rendered with updated params and not re-mounted when you change the route params, this is because params are passed as props to the component and on props change, React components re-render and not re-mounted.
EDIT: from v16.3.0 use getDerivedStateFromProps to set/update state based on props(no need to specify it in two different lifecyle methods)
```static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.match.params.product !== prevState.currentProductId){
const currentProductId = nextProps.match.params.product
const result = productlist.products.filter(obj => {
return obj.id === currentProductId;
})
return {
product: result[0],
currentId: currentProductId,
result
}
}
return null;
}```

React-navigation tabnavigator reloads on parents state change

I am using the tabnavigator from react navigation. I have a parent component that looks like this
changeService(serviceName){
this.setState({
});
}
render() {
const { container } = styles
return (
<View style={container}>
<Header
clientName = {this.state.project}
serviceName = {this.state.service}
cancelClick = {this.handleExitClick}
/>
<ServicesNavigator
changeService = {this.changeService}
/>
</View>
);
}
Then my service navigator export looks like this
export default (props) => {
const { screenProps, ...otherProps } = props;
const ServicesNavigator = makeServiceNavigator(props);
return <ServicesNavigator screenProps={{ ...screenProps, ...otherProps
}} />
};
The navigator itself looks like a normal navigator except it hsa navigationOptions equal to
tabBarOnPress: (scene, jumpToIndex) => {
changeService(scene.scene.route.routeName)
scene.jumpToIndex(scene.scene.index)
},
Whenever i try to change tabs the navigator is reloading completely and going back to default tab selection. Is there no way to allow the tabnavigors parents state variables to change without reseting the tab navigator?
Whenever tabBarOnPress is triggered, it requests to update state of the parent component. Then due to React component lifecycle, state update will cause parent component to re-render, along with all its children. This is why your tab navigator is reseted all the times.
There are several solutions for your case:
1 - Use shouldComponentUpdate to guide when parent component to be updated. Since you want to retain ServicesNavigator whenever changeService is called, you want to avoid re-rendering on related state items.
shouldComponentUpdate(nextProps, nextState) {
if (this.state.itemA !== nextState.itemA) {
return false; // avoid re-rendering
}
return true;
}
This is dirty but working; however it will haunt you in the long run since it causes parent component's behaviours not idempotent.
Edit: I forgot you have the Header which reads the state. If you use shouldComponentUpdate in the parent component, then Header will not receive new props. So 1st option is not correct. Sorry about this.
2 - Don't let parent component hold the state anymore. In your sample code, parent component has state only because its children components need to communicate and share data with each others. So why don't you just keep the sharing data somewhere else? Best tools can be state management libraries like mobx or redux, which can be integrated quite easily with react-navigation

Where should I place redirection code?

I have a React component that represents a page. Users should never reach the page unless they have some state. I wrote the following to have react-router redirect the user if state is missing:
class BoxScreen extends Component {
constructor(props) {
super(props);
if (Object.keys(this.props.boxState.current).length === 0) {
browserHistory.push('/secured/find')
}
I then get this message:
warning.js:36 Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
Ok, so I'll move my redirection code to componentWillMount:
class BoxScreen extends Component {
...
componentWillMount() {
if (Object.keys(this.props.boxState.current).length === 0) {
browserHistory.push('/secured/find')
}
}
render() {
// Don't want this executed or else I'll need to pollute it with null guards...
}
}
However, the issue is that the above doesn't stop render from being called, which is definitely not desired. Where is the most appropriate spot to put this so I can short-circuit the component rendering and perform the redirect?
You could use react-router's onEnter prop to accomplish that:
function checkAuthenticated(nextState, replace) {
if (!SOME_CONDITION) {
replace('/secured/find')
}
}
<Route ... onEnter={checkAuthenticated} />
But I believe you can't access the components props in this way. You'd have to keep track of the state in some other way. In case you're using Redux, I believe you could use redux-router to get the store.
You can check the Enter/leave hooks documentation
Just use react-router's <Redirect/> component inside render to achieve it, I am using v4 so I will provide you solution compatible to that.
import {Redirect} from 'react-router-dom';
class BoxScreen extends Component {
...
render() {
return this.props.condition ?
<ActualComponent /> : <Redirect to="/someGuardLink">;
}
}
Replace <ActualComponent /> with your components JSX, and yah, this should work. Your component will still mount and render but after that it is going to redirect you to some other link without you getting to notice any effect of it. This is the official way of doing it.
Expanding on what smishr4 had to say:
Have a default state for your object in the reducer. For example, my initial state for an ID is:
ID: -1
Be sure that you've mapped your object from state to props.
function mapStateToProps(state, ownProps) {
return {
ID: state.ID,
}
}
On anything that extends from React.Component, write an if condition within the componentWillMount() function - so that this happens before page load. This particular function will route me back to the homepage, if the .ID field is undefined (which is always initially true), and after it is set to its initial state in the reducer (-1).
componentWillMount() {
{
if (this.props.ID != -1 && this.props.ID != undefined) {
this.props.loadYourDataFromAPIGivenID(this.props.ID);
}
else {
this.props.history.push('/');
}
}
}
Is there a better way? Probably. You might be able to do the blocking within the router directly, but this is a good work around.
You can also use this.state directly, but this.props is a bit safer to use.

Reactjs route for asterisk executes only once

I have a single page app, I have defined all the Routes in the app to execute the same react component (using *-wildcard) when navigating to them.
it seems that the component will only execute once upon navigation.
How can I call an execution/instantiation of the component upon any change in navigation?
this is my Route jsx:
<Route path="/" component={App}>
{<IndexRoute component={TVPage} />}
{<Route path="*" component={TVPage} />}
</Route>
I assume when you say "the component only executes once" you mean it mounts only once.
Since you didn't show your code, I can only assume you have used one of the lifecycle methods: componentWillMount | componentDidMount
These methods only trigger once on Component mount. Given your Route configuration, whenever you switch to a different URL, since it's using the same component, it will not unmount and mount again (thus your loading logic is only triggered once), but simply re-render if its props have changed. That's why you should plug on a lifecycle method that is triggered on every prop change (like componentWillReceiveProps).
Try this instead:
class TVPage extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
// Load your data/state (initial)
this.props.loadMyData(this.props.whatever.data);
}
componentWillReceiveProps(nextProps) {
if (this.props.whatever.myStatus !== nextProps.whatever.myStatus) {
// Load your data/state (any page change)
nextProps.loadMyData(nextProps.whatever.data);
}
}
render() {
// Render whatever you want here
}
}
componentWillMount will trigger on mount (initial load), and componentWillReceiveProps will trigger at least every time your props change.
Look at this example for react router using query params : https://github.com/reactjs/react-router/blob/master/examples/query-params/app.js
In your componentDidMount function inside your TVPage component, I would get the data passed as params in the URL which then updates the state of the component. Every time the state changes within the component, it will reload itself.
Example component :
class TVPage extends Component {
constructor(props) {
super(props);
this.state = {
data: null
}
}
componentDidMount() {
// from the example path /TVPage/:id
let urlData = this.props.params.id;
this.setState({data: urlData})
}
render() {
return (
<div>
{this.state.data}
</div>
);
}
}

How to pass props to react-router 1.0 components?

Please note, that although the question itself is largely a duplicate of this, it concerns a different version which should support this. The linked question already accepted an answer on an old version
I'm pretty confused about what the intended workflow is.
Let's say I have a menu system where clicking on each item uses react-router to navigate to an area which pulls some data from the server.
url: yoursite/#/lists/countries
----------------------------
Billing Codes | <<Countries>> | Inventory Types
---------------------------------------------------------
Countries:
---------------
Afghanistan
Azerbaijan
Belarus
with routes something like
Route #/lists component: Lists
Route billing-codes component: BillingCodes
Route countries component: Countries
Route inventory-types component: InventoryTypes
I don't want to preload data from the server until an area is navigated to, so in my Countries component's on componentWillMount I fire an event (I'm using reflux but...whatever) that triggers a store to do an ajax request and update itself with the current list of countries.
Now the Countries component reacts to that change in state by updating the countries in its props. Except - reasonably - that generates an invariant error because I shouldn't be updating props on a child component, I should update it at the top level. But the top level is the router itself so now I'm just lost - where am I supposed to listen to changes and update props from?
(Cross-posted to the issue tracker as I think it needs some clearer documentation)
Reading the react-router 0.13 -> 1.0 Upgrade Guide and this example led me to the following:
{ this.props.children &&
React.cloneElement(this.props.children, {newprop: this.state.someobject }) }
Instead of including the child components directly, we clone them and inject the new properties. (The guard handles the case where there is no child component to be rendered.) Now, when the child component renders, it can access the needed property at this.props.newprop.
The easy way is to just use this.state, but if you absolutely have to use this.props then you should probably extend Router.createElement.
First add the createElement prop to your Router render.
React.render(
<Router history={history} children={Routes} createElement={createElement} />,
document.getElementById('app')
);
Wrap all of your components in a new Container component.
function createElement(Component, props) {
return <Container component={Component} routerProps={props} />;
}
Your Container component will probably look something like this. Pass an onLoadData function down to your component.
import React from 'react';
import _ from 'lodash';
class Container extends React.Component {
constructor(props) {
super(props);
this.state = { props: props.routerProps };
}
onLoadData(props) {
var mergedProps = _.merge(this.state.props, props);
this.setState({ props: mergedProps });
}
render() {
var Component = this.props.component;
return <Component {...this.state.props} onLoadData={this.onLoadData.bind(this)} />;
}
}
Then from your loaded component, when you get your data back from the server, just fire this.props.onLoadData(data) and the data will be merged with your current props.
Read the Router.createElement Documentation

Resources