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.
Related
I m building an appointment application with react native,its my first try in RC,Using react-redux to manage the state.I need some clarification about lifecycle methods.
Details.js
class Details extends Component {
constructor(props) {
super(props);
}
componentDidMount(){
this.props.bookingAndSuggestions(Data)
}
componentWillReceiveProps(nextProps){
if( Object.keys(nextProps.bookingSuggestionStatus).length >0) {
if(nextProps.bookingSuggestionStatus.res.data.status=='available') {
this.setState({isAvailable:false})
} else {
this.setState({isAvailable:true})
}} }
onBookNow=()=>{
this.props.shops(Data);
}
Here is the deal,Initially i call react-redux action prop this.props.bookingAndSuggestions(Data) and i capture the response inside componentWillReceiveProps,and On booking this.props.shops(Data); will trigger and it also updates the componentWillrecieveprops,the logic inside the componentwillrecieveprops updates each time when props changes.What is the proper approach to deal with such situation?
componentWillReceiveProps is called not only when the props change but also when the parent re-renders and hence whenever it is being called the state is being evaulated and set again.
You have two options
If you aren't modifying the isAvailable state internally, you could simply use it from props directly.
Eg:
const isAvailable = this.props.bookingSuggestionStatus && this.props.bookingSuggestionStatus.res.data.status=='available'
If you are modifying it, then you need to check if the props have changed which you can do in componentWillReceiveProps(use getDerivedStateFromProps from v16.3.0 onwards)
Eg:
componentWillReceiveProps(nextProps){
if(!_.isEqual(nextProps.bookingSuggestionStatus, this.props.bookingSuggestionStatus) && Object.keys(nextProps.bookingSuggestionStatus).length >0) {
if(nextProps.bookingSuggestionStatus.res.data.status=='available') {
this.setState({isAvailable:false})
} else {
this.setState({isAvailable:true})
}
}
}
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>)
}
}
When an error is thrown in our React 16 codebase, it is caught by our top-level error boundary. The ErrorBoundary component happily renders an error page when this happens.
Where the ErrorBoundary sits
return (
<Provider store={configureStore()}>
<ErrorBoundary>
<Router history={browserHistory}>{routes}</Router>
</ErrorBoundary>
</Provider>
)
However, when navigating back using the browser back button (one click), the URL changes in the address but the page does not update.
I have tried shifting the error boundary down the component tree but this issue persists.
Any clues on where this issue lies?
The op has probably found a resolution by now, but for the benefit of anyone else having this issue I'll explain why I think its happening and what can be done to resolve it.
This is probably occurring due to the conditional rendering in the ErrorBoundary rendering the error message even though the history has changed.
Although not shown above, the render method in the ErrorBoundary is probably similar to this:
render() {
if (this.state.hasError) {
return <h1>An error has occurred.</h1>
}
return this.props.children;
}
Where hasError is being set in the componentDidCatch lifecycle method.
Once the state in the ErrorBoundary has been set it will always render the error message until the state changes (hasError to false in the example above). The child components (the Router component in this case) will not be rendered, even when the history changes.
To resolve this, make use of the react-router withRouter higher order component, by wrapping the export of the ErrorBoundary to give it access to the history via the props:
export default withRouter(ErrorBoundary);
In the ErrorBoundary constructor retrieve the history from the props and setup a handler to listen for changes to the current location using history.listen. When the location changes (back button clicked etc.) if the component is in an error state, it is cleared enabling the children to be rendered again.
const { history } = this.props;
history.listen((location, action) => {
if (this.state.hasError) {
this.setState({
hasError: false,
});
}
});
To add to jdavies' answer above, make sure you register the history listener in a componentDidMount or useEffect (using [] to denote it has no dependencies), and unregister it in a componentWillUnmount or useEffect return statement, otherwise you may run into issues with setState getting called in an unmounted component.
Example:
componentDidMount() {
this.unlisten = this.props.history.listen((location, action) => {
if (this.state.hasError) {
this.setState({ hasError: false });
}
});
}
componentWillUnmount() {
this.unlisten();
}
The analog to jdavies answer for React Router 6 is:
const { pathname } = useLocation()
const originalPathname = useRef(pathname)
useEffect(() => {
if (pathname !== originalPathname.current) {
resetErrorBoundary()
}
}, [pathname, resetErrorBoundary])
tl;dr wrap components where you expect errors with error boundaries but not the entire tree
Tried first #jdavies answer using withRouter but then found a better solution for my use case: Dan from the React-Team advised against using HOCs with Error Boundaries and rather use them at stragetic places.
In that Twitter thread is a debate around the pros and cons though and Dan left it open which way you should go but I found his thoughts convincing.
So what I did was to just wrap those strategic places where I expect an error and not the entire tree. I prefer this for my use case because I can throw more expressive, specific error pages than before (something went wrong vs there was an auth error).
jdavies comment is the way to go,
but, if you are confused by this, basically you make it look like this:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
const { history } = props;
history.listen((location, action) => {
if (this.state["hasError"]) {
this.setState({
hasError: false,
});
}
});
this.state = { hasError: false };
}
...
then at the end of the file you add:
export default withRouter(ErrorBoundary);
(don't forget to import { withRouter } from "react-router-dom"; at top)
also, if you were using e.g. export class ErrorBoundry ... like i was, don't forget to change the import { ErrorBoundary } from "./ErrorBoundry"; to import ErrorBoundary from "./ErrorBoundry"; wherever you use it e.g. App.tsx
I'm attempting to make a nice ApiWrapper component to populate data in various child components. From everything I've read, this should work: https://jsfiddle.net/vinniejames/m1mesp6z/1/
class ApiWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
response: {
"title": 'nothing fetched yet'
}
};
}
componentDidMount() {
this._makeApiCall(this.props.endpoint);
}
_makeApiCall(endpoint) {
fetch(endpoint).then(function(response) {
this.setState({
response: response
});
}.bind(this))
}
render() {
return <Child data = {
this.state.response
}
/>;
}
}
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {
data: props.data
};
}
render() {
console.log(this.state.data, 'new data');
return ( < span > {
this.state.data.title
} < /span>);
};
}
var element = < ApiWrapper endpoint = "https://jsonplaceholder.typicode.com/posts/1" / > ;
ReactDOM.render(
element,
document.getElementById('container')
);
But for some reason, it seems the child component is not updating when the parent state changes.
Am I missing something here?
There are two issues with your code.
Your child component's initial state is set from props.
this.state = {
data: props.data
};
Quoting from this SO Answer:
Passing the intial state to a component as a prop is an anti-pattern
because the getInitialState (in our case the constuctor) method is only called the first time the
component renders. Never more. Meaning that, if you re-render that
component passing a different value as a prop, the component
will not react accordingly, because the component will keep the state
from the first time it was rendered. It's very error prone.
So if you can't avoid such a situation the ideal solution is to use the method componentWillReceiveProps to listen for new props.
Adding the below code to your child component will solve your problem with Child component re-rendering.
componentWillReceiveProps(nextProps) {
this.setState({ data: nextProps.data });
}
The second issue is with the fetch.
_makeApiCall(endpoint) {
fetch(endpoint)
.then((response) => response.json()) // ----> you missed this part
.then((response) => this.setState({ response }));
}
And here is a working fiddle: https://jsfiddle.net/o8b04mLy/
If the above solution has still not solved your problem I'll suggest you see once how you're changing the state, if you're not returning a new object then sometimes react sees no difference in the new previous and the changed state, it's a good practice to always pass a new object when changing the state, seeing the new object react will definitely re-render all the components needing that have access to that changed state.
For example: -
Here I'll change one property of an array of objects in my state, look at how I spread all the data in a new object. Also, the code below might look a bit alien to you, it's a redux reducer function BUT don't worry it's just a method to change the state.
export const addItemToCart = (cartItems,cartItemToBeAdded) => {
return cartItems.map(item => {
if(item.id===existingItem.id){
++item.quantity;
}
// I can simply return item but instead I spread the item and return a new object
return {...item}
})
}
Just make sure you're changing the state with a new object, even if you make a minor change in the state just spread it in a new object and then return, this will trigger rendering in all the appropriate places.
Hope this helped. Let me know if I'm wrong somewhere :)
There are some things you need to change.
When fetch get the response, it is not a json.
I was looking for how can I get this json and I discovered this link.
By the other side, you need to think that constructor function is called only once.
So, you need to change the way that you retrieve the data in <Child> component.
Here, I left an example code: https://jsfiddle.net/emq1ztqj/
I hope that helps.
Accepted answer and componentWillReceiveProps
The componentWillReceiveProps call in accepted answer is deprecated and will be removed from React with version 17 React Docs: UNSAFE_componentWillReceiveProps()
Using derived state logic in React
As the React docs is pointing, using derived state (meaning: a component reflecting a change that is happened in its props) can make your components harder to think, and could be an anti-pattern. React Docs: You Probably Don't Need Derived State
Current solution: getDerivedStateFromProps
If you choose to use derived state, current solution is using getDerivedStateFromProps call as #DiogoSanto said.
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing. React Docs: static getDerivedStateFromProps()
How to use componentWillReceiveProps
This method can not access instance properties. All it does describing React how to compute new state from a given props. Whenever props are changed, React will call this method and will use the object returned by this method as the new state.
class Child extends React.Component {
constructor() {
super(props);
// nothing changed, assign the state for the
// first time to teach its initial shape.
// (it will work without this, but will throw
// a warning)
this.state = {
data: props.data
};
}
componentWillReceiveProps(props) {
// return the new state as object, do not call .setState()
return {
data: props.data
};
}
render() {
// nothing changed, will be called after
// componentWillReceiveProps returned the new state,
// each time props are updated.
return (
<span>{this.state.data.title}</span>
);
}
}
Caution
Re-rendering a component according to a change happened in parent component can be annoying for user because of losing the user input on that component.
Derived state logic can make components harder to understand, think on. Use wisely.
I'm trying to separate a presentational component from a container component. I have a SitesTable and a SitesTableContainer. The container is responsible for triggering redux actions to fetch the appropriate sites based on the current user.
The problem is the current user is fetched asynchronously, after the container component gets rendered initially. This means that the container component doesn't know that it needs to re-execute the code in its componentDidMount function which would update the data to send to the SitesTable. I think I need to re-render the container component when one of its props(user) changes. How do I do this correctly?
class SitesTableContainer extends React.Component {
static get propTypes() {
return {
sites: React.PropTypes.object,
user: React.PropTypes.object,
isManager: React.PropTypes.boolean
}
}
componentDidMount() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
render() {
return <SitesTable sites={this.props.sites}/>
}
}
function mapStateToProps(state) {
const user = userUtils.getCurrentUser(state)
return {
sites: state.get('sites'),
user,
isManager: userUtils.isManager(user)
}
}
export default connect(mapStateToProps)(SitesTableContainer);
You have to add a condition in your componentDidUpdate method.
The example is using fast-deep-equal to compare the objects.
import equal from 'fast-deep-equal'
...
constructor(){
this.updateUser = this.updateUser.bind(this);
}
componentDidMount() {
this.updateUser();
}
componentDidUpdate(prevProps) {
if(!equal(this.props.user, prevProps.user)) // Check if it's a new user, you can also use some unique property, like the ID (this.props.user.id !== prevProps.user.id)
{
this.updateUser();
}
}
updateUser() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
Using Hooks (React 16.8.0+)
import React, { useEffect } from 'react';
const SitesTableContainer = ({
user,
isManager,
dispatch,
sites,
}) => {
useEffect(() => {
if(isManager) {
dispatch(actions.fetchAllSites())
} else {
const currentUserId = user.get('id')
dispatch(actions.fetchUsersSites(currentUserId))
}
}, [user]);
return (
return <SitesTable sites={sites}/>
)
}
If the prop you are comparing is an object or an array, you should use useDeepCompareEffect instead of useEffect.
componentWillReceiveProps() is going to be deprecated in the future due to bugs and inconsistencies. An alternative solution for re-rendering a component on props change is to use componentDidUpdate() and shouldComponentUpdate().
componentDidUpdate() is called whenever the component updates AND if shouldComponentUpdate() returns true (If shouldComponentUpdate() is not defined it returns true by default).
shouldComponentUpdate(nextProps){
return nextProps.changedProp !== this.state.changedProp;
}
componentDidUpdate(props){
// Desired operations: ex setting state
}
This same behavior can be accomplished using only the componentDidUpdate() method by including the conditional statement inside of it.
componentDidUpdate(prevProps){
if(prevProps.changedProp !== this.props.changedProp){
this.setState({
changedProp: this.props.changedProp
});
}
}
If one attempts to set the state without a conditional or without defining shouldComponentUpdate() the component will infinitely re-render
You could use KEY unique key (combination of the data) that changes with props, and that component will be rerendered with updated props.
componentWillReceiveProps(nextProps) { // your code here}
I think that is the event you need. componentWillReceiveProps triggers whenever your component receive something through props. From there you can have your checking then do whatever you want to do.
I would recommend having a look at this answer of mine, and see if it is relevant to what you are doing. If I understand your real problem, it's that your just not using your async action correctly and updating the redux "store", which will automatically update your component with it's new props.
This section of your code:
componentDidMount() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
Should not be triggering in a component, it should be handled after executing your first request.
Have a look at this example from redux-thunk:
function makeASandwichWithSecretSauce(forPerson) {
// Invert control!
// Return a function that accepts `dispatch` so we can dispatch later.
// Thunk middleware knows how to turn thunk async actions into actions.
return function (dispatch) {
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
};
}
You don't necessarily have to use redux-thunk, but it will help you reason about scenarios like this and write code to match.
A friendly method to use is the following, once prop updates it will automatically rerender component:
render {
let textWhenComponentUpdate = this.props.text
return (
<View>
<Text>{textWhenComponentUpdate}</Text>
</View>
)
}
You could use the getDerivedStateFromProps() lifecyle method in the component that you want to be re-rendered, to set it's state based on an incoming change to the props passed to the component. Updating the state will cause a re-render. It works like this:
static getDerivedStateFromProps(nextProps, prevState) {
return { myStateProperty: nextProps.myProp};
}
This will set the value for myStateProperty in the component state to the value of myProp, and the component will re-render.
Make sure you understand potential implications of using this approach. In particular, you need to avoid overwriting the state of your component unintentionally because the props were updated in the parent component unexpectedly. You can perform checking logic if required by comparing the existing state (represented by prevState), to any incoming props value(s).
Only use an updated prop to update the state in cases where the value from props is the source of truth for the state value. If that's the case, there may also be a simpler way to achieve what you need. See - You Probably Don't Need Derived State – React Blog.