Next.js - execute callback when Router.push() or getInitialProps() completes - javascript

I have a page that generates a random number in getInitialProps() after 2 seconds. There's a button that allows the user to "refresh" the page via Router.push(). As getInitalProps() takes 2 seconds to complete, I'll like to display a loading indicator.
import React from 'react'
import Router from 'next/router'
export default class extends React.Component {
state = {
loading: false
}
static getInitialProps (context) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({random: Math.random()})
}, 2000)
})
}
render() {
return <div>
{
this.state.loading
? <div>Loading</div>
: <div>Your random number is {this.props.random}</div>
}
<button onClick={() => {
this.setState({loading: true})
Router.push({pathname: Router.pathname})
}}>Refresh</button>
</div>
}
}
How can I know when Router.push()/getInitialProps() completes so I can clear my loading indicator?
Edit: Using Router.on('routeChangeComplete') is the most obvious solution. However, there are multiple pages and the user could click on the button multiple times. Is there a safe way to use Router events for this?

Router.push() returns a Promise. So you can do something like...
Router.push("/off-cliff").then(() => {
// fly like an eagle, 'til I'm free
})

use can use Router event listener in pages/_app.js, manage page loading and inject state into component
import React from "react";
import App, { Container } from "next/app";
import Router from "next/router";
export default class MyApp extends App {
state = {
loading: false
};
componentDidMount(props) {
Router.events.on("routeChangeStart", () => {
this.setState({
loading: true
});
});
Router.events.on("routeChangeComplete", () => {
this.setState({
loading: false
});
});
}
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
render() {
const { Component, pageProps } = this.props;
return (
<Container>
{/* {this.state.loading && <div>Loading</div>} */}
<Component {...pageProps} loading={this.state.loading} />
</Container>
);
}
}
and you can access loading as a props in your page component.
import React from "react";
import Router from "next/router";
export default class extends React.Component {
static getInitialProps(context) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ random: Math.random() });
}, 2000);
});
}
render() {
return (
<div>
{this.props.loading ? <div>Loading</div> : <div>Your random number is {this.props.random}</div>}
<button
onClick={() => {
this.setState({ loading: true });
Router.push({ pathname: Router.pathname });
}}
>
Refresh
</button>
</div>
);
}
}
you can also show loading text in _app.js (I've commented), that way you don't have to check loading state in every pages
If you wanna use third-party package here a good one nprogress

Related

React Redux not dispatching API Call to delete player

I am trying to migrate my previously working local state to redux. Now loading available Players works just fine, but deleting will somehow stop in the playerActions.js file, where I dispatch and then return an API Call. So to further give details here are my code parts in relevance:
PlayerPage.js (Component):
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { loadPlayers, deletePlayer } from '../../redux/actions/playerActions';
import PlayerForm from './playerform';
import PlayCard from './playercard';
import PropTypes from 'prop-types';
import { toast } from 'react-toastify';
class PlayerPage extends Component {
constructor(props) {
super(props);
this.handleDeletePlayer = this.handleDeletePlayer.bind(this);
state = {};
componentDidMount() {
const players = this.props;
players.loadPlayers().catch(err => {
alert('Loading players failed. ' + err);
});
}
handleDeletePlayer = player => {
toast.success('Player deleted');
try {
deletePlayer(player);
} catch (err) {
toast.error('Delete failed. ' + err.message, { autoClose: false });
}
};
render() {
const styles = {
margin: '20px'
};
return (
<div className="container-fluid">
<div>
<h2 style={styles}>Add Player</h2>
<div className="container-fluid">
<PlayerForm handleAddNewPlayer={this.handleAddPlayer} />
</div>
</div>
<hr></hr>
<div>
<h2 style={styles}>Available Player</h2>
<div className="container-fluid">
{this.props.players.map(player => (
<PlayCard
player={player}
key={player.id}
imageSource={`${process.env.API_URL}/${player.profileImg}`}
onDeletePlayer={this.handleDeletePlayer}
/>
))}
</div>
</div>
</div>
);
}
}
PlayerPage.propTypes = {
players: PropTypes.array.isRequired
};
function mapStateToProps(state) {
return {
players: state.players
};
}
const mapDispatchToProps = {
loadPlayers,
deletePlayer
};
export default connect(mapStateToProps, mapDispatchToProps)(PlayerPage);
And the Action being called is in here:
playerActions.js:
import * as types from './actionTypes';
import * as playerApi from '../../api/playerApi';
export function loadPlayersSuccess(players) {
return { type: types.LOAD_PLAYERS_SUCCESS, players };
}
export function deletePlayerOptimistic(player) {
return { type: types.DELETE_PLAYER_OPTIMISTIC, player };
}
export function loadPlayers() {
return function(dispatch) {
return playerApi
.getAllPlayers()
.then(players => {
dispatch(loadPlayersSuccess(players));
})
.catch(err => {
throw err;
});
};
}
export function deletePlayer(player) {
console.log('Hitting deletePlayer function in playerActions');
return function(dispatch) {
dispatch(deletePlayerOptimistic(player));
return playerApi.deletePlayer(player);
};
}
The console.log is the last thing the app is hitting. But the API Call is never made though.
API Call would be:
playerApi.js:
import { handleResponse, handleError } from './apiUtils';
const axios = require('axios');
export function getAllPlayers() {
return (
axios
.get(`${process.env.API_URL}/player`)
.then(handleResponse)
.catch(handleError)
);
}
export function deletePlayer(id) {
return (
axios
.delete(`${process.env.API_URL}/player/${id}`)
.then(handleResponse)
.catch(handleError)
);
}
I was like spraying out console.log in different places and files and the last one I am hitting is the one in playerActions.js. But after hitting it the part with return function(dispatch) {} will not be executed.
So if someone could point me in a general direction I'd be more than grateful.
It looks like you are calling your action creator deletePlayer but you aren't dispatching it correctly. This is why the console.log is being called but not the method that does the request.
I'd recommend taking a look at the documentation for mapDispatchToProps to fully understand how this works. In your example, you should just need to change the call to deletePlayer in your PlayerPage component to this.props.deletePlayer() to use the action creator after it's been bound to dispatch properly.
this how the mapDispatchToProps should be:
const mapDispatchToProps = dispatch => {
return {
load: () => dispatch(loadPlayers()),
delete: () => dispatch(deletePlayer()),
}
}
then call load players with this.props.load() and delete player with this.props.delete()

Why in the new react-router-dom <Redirect/> does not fire inside the setTimout?

So, I tried to make a little delay between the logout page and redirecting to the main website page. But I fall into the problem, that the react-router-dom <Redirect/> method does not want fire when we put it inside the setTimeout() of setInterval().
So, if we unwrapped it from timer, the <Redirect/> will work normally.
What is the problem is, any suggestions?
My code:
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import axios from 'axios';
class LogoutPage extends Component {
constructor(props) {
super(props);
this.state = {
navigate: false
}
}
componentDidMount(e) {
axios.get('http://localhost:3016/logout')
.then(
this.setState({
navigate: true
}),
)
.catch(err => {
console.error(err);
});
if (this.state.navigate) {
setTimeout(() => {
return <Redirect to="/employers" />
}, 2000);
}
};
render() {
if (this.state.navigate) {
setTimeout(() => {
return <Redirect to="/employers" />
}, 2000);
}
return (
<div>You successfully logouted</div>
)
}
}
export default LogoutPage;
You want the render() method to return <Redirect /> in order for the redirect to take place. Currently the setTimeout function returns a <Redirect />, but this does not affect the outcome of the render() itself.
So instead, the render() should simply return <Redirect /> if this.state.navigate is true and you delay the setState({ navigate: true }) method with 2 seconds.
Here's a corrected, declarative way of doing it:
class LogoutPage extends Component {
constructor(props) {
super(props);
this.state = {
navigate: false
}
}
componentDidMount(e) {
axios.get('http://localhost:3016/logout')
.then(() => setTimeout(() => this.setState({ navigate: true }), 2000))
.catch(err => {
console.error(err);
});
};
render() {
if (this.state.navigate) {
return <Redirect to="/employers" />
}
return (
<div>You successfully logouted</div>
);
}
}
For the imperative version, see #Shubham Khatri's answer.
Redirect needs to be rendered for it to work, if you want to redirect from an api call, you can use history.push to programmatically navigate. Also you would rather use setState callback instead of timeout
componentDidMount(e) {
axios.get('http://localhost:3016/logout')
.then(
this.setState({
navigate: true
}, () => {
this.props.history.push("/employers");
}),
)
.catch(err => {
console.error(err);
});
};
Check Programmatically Navigate using react-router for more details
Even if you add Redirect within setTimeout in render it won't work because it will not render it, rather just be a part of the return in setTimeout

Why does this component re-render completely when changing a single item with a unique key?

This is a reoccurring problem for me… Trying to figure out why an update to a single item in a component results in the entire component re-rendering. If I have a CSS fade in transition on the component, it fades in again when changing a child of the component.
I have a list of items, each with a link. Clicking the link adds the item to the cart. I have it set up to put that item in a “loading” state until the cart action is successful.
This used to work perfectly, but now it just re-renders the entire page, making it disappear for a second then reappear. I’m not entirely sure why.
This is the code stripped down to its basic bits:
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import autobind from 'class-autobind';
import Responsive from 'components/Responsive';
// Selectors
import { createStructuredSelector } from 'reselect';
import { selectCartLoading, selectCartMap, selectFavorites } from 'containers/App/selectors';
import { selectPackages } from 'store/fonts/selectors';
// Actions
import { addToCart } from 'containers/App/actions';
export class Packages extends React.Component {
constructor() {
super();
autobind(this);
}
state = {
loadingID: 0
}
componentWillReceiveProps(nextProps) {
if (this.props.cartLoading === true && nextProps.cartLoading === false) {
this.setState({ loadingID: 0 });
}
}
onAddToCart(e) {
e.preventDefault();
const { onAddToCart } = this.props;
const id = e.currentTarget.dataset.package;
const packageData = {
type: 'package',
id,
quantity: 1
};
onAddToCart(packageData);
this.setState({ loadingID: id });
}
render() {
const { cartMapping, packages } = this.props;
if (!packages) { return null; }
return (
<Responsive>
<div>
<ul>
{ packages.map((pack) => {
const inCart = !!cartMapping[parseInt(pack.id, 10)];
const isFavorited = !favorites ? false : !!find(favorites.favorites, (favorite) => parseInt(pack.id, 10) === favorite.items.id);
return (
<li key={ pack.id }>
<Icon iconName="heart" onClick={ (e) => this.onAddFavorite(e, pack) } />
<span>{ pack.name }</span>
{ inCart && <span>In Cart</span> }
{ !inCart && <a data-package={ pack.id } href="/" onClick={ this.onAddToCart }>Add to Cart</a> }
</li>
);
})}
</ul>
</div>
</Responsive>
);
}
}
Packages.propTypes = {
cartLoading: PropTypes.bool,
cartMapping: PropTypes.object,
onAddToCart: PropTypes.func.isRequired,
packages: PropTypes.array
};
Packages.defaultProps = {
cartLoading: null,
cartMapping: null,
packages: null
};
const mapStateToProps = createStructuredSelector({
cartLoading: selectCartLoading(),
cartMapping: selectCartMap(),
packages: selectPackages()
});
function mapDispatchToProps(dispatch) {
return {
onAddToCart: (data) => dispatch(addToCart(data)),
dispatch
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Packages);
So why does clicking on <a data-package={ pack.id } href="/" onClick={ this.onAddToCart }>Add to Cart</a> result in a complete component re-render?
In your onAddToCart function you are setting the state of the component which will by default trigger a re-render of the component. If you need to set the state but not cause a re-render you can add a shouldComponentUpdate() function and check the changes before issuing a re-render to the component.
Find out more about shouldComponentUpdate() and the rest of the component lifecycle here

React, TypeError (this.props.data.map is not a function) on an Array obj

Thank you for stopping by to help. I am working with a react/redux app. One of the component is using a lifecyle method to retrieve data from an API. Once recieved, the data JSON data is held within an array. My initialState for the data coming back is an empty array.
When the component listening to the state change is mounted, the data is rendered on to the page, but then 2 seconds later I am getting a
Uncaught TypeError: jobs.map is not a function
Component making the API call using lifecyle method and listening for state change
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getJobs } from '../../actions';
import { Card, Grid, Image, Feed } from 'semantic-ui-react';
// import './home.css';
const renderJobs = jobs => jobs.map((job, i) => (
<Card.Group stackable key={i}>
<Card className="jobscard">
<Card.Content>
<Card.Header href={job.detailUrl} target="_blank">{job.jobTitle}</Card.Header>
<Card.Meta>{job.location}</Card.Meta>
<Card.Description>{job.company}</Card.Description>
</Card.Content>
</Card>
</Card.Group>
));
class GetJobs extends Component {
componentDidMount() {
this.props.getJobs();
}
render() {
const { jobs } = this.props;
return (
<div className="getjobs">
{renderJobs(jobs)}
</div>
);
}
}
export default connect(({ jobs }) => ({ jobs }), { getJobs })(GetJobs);
Action creator/action
export const getJobsRequest = () => fetch('https://shielded-brushlands-43810.herokuapp.com/jobs',
)
.then(res => res.json());
// action creator
export const getJobs = () => ({
type: 'GET_JOBS',
payload: getJobsRequest(),
});
Reducer
import initialState from './initialState';
export default function (jobs = initialState.jobs, action) {
switch (action.type) {
case 'GET_JOBS_PENDING':
return { ...jobs, isFetching: true };
case 'GET_JOBS_FULFILLED':
return action.payload;
case 'GET_JOBS_REJECTED':
return jobs;
default:
return jobs;
}
}
And intial state
export default {
userData: {},
jobs: [],
}
enter image description here
any thoughts on why this is happening?
You can put a simple check to ensure that your jobs is ready before you attempt rendering it.
{jobs.length && renderJobs(jobs)}

React/Redux Loading application state in component constructor

I'm rendering high-order component, say Application and I need to fetch some data from server, before it's rendered. What I do, in constructor of Application I issue loadApplicationState() action, that performs server call and prepares initial state.
Some simplified code,
class Application extends Component {
constructor(props) {
super(props);
const { dispatch } = this.props;
dispatch(loadApplicationState());
}
render() {
const { stateLoaded } = this.props.state;
render (
<div>
{ stateLoaded ? renderApp() : renderProgress() }
</div>
)
}
}
function loadApplicationState() {
return (dispatch) => {
// fetch data and once ready,
applicationStateLoaded(data);
}
}
I've tried that on practice, it works fine. But not sure is this a right approach? Especially using a constructor for such purposes.
We run this in componentDidMount, and then test for an $isLoading flag in our Redux state, rendering either a loading indicator or the actual UI. Something like so:
import React, { Component } from 'react';
const mapStateToProps = (state) => ({
$isLoading: state.initialState.$isLoading
})
const mapDispatchToProps = (dispatch) => ({
loadApplicationState(){ dispatch(loadApplicationState()); }
})
export class Application extends Component {
componentDidMount(){
this.props.loadApplicationState();
}
render(){
const {
$isLoading
} = this.props;
{$isLoading ? (<Loader />) : <ActualApplication />}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Application)

Categories