Browser navigation broken by use of React Error Boundaries - reactjs

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

Related

componentDidMount always called before react render

I am new to react world, I tried to fetch user data from axios call, and tried to get the data before the react's render executed.
How I call this component
<Route path="/users" render={(props) => <User {...props}/>} />
Here is my component class
class User extends React.Component {
constructor(props) {
super(props);
this.state = { authenticated: false }
this.getCurrentUser = this.getCurrentUser.bind(this);
}
componentDidMount(){
console.log("componentDidMount");
this.getCurrentUser();
}
getCurrentUser(){
Api.getCurrentUser().then(response => {
if (response) {
console.log(response.data.username);
this.setState({authenticated: true});
}
}).catch(error =>{
this.setState({authenticated: false});
}
}
render() {
console.log(this.state.authenticated);
if (this.state.authenticated === false) {
return <Redirect to={{ pathname: '/login' }}/>
}
return (
<div><Page /> <div>
);
}
}
export default User;
The console.log sequence is
false
componentDidMount
user_one
Warning: Can't perform a React state update on an unmounted component. This is a no-op
The warning makes sense because react already redirect me to login so the user component is not mounted.
I don't understand why componentDidMount is not called before render, because it supposes to change the defaultState through this.setState()...
What am I missing here?
ComponentDidMount works the way you described it. It runs immediately after the component is rendered. What you can do is to wrap your Component with a parent component where you have the API call and pass on the isAuthenticated as props to .
Docs for reference
As #user2079976 rightly stated, your usage of componentDidMount is correct & it behaves the way it is intended to, but I think you might be getting the wrong impression due to your code execution workflow.
Problem Reason
This issue/warning is generally something to go with when you're updating a component that has unmounted, in your case it's likely the redirect that happens before your api return a result.
More Details:
Not having the full code sample, I had to guess a few of the variables in your setup & I'm unable to get the exact issue on my JsFiddle as you've explained (I think JsFiddle/react.prod swallows the warning messages), but... I'll try to update this fiddle to explain it as much as I can with comments.
// at render this is still false as `state.authenticated`
// only becomes true after the redirect.
// the code then redirects....
// then updates the not yet mounted component & its state
// which is causing the warning
if (this.state.authenticated === false) {
return (<Redirect to={{ pathname: '/about' }}/>)
}
return (<div>On Home</div>);
Possible Solution
Rather do your auth/logged-in (state) to a higher level/parent component, and have the router decide where to send the user.
We have used this exact example in one of our apps (which is an implementation of the above suggestion). It works well for an auth type workflow & is straight from the docs of the Router lib you're using :P https://reactrouter.com/web/example/auth-workflow

React-Router <Link> to load a page, even if we're already on that page

I use react router in my website. I have homepage and contact.
When I am on homepage and I click on the homepage again. How can I reload the page like Facebook
issue here: https://github.com/ReactTraining/react-router/issues/1982
After looking at the issue, I can realize that you may use force update:
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
this.forceUpdate();
}
}
You could set up an event handler on the 'home' Link, that will fetch the data you want to be updated.
class Nav extends React.Component {
constructor () {
super()
this.handleClick = this.handleClick.bind(this)
}
handleClick () {
// code that fetches Home Page data
}
render () {
return (
<Link to='your/path' onClick={this.handleClick}>Home</Link>
)
}
}
This might be a common problem and I was looking for a decent solution to have in my toolbet for next time. React-Router provides some mechanisms to know when an user tries to visit any page even the one they are already.
Reading the location.key hash, it's the perfect approach as it changes every-time the user try to navigate between any page.
componentDidUpdate (prevProps) {
if (prevProps.location.key !== this.props.location.key) {
this.setState({
isFormSubmitted: false,
})
}
}
Reference: A location object is never mutated so you can use it in the lifecycle hooks to determine when navigation happens

How can I make use of Error boundaries in functional React components?

I can make a class an error boundary in React by implementing componentDidCatch.
Is there a clean approach to making a functional component into an error boundary without converting it into a class?
Or is this a code smell?
As of v16.2.0, there's no way to turn a functional component into an error boundary.
The React docs are clear about that, although you're free to reuse them as many times as you wish:
The componentDidCatch() method works like a JavaScript catch {} block, but for components. Only class components can be error boundaries. In practice, most of the time you’ll want to declare an error boundary component once and use it throughout your application.
Also bear in mind that try/catch blocks won't work on all cases.
If a component deep in the hierarchy tries to updates and fails, the try/catch block in one of the parents won't work -- because it isn't necessarily updating together with the child.
There is an implementation that can handle with non-existent functionalities for a functional component such as componentDidCatch and deriveStateFromError.
According to the author, it is based on React.memo().
The proposed solution is greatly inspired by the new React.memo() API.
import Catch from "./functional-error-boundary"
type Props = {
children: React.ReactNode
}
const MyErrorBoundary = Catch(function MyErrorBoundary(props: Props, error?: Error) {
if (error) {
return (
<div className="error-screen">
<h2>An error has occured</h2>
<h4>{error.message}</h4>
</div>
)
} else {
return <React.Fragment>{props.children}</React.Fragment>
}
})
reference and API here
As mentioned already, the React team has not yet implemented a hook equivalent, and there are no published timelines for a hook implementation.
A few third party packages on npm implement error boundary hooks. I published react-use-error-boundary, attempting to recreate an API similar to useErrorBoundary from Preact:
import { withErrorBoundary, useErrorBoundary } from "react-use-error-boundary";
const App = withErrorBoundary(({ children }) => {
const [error, resetError] = useErrorBoundary(
// You can optionally log the error to an error reporting service
(error, errorInfo) => logErrorToMyService(error, errorInfo)
);
if (error) {
return (
<div>
<p>{error.message}</p>
<button onClick={resetError}>Try again</button>
</div>
);
}
return <div>{children}</div>;
});
Official React team not provided Error boundary support for functional component.
We can achieve error boundary for functional component using npm package.
https://www.npmjs.com/package/react-error-boundary
What I have done is create custom class component and wrapped my functional/class component inside it where ever its required. This is how my custom class component looks like:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {error: ""};
}
componentDidCatch(error) {
this.setState({error: `${error.name}: ${error.message}`});
}
render() {
const {error} = this.state;
if (error) {
return (
<div>{error}</div>
);
} else {
return <>{this.props.children}</>;
}
}
}
And used it like this my functional/class components:
<ErrorBoundary key={uniqueKey}>
<FuncationalOrChildComponent {...props} />
</ErrorBoundary>
PS: As usual the key property is very important as it will make sure to re-render the ErrorBoundary component if you have dynamic child elements.

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.

Prevent react-native-router-flux from rendering all components

I'm using React-Native-Router-Flux for routing my app. The issue is that it seems like when a redux state changes, ALL the components under the Router gets rerendered, not just the "current" component.
So lets say I have 2 components under the Router: Register and Login and both share the same authenticationReducer. Whenever an authentication event (such as user registration or signin) fails, I want to display error Alerts.
The problem is that when an error is fired from one of the components, two Alerts show up at the same time, one from each component. I assumed when I am currently on the Register scene, only the error alert would show from the Register component.
However, it seems like both components rerender whenever the redux state changes, and I see 2 alerts (In the below example, both 'Error from REGISTER' and 'Error from SIGNIN').
Here are the components:
main.ios.js
export default class App extends Component {
render() {
return (
<Provider store={store}>
<Router>
<Scene key='root'>
<Scene key='register' component={Register} type='replace'>
<Scene key='signin' component={SignIn} type='replace'>
</Scene>
</Router>
</Provider>
);
}
}
Register.js
class Register extends Component {
render() {
const { loading, error } = this.props;
if (!loading && error) {
Alert.alert('Error from REGISTER');
}
return <View>...</View>;
}
}
const mapStateToProps = (state) => {
return {
loading: state.get("authenticationReducer").get("loading"),
error: state.get("authenticationReducer").get("error"),
};
};
export default connect(mapStateToProps)(Register);
SignIn.js
class SignIn extends Component {
render() {
const { loading, error } = this.props;
if (!loading && error) {
Alert.alert('Error from SIGNIN');
}
return <View>...</View>;
}
}
const mapStateToProps = (state) => {
return {
loading: state.get("authenticationReducer").get("loading"),
error: state.get("authenticationReducer").get("error"),
};
};
export default connect(mapStateToProps)(SignIn);
How do I change this so that only the REGISTER error message shows when I am currently on the Register Scene, and vice versa?
Thanks
Because of the way react-native-router-flux works, all previous pages are still "open" and mounted. I am not totally sure if this solution will work, because of this weird quirk.
Pretty strict (and easy) rule to follow with React: No side-effects in render. Right now you are actually doing a side-effect there, namely, the Alert.alert(). Render can be called once, twice, whatever many times before actually rendering. This will, now, cause the alert to come up multiple times as well!
Try putting it in a componentDidUpdate, and compare it to the previous props to make sure it only happens once:
componentDidUpdate(prevProps) {
if (this.props.error && this.props.error !== prevProps.error) {
// Your alert code
}
}
I am not totally convinced that this will actually work, as the component will still update because it is kept in memory by react-native-router-flux, but it will at least have less quirks.
I solved this by creating an ErrorContainer to watch for errors and connected it to a component that uses react-native-simple-modal to render a single error modal throughout the app.
This approach is nice because you only need error logic and components defined once. The react-native-simple-modal component is awesomely simple to use too. I have an errors store that's an array that I can push errors to from anywhere. In the containers mapStateToProps I just grab the first error in the array (FIFO), so multiple error modals just "stack up", as you close one another will open if present.
container:
const mapStateToProps = (
state ) => {
return {
error: state.errors.length > 0 ? state.errors[0] : false
};
};
reducer:
export default function errors (state = [], action) {
switch (action.type) {
case actionTypes.ERRORS.PUSH:
return state.concat({
type: action.errorType,
message: action.message,
});
case actionTypes.ERRORS.POP:
return state.slice(1);
case actionTypes.ERRORS.FLUSH:
return [];
default:
return state;
}
}

Resources