Delay a push to a page on success - reactjs

I am currently working on pushing to a log in page on the confirmation of a user via email. Problem is, I would like to first re-direct the user confirming to a page thanking them for registering and a few seconds later push to the log in page. What's the best way of approaching this?
I have a push to the log in page that is happening right away, would like to delay it a few seconds.
confirmSuccess = () => {
this.setState(() => ({ success: true }));
this.props.history.push("/login", { fromRegistration: true });
};
Currently as soon as the user clicks on "confirm account" it pushes them straight the log in page, would like to have it delayed so the "thank you for registering" page can appear first.

You can create a new component like this
import React from "react"
import { withRouter } from "react-router-dom"
class ThankYouComponent extends React.Component {
componentDidMount() {
setTimeout(() => {this.props.history.push("/login")}, 5000) // redirect in 5 secs
}
render() {
return(
<h1>Thank You</h1>
)
}
}
export default withRouter(ThankYouComponent)
and in your confirmSuccess function redirect to ThankYouComponent page instead:
confirmSuccess = () => {
this.setState(() => ({ success: true }));
this.props.history.push("/thankYouPage", { fromRegistration: true });
};
And of course don't forget to add ThankYouComponent to Routes

Related

React Simulate back button behavior in react testing library and jest

Hey guys I've got a component that overrides the back button behavior by creating a popstate event, but I haven't found a way to test it's functionality. It should be as easy as creating a spy and checking if the window.confirm is being called, but it's not calling the function when I do window.history.back(), and I don't understand why.
Also if I pull the function outside of the component, it's being rendered as an anonymous function and the remove event listener isn't being called, which makes the popup event display on every page, and there's no way to remove it because it's an anonymous function. I'm able to test the function though and the logic is working just fine (;
How do we fix this? What should I do?
This function stops the initial back button navigation behavior, and creates a popup event to ask if the person wants to navigate to the home page, and if they click okay then they navigate, otherwise they stay on the page. We have this wrapped around a page after they submit a form to prevent them from submitting another form when they click the back button
Here's my component:
import React, {useEffect, ReactElement} from 'react';
import { navigate } from '#reach/router';
export interface BackButtonBehaviorProps {
children: ReactElement;
}
export const BackButtonBehavior = ({children}: BackButtonBehaviorProps) => {
const onBackButtonEvent = (e: { preventDefault: () => void; }) => {
e.preventDefault();
const backButtonIsConfirmed = window.confirm("Your claim has been submitted, would you like to exit before getting additional claim information?");
if (backButtonIsConfirmed) {
navigate('/');
} else {
window.history.pushState(window.history.state, "success page", window.location.pathname); // When you click back (this refreshes the current instance)
}
};
useEffect(() => {
window.history.pushState(window.history.state, "", window.location.pathname);
window.addEventListener('popstate', onBackButtonEvent, true);
// As you're unmounting the component
return () => {
window.removeEventListener('popstate', onBackButtonEvent);
};
}, []);
return (children);
};
If I pull the function outside of the component and export it, it's rendered in the popstate event listeners as anonymous, and will not be deleted when I'm unmounting the component, and there's no way to fix that. Here are the tests that work when I exported it though:
import {cleanup, render} from '#testing-library/react';
import * as router from '#reach/router';
import { mockPersonalReturnObj } from '../Summary/testData';
describe('<App />', () => {
let navigateSpy: any;
let backButtonBehavior: any;
beforeEach(() => {
const mod = require('./BackButtonBehavior');
backButtonBehavior = mod.onBackButtonEvent;
navigateSpy = jest.spyOn(router, 'navigate');
});
afterEach(() => {
jest.resetAllMocks();
cleanup();
});
it('should display a popup when the user clicks the back button', async () => {
jest.spyOn(global, 'confirm' as any).mockReturnValueOnce(true);
backButtonBehavior({preventDefault: () => {}});
expect(global.confirm).toHaveBeenCalled();
});
it('should navigate to home page when you click ok on the window.confirm popup', async () => {
jest.spyOn(global, 'confirm' as any).mockReturnValueOnce(true);
backButtonBehavior({preventDefault: () => {}});
expect(global.confirm).toHaveBeenCalled();
expect(await navigateSpy).toHaveBeenCalledWith('/');
});
});
I haven't found a way to call the global confirm when I do a test and I literally just want to test if the window.confirm event is being called (there's a bunch of ways to check window.confirm through spies, none of them worked). I need a way to simulate the back button click event for this to be called, but I haven't found a way to do this. Here's a test example:
it('should navigate to home page when you click ok on the window.confirm popup', async () => {
jest.spyOn(global, 'confirm' as any).mockReturnValueOnce(true);
render(
<BackButtonBehavior>
<CurrentPage />
</BackButtonBehavior>
);
window.history.back();
expect(global.confirm).toHaveBeenCalled();
expect(await navigateSpy).toHaveBeenCalledWith('/', {});
});
How do we simulate clicking the back button in the browser for react tests?

How can I get role before render()

I want to do the following:
send a request to the server, verify JSON web-token and get user details (name, email and role);
then the page appears with the specific menu items (if the user is admin it shows him Main, Admin and Logout items; if not - Main and Logout).
I thought about just getting the token from localStorage, then decoding it and taking a role from it. But what should I do if I change the role in database (for example from admin to user)? Decoded token on the client-side will contain the "admin" role. So this user will be able to see the admin page.
You can use a variable in store called isFetchingUserDetails and set it to true when you make that async call to the server to get the details.
Till isFetchingUserDetails is set true, you can have an if statement in the render() to return a spinner or other component which shows the user that the page is loading.
Once you get the response from the server, isFetchingUserDetails will be set to false and the rest of the render() will be executed.
Without Store
import React, { Component } from "react";
import ReactDOM from "react-dom";
class App extends Component {
constructor() {
super();
this.state = { isFetchingUserDetails: true };
}
componentDidMount() {
fetch(`https://api.makethecall.com/users/1`)
.then(res => res.json())
.then(() => this.setState({ isFetchingUserDetails: false }));
}
render() {
if (this.state.isFetchingUserDetails) {
return <Spinner />
}
return (
<Home />
);
}
}

How can you show a modal in react when you press the back button browser?

I need to show a modal when user wants to leave a specified page.
When User wants to go on a different link from the page, I solve this with getUserConfirmation like that:
const getUserConfirmation = (message, callback) => {
const history = createBrowserHistory({
forceRefresh: true
})
if (history.location.pathname == "/add/car") {
store.dispatch(showModal('ConfirmationLeavingAddPageModal', { callback }));
}
}
The problem is when I press the back button on browser it doesn't work anymore.
Any Help Accepted?
For react-router 2.4.0+
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, () => {
if (history.location.pathname == "/add/car") {
store.dispatch(showModal('ConfirmationLeavingAddPageModal', {
callback
}));
}
})
}
in addition you need to import { withRouter } from 'react-router' and export default withRouter(YourComponent)

API call in React

I'm trying myself in react and trying to make a simple application, which will display articles from the Hacker News. I have already made the first call to their API
componentDidMount() {
fetch('https://hacker-news.firebaseio.com/v0/jobstories.json?print=pretty')
.then(res => res.json())
.then(articles => {
this.setState({articles})
})
}
As the response, it returns an array of the articles IDs. To get detailed information for every single article I need to iterate the array I get and to make a second request for every article ID which has to look like
fetch(`https://hacker-news.firebaseio.com/v0/item/{id_from_the_array}`)
And I faced the problem because I have no idea how to implement it in a correct way.. Could someone please advice me?
this will help you
import React from "react";
import { render } from "react-dom";
import Hello from "./Hello";
class App extends React.Component {
state = {
articles: []
};
componentDidMount() {
fetch("https://hacker-news.firebaseio.com/v0/jobstories.json?print=pretty")
.then(res => res.json())
.then(articles => {
articles.map(item => {
fetch(`https://hacker-news.firebaseio.com/v0/item/${item}.json?print=pretty`)
.then(res => res.json())
.then(detailArticles => {
const articles = this.state.articles.concat(detailArticles);
this.setState({ articles });
});
});
});
}
render() {
return <p>{JSON.stringify(this.state.articles) }</p>;
}
}
render(<App />, document.getElementById("root"));
codesandbox
One way you can do is like using pagenation or infinite scroll so you can show some 10 or 15 news on the screen and load the next set of data on click of a button. Else you can show only id's on screen and fetch the data on click of a button.

Custom leave dialog in react-router

I have a form where the user can edit stuff. If the user has some unsaved changed and wants to leave the page I want to give him a custom dialog, asking him if he is sure. If then the navigation should continue and the form changed flag should be reset, otherwise the user should stay at the page.
Here you can check my current higher order component which is checking this condition. It is working, but the function leave returns a string and react-router will then display that text in a native alert.
Is there a way that I can show my custom dialog in here? And can I also get a callback or similar of the alert, so that I can dispatch an event which tells the storage, that the latest changes are not important.
import React, {Component, PropTypes} from "react";
import connectRouter from "./connectRouter";
import { connect } from "react-redux";
import {injectIntl, intlShape} from "react-intl";
export default function confirmLeave(RouteTargetComponent) {
#injectIntl
#connectRouter
#connect((state, routing) => {
return {
studies: state.studies
};
})
class ConfirmLeaveHOC extends Component { // HOC = Higher Order Component
static propTypes = {
router: PropTypes.object.isRequired,
route: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
intl: intlShape.isRequired,
studies: PropTypes.object.isRequired,
};
leave = () => {
if (this.props.studies.isChanged) {
// lets stop the navigation
return this.props.intl.formatMessage({ id: "confirmLeave" });
}
// continue the navigation
return true;
}
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, this.leave.bind(this));
}
render() {
// render the component that requires auth (passed to this wrapper)
return (<RouteTargetComponent {...this.props}/>);
}
}
return ConfirmLeaveHOC;
}
Since customizing browser dialogs is not possible, you'll have to render a separate component (e.g bootstrap modal) and use a callback to determine which button was clicked, and what action to take.
I actually ran into the same problem you're facing very recently, and I was able to solve it by using routerWillLeave and using callbacks from another component.
Form component
routerWillLeave = (route) => {
if (!this.waitingForConfirm && this._hasUnsavedChanges() && !this.clickedSave) {
this.refs.confirmAlert._show( ((v) => {
if (v) {
this.context.router.push(route.pathname);
}
this.waitingForConfirm = false;
}).bind(this));
this.waitingForConfirm = true;
return false;
}
}
The implementation of customized dialog like this one is unfortunately quite a pain in the back. I had to use 3 variables here to correctly control the desired behavior:
waitingForConfirm - necessary to prevent the logic from running a second time when the user confirms to navigate out. Specifically, when the callback is run and we do this.context.router.push(route.pathname), the routerWillLeave will run again(!), but since we've already confirmed navigation we must prevent this logic from running again.
_hasUnsavedChanges() - checks if any input fields have changed (no reason to ask if there's no changes to be saved).
clickedSave - don't ask for confirmation if the user clicked Save - we know we want to leave.
Dialog component
_show = (callback) => {
this.callback = callback;
this.setState({show: true});
}
_hide = () => {
this.setState({show: false});
this.callback = null;
}
_dialogAction = (input) => {
if (this.callback) {
this.callback(input);
}
this._hide();
}
render() {
return (
...
<Button onClick={this._dialogAction.bind(this, true)}>Yes</Button>
<Button onClick={this._dialogAction.bind(this, false)}>No</Button>
);
}
Obviously, you'll have to customize the above snippets to fit your application, but hopefully it will provide some insight into how to solve the problem.
A less complicated and more forward way is using a setRouteLeaveHook on your component. In accordance to the react-router v2.4.0
import React from 'react'
import { withRouter } from 'react-router'
const Page = React.createClass({
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, () => {
if (this.state.unsaved)
return 'You have unsaved information, are you sure you want to
leave this page?'
})
}
render() {
return Stuff
}
})
export default withRouter(Page)
Here is the React-router version on their github page

Resources