reactjs storing state in constructor vs property - javascript

I have a very simple react component that needs to connect to an API and retrieve some JSON data, which will then be used for displaying some information.
In the following class/component, I have mounted and state as a property. I normally use a constructor to hold my states but in this case, if I move the state to a constructor, I cannot seem to access the data (projectInfo) inside the renderer. When inside the renderer (line containinig {projectInfo.name}), I get the error: TypeError: Cannot read property 'name' of null
How can I use the constructor in this class to hold the state? Why does the following class work but not when I use a constructor? What is the convention for handling something like this?
class MyReportSummary extends Component {
mounted = true;
state = {
projectInfo: null,
isLoading: true,
error: null
};
componentDidMount() {
fetch(`/api/projects/${this.props.projectId}`)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error("Encountered problem fetching project info");
}
})
.then(data => {
if (this.mounted) {
this.setState({
projectInfo: data
});
}
})
.catch(fetchError => {
if (this.mounted) {
this.setState({
isLoading: false,
error: fetchError
});
}
});
}
componentWillUnmount() {
this.mounted = false;
}
render() {
const { isLoading, error, projectInfo } = this.state;
if (error) {
return <p>{error.message}</p>;
}
if (isLoading) {
return <p>Loading...</p>;
}
return (
<div className="myReportSummary">
Summary of Project name: {projectInfo.name}
Number of events: {this.props.data.length}
</div>
);
}
}
UPDATE: Just for clarity, the above sample code works just fine. What I'm trying to understand is if my class look like this that has a constructor initializing state, then I get that TypeError.
class MyReportSummary extends Component {
mounted = true;
constructor(props) {
super(props);
this.state = {
projectInfo: null,
isLoading: false,
error: null
};
}
componentDidMount() {
// same as the previous sample code
}
componentWillUnmount() {
this.mounted = false;
}
render() {
//same as the previous sample code
}
}
What is the correct convention for states? Is constructor not the proper way of doing this?

Initializing state in the constructor like your second example is perfectly valid, but your two examples are not the same. You are setting isLoading to true in the class property version, but isLoading to false in the constructor.
If error in null and isLoading is false you will hit the last part of your render method. Since projectInfo is null before your request is complete, you will try to access name on null and get your error.
Set isLoading to true and it should work as expected, or even projectInfo to an empty object {}, but then you will not get the loading indicator.
class MyReportSummary extends Component {
constructor(props) {
super(props);
this.mounted = true;
this.state = {
projectInfo: {},
isLoading: true,
error: null
};
}
// ...
}

Related

Returning state after API call inside getDerivedStateFromProps

I have a react component that receives props for filtering from its parent. When the parent props change I use getDerivedStateFromProps in the child in the following code:
static getDerivedStateFromProps(props, state) {
if (props.filter === null) {
return state;
}
const { name, description } = props.filter;
ApiService.get("cards", { name: name, description: description })
.then(response => {
console.log("get request returned ", response.data);
return { cards: response.data };
}
);
}
in the console the response.data log is the approriate array of objects. However the state does not update and the rendering function still uses the old cards array and not the one that was received from the ApiService response. How do I make it so the cards array updates properly so that on the next render it will show the filtered cards?
getDerivedStateFromProps is not the correct lifecycle hook for this. You'll need to put the fetching code in componentDidMount and componentDidUpdate, and use this.setState once the data is available.
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
cards: null,
}
}
componentDidMount() {
this.loadData();
}
componentDidUpdate(prevProps) {
if (this.props.filter !== prevProps.filter) {
this.loadData();
}
}
loadData() {
const { name, description } = this.props.filter;
ApiService.get("cards", { name: name, description: description })
.then(response => {
this.setState({ cards: response.data });
});
)
render() {
if (!this.state.cards) {
return <div>Loading...</div>
}
return (
// some jsx using the cards
)
}
}

Console.log() shows undefined before getting data

I seem to have a lifecycle hook issue that I can't seem to solve.
export default class EditRevision extends Component {
state = {
data: [],
customColumns: []
}
componentWillMount = () => {
axios.get('http://localhost:8080/lagbevakning/revision/subscriptions?id=' + (this.props.match.params.id)).then(response => {
this.setState({
data: response.data,
loading: false
})
})
}
render() {
/* THIS IS THE CONSOLE.LOG() I AM REFERRING TO */
console.log(this.state.data.subscriptionRevisionDTOS)
return (
<div></div>
)
}
}
And this is my log upon rendering the component
https://i.gyazo.com/9dcf4d13b96cdd2c3527e36224df0004.png
It is undefined, then retrieves the data as i desire it to, then it gets undefined again.
Any suggestions on what causes this issue is much appreciated, thank you.
Replace this:
componentWillMount = () => {
axios.get('http://localhost:8080/lagbevakning/revision/subscriptions?id=' + (this.props.match.params.id)).then(response => {
this.setState({
data: response.data,
loading: false
})
})
with:
constructor(props){
super(props)
this.state = {
data: [],
customColumns: []
}
axios.get('http://localhost:8080/lagbevakning/revision/subscriptions?id=' + (this.props.match.params.id)).then(response => {
this.setState({
data: response.data,
loading: false
})
})
}
try to call axios in constructor or componentDidMount() (componentWillMount should not be used). the undefined result is caused by the async call. Looks like you have a lot of uncontrolled renders. try to add a shouldComponentUpdate function or convert your component in a PureComponent
Take a look at https://reactjs.org/docs/react-component.html
You have init the state with
state = {
data: [],
customColumns: []
}
Here this.state.data is empty array which did not have definition of
subscriptionRevisionDTOS that is why you are getting this.state.data.subscriptionRevisionDTOS undefined.
Meanwhile, your asyncaxios.get call is completed and this.state.data is updated with subscriptionRevisionDTOS.
As soon as state is updated render() called again and you are getting the proper value of this.state.data.subscriptionRevisionDTOS.
So below line will surely work.
state = {
data:{subscriptionRevisionDTOS:[]},
customColumns: []
}
export default class EditRevision extends Component {
state = {
data:{subscriptionRevisionDTOS:[]},
customColumns: []
}
componentDidMount = () => {
axios.get('http://localhost:8080/lagbevakning/revision/subscriptions?id=' +
(this.props.match.params.id)).then(response => {
this.setState({
data: response.data,
loading: false
})
})
render() {
/* THIS IS THE CONSOLE.LOG() I AM REFERRING TO */
console.log(this.state.data.subscriptionRevisionDTOS)
return (
<div></div>
)
}
see this it should be like this

React setState twice renders state

When I trade with setState, it turns twice. First the Data Fail is returned and then the Data Ready is returned. Console return false first then true. I just want to return true in the console.
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
data: []
};
}
componentDidMount() {
fetch("https://reqres.in/api/users?page=2")
.then(res => res.json())
.then(result => {
this.setState({
data: result.data,
loading: true
});
});
}
render() {
const { loading } = this.state;
console.log(loading);
const dataReady = <div>Data Ready </div>;
const DataFail = <div>Data Fail </div>;
return <div>{loading === true ? dataReady : DataFail}</div>;
}
}
Example:
https://codesandbox.io/s/kopz8wrl7o
You simply reversed the boolean associated with the loading. You should initially set it to true and then to false when your data is loaded.
Working example :
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
error: false,
data: []
};
}
componentDidMount() {
fetch("https://reqres.in/api/users?page=2")
.then(res => res.json())
.then(result => {
this.setState({
data: result.data,
loading: false
});
})
.catch(error =>{
this.setState({ error: true });
});
}
render() {
const { loading, error } = this.state;
!loading && console.log(true)
return <div>{!loading && <div>Data Ready </div>}
{error && <div>Data Error :( </div>}
</div>;
}
}
ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.1.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.1.1/umd/react-dom.production.min.js"></script>
<div id='root'/>
your data changes that's why you have two messages:
you need did decision, what's you gonna do?
if you didn't want change state you can use shouldComponentUpdate, or you can just skip 1 console message
It is due to the React's lifecycle and the fact that you are setting the state.loading property initially to false. https://reactjs.org/docs/react-component.html
First there is the initial render, then the componentDidMount with its setState is called. You might want to leave the loading undefined and then in the render method check if its defined or not before checking its value.

React Warning: Can't call setState (or forceUpdate) on an unmounted component

I have 2 components:
Orders - fetch some data and display it.
ErrorHandler - In case some error happen on the server, a modal will show and display a message.
The ErrorHandler component is warping the order component
I'm using the axios package to load the data in the Orders component, and I use axios interceptors to setState about the error, and eject once the component unmounted.
When I navigate to the orders components back and forward i sometimes get an error in the console:
Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
in Orders (at ErrorHandler.jsx:40)
in Auxiliary (at ErrorHandler.jsx:34)
in _class2 (created by Route)
I tried to solve it by my previous case React Warning: Can only update a mounted or mounting component but here I can't make an axios token by the inspectors. Has anyone solved this issue before?
Here are my components:
Orders:
import React, { Component } from 'react';
import api from '../../api/api';
import Order from '../../components/Order/Order/Order';
import ErrorHandler from '../../hoc/ErrorHandler/ErrorHandler';
class Orders extends Component {
state = {
orders: [],
loading: true
}
componentDidMount() {
api.get('/orders.json')
.then(response => {
const fetchedOrders = [];
if (response && response.data) {
for (let key in response.data) {
fetchedOrders.push({
id: key,
...response.data[key]
});
}
}
this.setState({ loading: false, orders: fetchedOrders });
})
.catch(error => {
this.setState({ loading: false });
});
}
render() {
return (
<div>
{this.state.orders.map(order => {
return (<Order
key={order.id}
ingrediencies={order.ingrediencies}
price={order.price} />);
})}
</div>
);
}
}
export default ErrorHandler(Orders, api);
ErrorHandler:
import React, { Component } from 'react';
import Auxiliary from '../Auxiliary/Auxiliary';
import Modal from '../../components/UI/Modal/Modal';
const ErrorHandler = (WrappedComponent, api) => {
return class extends Component {
requestInterceptors = null;
responseInterceptors = null;
state = {
error: null
};
componentWillMount() {
this.requestInterceptors = api.interceptors.request.use(request => {
this.setState({ error: null });
return request;
});
this.responseInterceptors = api.interceptors.response.use(response => response, error => {
this.setState({ error: error });
});
}
componentWillUnmount() {
api.interceptors.request.eject(this.requestInterceptors);
api.interceptors.response.eject(this.responseInterceptors);
}
errorConfirmedHandler = () => {
this.setState({ error: null });
}
render() {
return (
<Auxiliary>
<Modal
show={this.state.error}
modalClosed={this.errorConfirmedHandler}>
{this.state.error ? this.state.error.message : null}
</Modal>
<WrappedComponent {...this.props} />
</Auxiliary>
);
}
};
};
export default ErrorHandler;
I think that's due to asynchronous call which triggers the setState, it can happen even when the component isn't mounted. To prevent this from happening you can use some kind of flags :
state = {
isMounted: false
}
componentDidMount() {
this.setState({isMounted: true})
}
componentWillUnmount(){
this.state.isMounted = false
}
And later wrap your setState calls with if:
if (this.state.isMounted) {
this.setState({ loading: false, orders: fetchedOrders });
}
Edit - adding functional component example:
function Component() {
const [isMounted, setIsMounted] = React.useState(false);
useEffect(() => {
setIsMounted(true);
return () => {
setIsMounted(false);
}
}, []);
return <div></div>;
}
export default Component;
You can't set state in componentWillMount method. Try to reconsider your application logic and move it into another lifecycle method.
I think rootcause is the same as what I answered yesterday, you need to "cancel" the request on unmount, I do not see if you are doing it for the api.get() call in Orders component.
A note on the Error Handling, It looks overly complicated, I would definitely encourage looking at ErrorBoundaries provided by React. There is no need for you to have interceptors or a higher order component.
For ErrorBoundaries, React introduced a lifecycle method called: componentDidCatch.
You can use it to simplify your ErrorHandler code to:
class ErrorHandler extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
this.setState({ hasError: true, errorMessage : error.message });
}
render() {
if (this.state.hasError) {
return <Modal
modalClosed={() => console.log('What do you want user to do? Retry or go back? Use appropriate method logic as per your need.')}>
{this.state.errorMessage ? this.state.errorMessage : null}
</Modal>
}
return this.props.children;
}
}
Then in your Orders Component:
class Orders extends Component {
let cancel;
state = {
orders: [],
loading: true
}
componentDidMount() {
this.asyncRequest = api.get('/orders.json', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
})
.then(response => {
const fetchedOrders = [];
if (response && response.data) {
for (let key in response.data) {
fetchedOrders.push({
id: key,
...response.data[key]
});
}
}
this.setState({ loading: false, orders: fetchedOrders });
})
.catch(error => {
this.setState({ loading: false });
// please check the syntax, I don't remember if it is throw or throw new
throw error;
});
}
componentWillUnmount() {
if (this.asyncRequest) {
cancel();
}
}
render() {
return (
<div>
{this.state.orders.map(order => {
return (<Order
key={order.id}
ingrediencies={order.ingrediencies}
price={order.price} />);
})}
</div>
);
}
}
And use it in your code as:
<ErrorHandler>
<Orders />
</ErrorHandler>

Passing parameters in ReactJS

I'm trying to navigate between a master detail data format in React. The source page does this:
{myDataList.map(myData =>
<tr key={myData.dataId}>
<td>{myData.dataId}</td>
<td>{myData.dataDescription}</td>
<td>
<NavLink to={'/details/' + myData.dataId}>
Details
</NavLink>
</td>
Clicking the edit link does navigate to the details page with the correct URL; which does this (mainly taken from the example template in VS):
interface DetailsState {
details?: DataDetails;
loading: boolean;
}
export class Details extends React.Component<RouteComponentProps<number>, DetailsState> {
constructor() {
super();
this.state = { details: undefined, loading: true };
console.log("match params: " + this.props.match.params);
fetch('api/Data/Details/' + this.props.match.params)
.then(response => response.json() as Promise<DataDetails>)
.then(data => {
this.setState({ details: data, loading: false });
});
}
My problem is that the console.log like above shows:
TypeError: _this.props is undefined
And so I can't access the dataId that I'm passing through.
Clearly I'm passing (or receiving) this parameter incorrectly; so my question is: what is the correct way to pass the parameter? It's worth noting that my project is based on the VS Template, and I am using tsx, rather than jsx files.
You cannot access this.props in a component's constructor. However, it is available as the first parameter. You can do this:
export class Details extends React.Component<RouteComponentProps<number>, DetailsState> {
constructor(props) {
super(props);
this.state = { details: undefined, loading: true };
console.log("match params: " + props.match.params);
fetch('api/Data/Details/' + props.match.params)
.then(response => response.json() as Promise<DataDetails>)
.then(data => {
this.setState({ details: data, loading: false });
});
}
}
Make sure that you call super(props) as the first thing in the constructor, or else React will yell at you.
EDIT:
As #Dan notes, it's generally not avisable to perform asynchronous actions in the constructor. Better to do that in componentDidMount():
export class Details extends React.Component<RouteComponentProps<number>, DetailsState> {
constructor(props) {
super(props);
this.state = { details: undefined, loading: true };
console.log("match params: " + props.match.params);
}
componentDidMount() {
fetch('api/Data/Details/' + this.props.match.params)
.then(response => response.json() as Promise<DataDetails>)
.then(data => {
this.setState({ details: data, loading: false });
});
}
}
If you want to access props in your constructor, you need to pass them in:
constructor(props) {
super(props);
}

Categories

Resources