'Home' is an intermediate component that redirects based on a xhr response. I expected 'Home' to display 'Loading ...' as per its render/return, until it receives the xhr response.
But surprisingly,
1) I never see 'Loading ...' at all, but I only see the redirected page.
2) Changing the delay in fake xhr request has no impact, i.e. I still get the response in < 5s
3) Changing componentWillMount() to componentDidMount() has no impact
4) If I comment out componentWillMount() { ... }, then I see 'Loading ...'
Can you please help me understand why ?
xhr.js
const myData = {
getData(cb) {
setTimeout(cb(true), 25000); // fake async
}
};
export default myData;
Home.js
"use strict";
import React from 'react';
import xhr from '../../utils/xhr';
import PropTypes from 'prop-types';
import {withRouter} from 'react-router-dom';
class Home extends React.Component {
constructor(props) {
super(props);
}
componentWillMount() {
const {history} = this.props;
xhr.getData((flag) => {
flag ? history.push('/myData') : history.push('/welcome');
});
}
render() {
return (
<div>Loading ...</div>
);
}
}
Home.propTypes = {
history: PropTypes.object.isRequired
};
export default withRouter(Home);
Your code should work fine with componentDidUpdate except one thing, you didnt run setTimeout correctly
Example:
const getData = cb => {
setTimeout(() => cb(false), 2500); // fake async
};
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
pageName: "Loading..."
};
}
componentDidMount() {
getData(flag => {
flag
? this.setState({ pageName: "myData" })
: this.setState({ pageName: "wellcome" });
});
}
render() {
return <div>{this.state.pageName}</div>;
}
}
ReactDOM.render(<Home />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Update
As commented below i should explain why and where you should invoke async operations.
componentWillMount - Is almost the same as the constructor, it
runs one time before the component is mounted to the DOM.
The render method can be invoked before your async operation has finished
hence it is not ideal to do async operation in this method. By the
way, in the DOCS its clearly says not to use this method and use
the constructor instead.
componentDidMpunt - Will invoked right after the component is
mounted to the DOM, this is the ideal method to run async operations.
Setting the state in this method will trigger a re-render (which
exactly what happens in the example above) DOCS.
Related
I have the following React component, that on componentDidMount() performs an async operation and once data is received, updates the state with the result.
import * as React from "react";
export interface IAppProp {
App: any
}
export interface IAppProp {
App: any
}
export class App extends React.Component<IAppProp, IAppState> {
constructor(props: IAppProp) {
super(props);
this.state = { App: undefined };
}
public componentDidMount(){
// Some async operation
// Once data comes in update state as follows
this.setState({ App: data returned from async operation });
}
public render() {
if (this.state && this.state.App) {
return (
<SomeComponent />
)
} else {
return <span>Loading...</span>
}
}
}
However, while I wait for data to come back, I return a Loading... message in the render() function.
I'm trying to test this Loading state in my Jest test. This is what I have so far:
it("Should display loading when state is undefined", () => {
const inputControl = enzyme.mount(<MyApp App={pass in the right prop} />);
expect(inputControl.find("span").text()).toEqual("Loading...");
});
I know the above is wrong because it never finds the Loading span. I also tried passing undefined in the props but that crashes the test because the async operation in componentDidMount() expects a legit prop.
How can I test this? Thanks!
Here is a working example based on your code (modified since I don't have SomeComponent, the async function wasn't specified, etc.).
Given this component defined in App.tsx:
import * as React from "react";
const getMessageById = (id: number): Promise<string> => {
return Promise.resolve('Message ' + id);
}
interface IAppProp {
messageid: number
}
interface IAppState {
message: string
};
export class App extends React.Component<IAppProp, IAppState> {
constructor(props: IAppProp) {
super(props);
this.state = { message: '' };
}
public componentDidMount(){
// Some async operation
// Once data comes in update state as follows
getMessageById(this.props.messageid).then((message) => {
this.setState({ message });
});
}
public render() {
if (this.state && this.state.message && this.state.message.length > 0) {
return (
<div>The message: {this.state.message}</div>
)
} else {
return <span>Loading...</span>
}
}
}
A test App.test.tsx can be created as follows:
import { mount } from 'enzyme';
import * as React from 'react';
import { App } from './App';
describe('App', () => {
it ('Should display loading until data arrives', async () => {
const inputControl = mount(<App messageid={1} />);
expect(inputControl.html()).toBe('<span>Loading...</span>');
// Let the event loop cycle so the callback queued by 'then'
// in 'componentDidMount()' has a chance to execute
await Promise.resolve();
expect(inputControl.html()).toBe('<div>The message: Message 1</div>');
});
});
For your actual component you will likely need to mock the async function that is getting the data (so you avoid making network requests, etc.), but this should provide a solid start for what you are trying to do and is the best I can do with the information provided.
Sorry for necroing (just a little)
I find that it's hard to test everything from business logic to rendering all in 1 go.
So I split the tests to:
Testing side effects, i.e. prop/state changes, from user interactions and/or async operations
Testing snapshots of exact prop/state combinations that I expect to receive.
I am using react material UI. I am frequently checking if token is expired using setInternval() and if its expire than login dialog should be open and setInterval should be cleared using clearInterval(). Below is my code but I am getting warning as Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. and not able to achieve the required result.
App.js
import AuthService from './includes/AuthService.js';
class App extends React.Component {
constructor(props) {
super(props);
this.Auth = new AuthService();
}
componentDidMount(){
setInterval(() => {this.Auth.checkToken()}, 10000);
}
}
AuthService.js
class AuthService extends React.Component{
constructor(props) {
super(props);
this.state = {email: '', password : '', loginOpen : false};
}
checkToken() {
console.log("token checked");
if (decode(localStorage.getItem('jwtToken')).exp < Date.now() / 1000) {
this.setState({loginOpen : true}, () => {
console.log('state updated');
console.log(this.state.loginOpen);
clearInterval();
});
}
}
render(){
const { onRequestClose } = this.props;
const actions = [
<FlatButton
label="Close"
primary={true}
onClick={this.handleClose}
/>,
];
return (
<MuiThemeProvider>
<Dialog title="Result Details"
actions={actions}
modal={false}
open={this.state.loginOpen}
onRequestClose={this.handleClose}
autoScrollBodyContent={true}
>
</Dialog>
</MuiThemeProvider>
}
loginOpen and checkToken() look like something that can be moved up to App component, and be passed to AuthService as props.
Alternatively, you can simply move down and call checkToken() on componentDidMount() function of AuthService.
Like this:
class AuthService extends React.Component{
constructor(props) {
super(props);
// ...
this.checkToken = this.checkToken.bind(this);
this.intervalId = null;
}
componentDidMount(){
this.intervalId = setInterval(() => {this.checkToken()}, 10000);
}
checkToken() {
console.log("token checked");
if (decode(localStorage.getItem('jwtToken')).exp < Date.now() / 1000) {
this.setState({loginOpen : true}, () => {
console.log('state updated');
console.log(this.state.loginOpen);
if (this.itv) {
clearInterval(this.intervalId);
}
});
}
}
See which approach will work better, and see if my fix works, and I can add more explanations.
One more thing I want to point out is that clearInterval(..) takes the ID returned from setInterval.
Hence the setting of this.intervalId and passing it to clearInterval(..).
From your comment:
how can I change state in parent component i.e. app component from its
child component. Becase login modal is in app component.
You are rendering login modal in App component.
You can conditionally render the login modal based on App's this.state.loginOpen.
For example, if your App render function contains a login modal component called LoginModal
render() {
<div>
{ this.state.loginOpen && <LoginModal /> }
</div>
Or, if you are calling some function to show the login modal, you can do something like if (this.state.loginOpen) { showLoginModal(); }.
I am trying to build a Higher Order Component that would be used to protect routes.It would check (via a network call, not from cache) for the current user, and if not found, would redirect to /login.
Any ideas on how to do so?
First understand how React HOC works. I'll try to answer this using the pseudo code.
HOC.jsx
export default (Component) => class HOC extends React.Component {
constructor(){
super()
this.state = {
waiting: true,
}
}
componentDidMount() {
this.props.actions.validateUser() // your network call to validate user
.then(() => {
// do your stuff
this.setState({
waiting: false,
})
})
.catch(() => {
// handle redirect using react-router
})
}
render() {
if(this.state.waiting) {
// Add a loader here
return <h1>Loading...</h1>
}
return <Component {...this.props} />
}
}
Component.jsx
import HOC from './HOC.jsx'
class Comp extends React.Component {
render() {
return <h1>I'm a valid user</h1>
}
}
export default HOC(Comp)
When any component uses the HOC, the network call will be executed on mount (can be moved to componentWillMount as well) and based on the response the component will be rendered.
Hope this helps!
I have a parent component called Home.js which retrieves the data from an API and send the data as a property to it's child component DishItem.js that shows the dish details. I am trying to render the dish_name, quantity (that can be changed) and net_cost, and I am storing all of these in a state and setting their state for the initial render through componentWillMount() lifecycle method. But it fails to set the state and returns undefined.
Home.js
import React, { Component } from 'react';
import DishItem from './DishItem';
import $ from 'jquery';
export default class Home extends Component {
constructor() {
super();
this.state = {
dish: ''
}
}
getDishes() {
$.ajax({
url: 'http://testbed2.riktamtech.com/foody_buddy/public/api/dishes/dish/31816',
dataType: 'json',
cache: false,
success: function(response) {
this.setState({dish: response.data});
}.bind(this),
error: function(xhr, status, err) {
console.log(err);
}
});
}
componentWillMount() {
this.getDishes();
}
componentDidMount() {
this.getDishes();
}
render() {
return (
<div className="Home">
<DishItem dish={this.state.dish}/>
</div>
);
}
}
DishItem.js
import React, { Component } from 'react';
import Quantity from './Quantity';
import { browserHistory } from 'react-router';
import './style.css';
export default class DishItem extends Component {
constructor(props) {
super(props);
this.state = {
quantity: 1,
delivery: '',
total_cost: '',
net_cost: null,
counter: 1
}
}
componentWillMount() {
console.log(this.props.dish.net_cost); //undefined instead of some value
this.setState({net_cost: this.props.dish.net_cost});
console.log(this.state.net_cost); // null
}
handleChangeInQuantity(value) {
var net_cost = 0;
var total_cost = 0;
total_cost = value * this.props.dish.cost;
net_cost = value * this.props.dish.net_cost;
this.setState({quantity: value, net_cost: net_cost, total_cost: total_cost});
}
saveAndContinue(e) {
e.preventDefault();
}
render() {
let address = <div>{this.props.dish.address}<br/>{this.props.dish.landmark}<br/>{this.props.dish.locality}</div>
return (
<div>
<h3>{this.props.dish.name}<small>{this.props.dish.description}</small></h3>
Total- {this.state.net_cost}
<Quantity change_in_quantity={this.handleChangeInQuantity.bind(this)} />
<button className="btn btn-default" onClick={this.saveAndContinue.bind(this)}>Next</button>
</div>
);
}
}
componentWillMount() function is triggered only once, it does not wait to get the data from the server. you should use componentWillReceiveProps() in the DishItem component, and set the new state with the data sent from the Home component to it. This way whenever the DishItem receives new props from the Home component, it will update accordingly.
You should not use the componentWillMount method of a React component to do asynchronous data fetching. Remove your call to the getDishes method and leave it in componentDidMount. In the render method of your Home component, check if the state already has the dish data in the state. If it doesn't, don't render the DishItem component. You can put a "Please wait, loading" message or something like that instead.
When the page is loaded, React will render the component with the "loading " message and start loading the dish data asynchronously. When the data is done loading and the state is set, React will render the component again with the DishItem component.
Here's an updated version of your Home component:
import React, { Component } from 'react';
import DishItem from './DishItem';
import $ from 'jquery';
export default class Home extends Component {
constructor() {
super();
this.state = {
dish: null
};
}
getDishes() {
$.ajax({
url: 'http://testbed2.riktamtech.com/foody_buddy/public/api/dishes/dish/31816',
dataType: 'json',
cache: false,
success: function (response) {
this.setState({ dish: response.data });
}.bind(this),
error(xhr, status, err) {
console.log(err);
}
});
}
componentDidMount() {
this.getDishes();
}
render() {
return (
<div className="Home">
{
this.state.dish === null ?
<div>Please wait, loading…</div> :
<DishItem dish={this.state.dish} />
}
</div>
);
}
}
it seems that I can't add a comment so I will just send this since there are already many answers. Please don't give me minus one because of this :)
Even though it is in componentDidMount, once it fetches the data, it will re-render the DOM so it will look like it is the initial render. If the fetching is slow, you will see it blank for a moment and then renders to what you need it to be.
If you really really need to make it show up at the very first moment, I suggest that you fetch the data and save it inside Flux store or Redux store before redirecting to this page. For example, fetch the data and do the redirection inside then() or callback function.
I'm making a get request within my component's componentDidMount() method. I'm able to successfully make the request and set my component's state. However, when I try to get access to state within my render() method, it comes back as undefined. I'm guessing it has something to do with the asynchronous nature of javascript, but can't seem to figure out how to properly set the state, wait to make sure that state has been set, then pass it down to my render() method so I can access it there. Thanks.
Game.js (component file)
import React, { Component } from 'react';
import { Link } from 'react-router';
import axios from 'axios';
export default class Game extends Component {
constructor(props) {
super(props)
this.state = {
data: []
};
}
getReviews() {
const _this = this;
axios.get('/api/reviews')
.then(function(response) {
_this.setState({
data: response.data
});
console.log(_this.state.data); // shows that this is an array of objects.
})
.catch(function(response) {
console.log(response);
});
}
componentDidMount() {
this.getReviews();
}
render() {
const allReviews = this.props.data.map((review) => {
return (
<li>{review.username}</li>
);
})
console.log(this.props.data); // comes in as undefined here.
return (
<div>
{allReviews}
</div>
);
}
}