Component not updating when state changes - reactjs

This is the first time I'm using React without JSX so I may have made an obvious error, but I can't seem to figure out why my component is not updated.
I make a xhr request in componentDidMount and attempt to set this.state.isLoading to false. This should cause the loading message to be removed once the xhr request has finished.
Here's my code:
import React from 'react'
import request from 'superagent'
export class SurveyList extends React.Component {
constructor () {
super()
this.state = {
error: false,
surveys: [],
isLoading: true
}
}
componentDidMount () {
request
.post('/surveys')
.set('accept', 'json')
.end((err, res) => {
if (err) this.state.error = err
this.state.isLoading = false
})
}
render () {
const items = this.state.surveys.map(survey => React.createElement('li', survey))
const loading = React.createElement('p', null, 'Loading ...')
const list = React.createElement('ul', null, items)
return this.state.isLoading ? loading : list
}
}
I render like so:
const surveyListElem = document.getElementById('survey-list')
if (surveyListElem) {
ReactDOM.render(
React.createElement(SurveyList),
surveyListElem
)
}

Setting the state directly will have no effect. You need to use setState() instead to trigger a re-render.

Related

Replacing componentWillMount() with constructor does not work

A class is used to intercept axios error. to bind arrow functions, componentDidMount() is being used. now I need to initiliaze data from the server so I have to use componentWillMount() except it will be removed in React 17 and warning message suggest I use constructor. When I do it gives me an error.
import React, {Component} from "react";
import Modal from "../../components/UI/Modal/Modal";
import Aux from "../Auxiliary/Auxiliary";
const withErrorHandler = (WrappedComponent, axios) => {
return class extends Component{
state = {
error: null
};
// componentWillMount() {
// axios.interceptors.request.use(request => {
// this.setState({
// error: null
// });
// return request;
// });
// axios.interceptors.response.use(res => res, (error) => {
// this.setState({
// error: error
// })
// });
// }
constructor(props) {
super(props);
axios.interceptors.request.use(request => {
this.setState({
error: null
});
return request;
});
axios.interceptors.response.use(res => res, (error) => {
this.setState({
error: error
})
});
}
errorConfirmedHandler = () => {
this.setState({error: null})
};
render() {
return (
<Aux>
<Modal show={this.state.error} modalClosed = {this.errorConfirmedHandler}>
{this.state.error ? this.state.error.message : null}
</Modal>
<WrappedComponent {...this.props}></WrappedComponent>
</Aux>
);
}
}
};
export default withErrorHandler;
I removed .json from URL to produce an error
class BurgerBuilder extends Component {
state = {
ingredients: null,
totalPrice: 4,
purchasable: false,
purchasing: false,
loading: false,
// axiosError: null
};
componentDidMount() {
axios.get('https://burger-m.firebaseio.com/ingredients').then(response => {
this.setState({ingredients: response.data});
}).catch(error => {});
}
..
export default withErrorHandler(BurgerBuilder, axios);
&
Error Message: "index.js:1 Warning: Can't call setState on a component that
is not yet mounted. This is a no-op, but it might indicate a bug in your
application. Instead, assign to `this.state` directly or define a `state = {};`
class property with the desired state in the _temp component."
componentWillMount() does work however. so What Should I change?
Keep constructor simple by just adding state and do not register axios interceptors in constructor method, instead register interceptors in render method.
componentWillUnmount(){
console.log('unregistering interceptors', this.reqInterceptor, this.resInterceptor)
axios.interceptors.request.eject(this.reqInterceptor);
axios.interceptors.response.eject(this.resInterceptor);
}
render() {
if(!this.resInterceptor){
console.log('Registering Interceptors');
this.reqInterceptor = axios.interceptors.request.use(req => {
this.setState({ error: null })
return req;
})
this.resInterceptor = axios.interceptors.response.use(response => response, error => {
this.setState({error})
})
}
return (
<Aux>
<Modal show={this.state.error} modalClosed={this.errorConfirmedHandler }>{this.state.error ? this.state.error.message : null}</Modal>
<WrappedComponent />
</Aux>
)
The constructor initializes the state, that's why you are prohibited from using setState() there.
You could use componentDidMount() instead, I think it matches better your needs and will avoid any confusion.
const withErrorHandler = (WrappedComponent, axios) => {
return class extends Component{
state = {
error: null
};
componentDidMount() {
axios.interceptors.request.use(request => {
this.setState({
error: null
});
return request;
});
axios.interceptors.response.use(res => res, (error) => {
this.setState({
error: error
})
});
}
errorConfirmedHandler = () => {
this.setState({error: null})
};
render() {
return (
<Aux>
<Modal show={this.state.error} modalClosed = {this.errorConfirmedHandler}>
{this.state.error ? this.state.error.message : null}
</Modal>
<WrappedComponent {...this.props}></WrappedComponent>
</Aux>
);
}
}
};
Use this.state = {error: null} and this.state = {error} instead of setState in then blocks of interceptors.
Regarding this specific example I think the best solution is to use a "temporary" variable , call the use method on the interceptors, and the define the state. All this inside the constructor.
Like so:
constructor() {
super();
let tempErrorState = null;
this.requestInterceptor = axios.interceptors.request.use(req => {
tempErrorState = null;
return req;
});
this.resoponseInterceptor = axios.interceptors.response.use(res => res, error => {
tempErrorState = error;
});
this.state = {
error: tempErrorState
};
}
As per your condition, you can't use the componentDidMount() method. You MUST NOT use the render() method for setting up Axios interceptors because you are using withErrorHandler as a higher-order component which can lead to many interceptors eventually created while wrapping other components too. This is because for each render cycle, you are unnecessarily setting up an interceptor if you define it inside a render() method. You should set up an Axios interceptor only once in the component.
The constructor() would be the best place to set this up (as per the latest version of React where componentWillMount() is deprecated).
You can edit your code to include a constructor():
constructor(props) {
super(props);
this.state = {
error: null
};
// clear the error when sending a request
axios.interceptors.request.use(req => {
this.state = {
error: null
};
return req;
});
axios.interceptors.response.use(res => res, err => {
this.state = {
error: err
};
});
}
Notice that here, you are not using the setState() method which throws a warning in constructor but you are directly setting the state, which is allowed only inside the constructor().

React + Axios - Map returns is not a function?

I'm working on my first API with React. I am able to console log my current state after its loaded and the state for that array is set. However, running my component with a prop "FragrancesArray" which is set after loading the data from this.state.fragrances returns in not a function.
Using axios async and await.
No idea why? Can someone help?
Thanks.
My code:
// Core React
import React, { Component } from 'react';
// Axios
import axios from 'axios';
// Constants
import { FRAGRANCES_URL, BLOGS_URL, MAKE_UP_URL } from 'constants/import';
// Components
import Fragrances from 'components/Fragrances/Fragrances';
class App extends Component {
state = {
fragrances: [],
blogs: [],
makeup: []
}
getCoffee() {
return new Promise(resolve => {
setTimeout(() => resolve('☕'), 0); // it takes 1 seconds to make coffee
});
}
async showData() {
try {
// Coffee first
const coffee = await this.getCoffee();
console.log(coffee); // ☕
// Axios API's
const fragranceData = axios(FRAGRANCES_URL);
const blogData = axios(BLOGS_URL);
const makeupData = axios(MAKE_UP_URL);
// await all three promises to come back and destructure the result into their own variables
await Promise.all([fragranceData, blogData, makeupData])
.then((data) => {
this.setState({
fragrances: data[0],
blogs: data[1],
makeup: data[2]
});
const { blogs } = this.state;
console.log(blogs);
})
} catch (e) {
console.error(e); // 💩
}
}
componentDidMount() {
this.showData();
}
render() {
return (
<Fragrances FragranceArray={this.state.fragrances} AppURL={FRAGRANCES_URL} />
)
}
}
export default App;
In react, before you can set/use state, you need to declare it with getInitialState() but with ES6 class model you initialize state in a constructor.
class App extends Component {
constructor(props) {
super(props)
//- Initialize default state values
this.state = {
fragrances: [],
blogs: [],
makeup: []
}
}
//The rest of code stays the same.
render() {
return (
<Fragrances FragranceArray={this.state.fragrances} AppURL={FRAGRANCES_URL} />
)
}
}
More about React state

how to test a react component after data is fetch in componentDidMount?

I have a react component that renders conditionally (renders if data is fetched otherwise returns null) and I want to test this with jest & enzyme. The problem that I'm having is I want to test one of the methods in the class but .instance() keeps returning null so it doesn't allow me to test the instance.
my code looks something like this
export default class MyComponent extends React.Component<Props, State> {
componentDidMount() {
this.props.fetchData.then(() =>
this.setState({ loaded: true });
);
}
methodThatIWantToTest() {
//do some stuff here
}
render() {
if (this.loaded) {
// render stuff here
} else {
return null;
}
}
}
and in the test I want to test
describe('myComponent', () => {
it('should do some stuff', () => {
const shallowWrapper = shallow(<MyComponent {...props}/>);
const method = shallowWrapper.instance().methodThatIWantToTest();
....such and such
});
});
but it looks like MyComponent only returns null so shallowWrapper.instance() returns null as well. I tried shallowWrapper.update() and many other things but it seems it doesn't want to render at all.. How do I wait for my component to be updated and then starts expect statement?
has anyone had a similar issue as mine and know how to work around this?
It is render result and not an instance that is null. shallowWrapper.instance() is an instance of component class, it cannot be null for stateful component. As the reference states:
Returns (React 16.x)
ReactComponent: The stateful React component instance.
null: If stateless React component was wrapped.
While shallowWrapper.html() will be initially null indeed.
There is a mistake in original code, it should be this.state.loaded and not this.loaded:
MyComponent extends React.Component {
state = { loaded: false };
componentDidMount() {
this.props.fetchData.then(() => {
this.setState({ loaded: true });
});
}
methodThatIWantToTest() {
//do some stuff here
}
render() {
if (this.state.loaded) {
return <p>hi</p>;
} else {
return null;
}
}
}
componentDidMount and methodThatIWantToTest should be preferably considered different units. They belong to different tests. In case methodThatIWantToTest is called in lifecycle hooks, it may be stubbed in componentDidMount test:
it('should fetch data', async () => {
const props = { fetchData: Promise.resolve('data') };
const shallowWrapper = shallow(<MyComponent {...props}/>);
expect(shallowWrapper.html()).toBe(null);
await props.fetchData;
expect(shallowWrapper.html()).toBe('<p>hi</p>');
});
Then the method can be tested separately. Lifecycle hooks can be disabled to reduce number of moving parts:
it('should do some stuff', () => {
const shallowWrapper = shallow(<MyComponent {...props}/>, {disableLifecycleMethods: true});
const result = shallowWrapper.instance().methodThatIWantToTest();
expect(result).toBe(...);
});
Here is a working example:
myComponent.js
import * as React from 'react';
export default class MyComponent extends React.Component {
constructor(...props) {
super(...props);
this.state = { loaded: false };
}
componentDidMount() {
this.props.fetchData().then(() =>
this.setState({ loaded: true })
);
}
methodThatIWantToTest() {
return 'result';
}
render() {
if (this.state.loaded) {
return <div>loaded</div>;
} else {
return null;
}
}
}
myComponent.test.js
import * as React from 'react';
import { shallow } from 'enzyme';
import MyComponent from './myComponent';
describe('myComponent', () => {
it('should do some stuff', async () => {
const fetchData = jest.fn(() => Promise.resolve());
const props = { fetchData };
const shallowWrapper = shallow(<MyComponent {...props}/>);
expect(shallowWrapper.html()).toBe(null);
expect(shallowWrapper.instance().methodThatIWantToTest()).toBe('result');
// pause the test and let the event loop cycle so the callback
// queued by then() within componentDidMount can run
await Promise.resolve();
expect(shallowWrapper.html()).toBe('<div>loaded</div>');
});
});

componentDidMount not firing in React

import React from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
import Post from './components/Post.jsx';
import Feed from './components/Feed.jsx';
class App extends React.Component {
constructor() {
super();
this.state = {
view: 'feed',
collection: ''
}
}
componentDidMount() {
console.log('component did mount')
this.getCollection();
}
async getCollection() {
try {
const response = await fetch('/api/page');
const responseJSON = await response.json();
this.setState({ collection: responseJSON }, () => {
console.log("App Component - getCollection() State Updated", this.state);
});
} catch (error) {
console.log("App Component - getCollection() error", error);
}
}
render() {
return (
<div>
Text
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
Can't get component did mount to function. Trying to make an ajax request to my mongodb and render that on the client side. All I need to do now is make a state change setting the collection to the information I get back. I tried using an Ajax request but that didnt work. Now I'm implementing an async fetch call as per the suggestion of another contributor.
the nonworking ajax request:
As of now, componentDidMount is still not being triggered and the collection property of the state is still an empty string.
I would recommend using the Fetch API for AJAX calls and making use of ES6 Async/Await, since importing an entire library just for Ajax seems a bit overkill.
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
collection: ''
}
this.getCollection = this.getCollection.bind(this)
}
componentDidMount() {
console.log('component did mount')
this.getCollection();
}
async getCollection() {
try {
const response = await fetch('/api/blogs');
const responseJSON = await response.json();
this.setState({ collection: responseJSON }, () => {
console.log("App Component - getCollection() State Updated", this.state);
});
} catch (error) {
console.log("App Component - getCollection() error", error);
}
}
render() {
return (
<div>
<h1>Welcome</h1>
</div>
);
}
}
ReactDOM.render(<App /> , document.getElementById('app'));
I'm not sure what you're doing with your render, but I've left it out. Hopefully, this will shed some light on how best to perform what you want.
To get componentDidMount to fire you need the render function. Because first the component renders and then it calls the function componentDidMount.
I think adding this to your class should solve your problem.
render() {
return null;
}

beginner at react.js - rendering component from state which is updated later

I'm trying to test rendering a list component from an JSON object called with fetch, however I suspect the render is called before the JSON is returned as I get an undefined error. Note the debug alert() gets the value, but the render has already error on undefined. How do I handle this? Is there a way to call the render function after the data has been fetched? OR could how would I get the data to render with '?' and then update when the data arrives? Note my if (!object) doesn;t work when called as an inline wrapper - why not - or more impoartanly how would I achive this? Sorry for the begginer questions - I suspect I meant to be using a data framework to bring data for react to display but I wanted to start learning with less frameworks and get the basics. Many thanks! - code is:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class ItemLister extends React.Component {
constructor(props ) {
super(props);
this.state = { items: [] };
}
componentDidMount() {
let url = "http://localhost:8888";
let iterator = fetch(url, {method: 'GET'});
iterator
.then(response => {
//console.log('sss', response)
return response.json();
})
.then(post => {
//console.log(post)
this.state.items = post;
alert(this.state.items.users[3].firstname);
});
}
output(object){
if (!object){
return '?';
}else{
return object;
}
}
render() {
return(
<div>
<div>Items:</div>
<div>{this.output(this.state.items.users[3].firstname)}</div>
</div>
);
}
}
export default ItemLister;
your render function could be something like this:
render() {
const username = (this.state.items === []) ? placeholder : this.output(this.state.items.users[3].firstname;
return(
<div>
<div>Items:</div>
<div>{username}</div>
</div>
);
}
basically, you render a placeholder component until your data arrives, since your data fetch is async and in componentDidMount, so your render will happen initially before you have data.
although you probably want to rethink how this component is constructed in general, why are you accessing specifically users[3]?
Try this code it will help you
import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";
class ItemLister extends Component {
constructor(props) {
super(props);
this.state = { items: [], firstname: "" };
}
componentDidMount() {
let url = "http://localhost:8888";
let iterator = fetch(url, { method: "GET" });
iterator
.then(response => {
//console.log('sss', response)
return response.json();
})
.then(post => {
//console.log(post)
this.setState({
items: post,
firstname: this.state.items.users[3].firstname
});
alert(this.state.items.users[3].firstname);
});
}
output(object) {
if (!object) {
return "?";
} else {
return object;
}
}
render() {
return (
<div>
<div>Items:</div>
<div>{this.state.firstname}</div>
</div>
);
}
}
export default ItemLister;

Resources