I'm pretty new to react. I'm building a web app that is working pretty well but I'm not sure I'm handling state changes correctly. For instance, I have a method that gets called in componentDidMount.
It's seems inefficient and bad practice to keep calling setState but I'm not really sure of a better way to do it.
I would love some feedback.
class AuditScreen extends Component {
constructor(props) {
super(props);
this.state = {
currentVehicle: null,
currentVehicleIndex: 0,
vehicles: [],
activePictureIndex: 0,
show: false,
loading: false,
next_page: `${props.backend.id}/images`,
};
}
componentDidMount() {
this.getImages()
}
getImages() {
if (!this.state.loading) {
this.setState({
loading: true,
});
this.registerScrollEvent();
axios.get(this.state.next_page)
.then((response) => {
const paginator = response.data;
const vehicles = paginator.data.filter((vehicle) => {
if (vehicle.enhanced && vehicle.enhanced.length) {
return vehicle;
}
});
if (vehicles && vehicles.length) {
this.setState({
vehicles: [...this.state.vehicles, ...vehicles],
next_page: paginator.next_page_url,
});
if (this.state.currentVehicle === null) {
this.setState({
currentVehicle: vehicles[0]
});
}
}
// remove scroll event if next_page_url is null
if (!paginator.next_page_url) {
this.removeScrollEvent();
}
})
.finally((response) => {
this.setState({
loading: false,
});
});
}
}
}
.....
}
Thanks!
Like #Dave Newton says, I don't think this is bad practice. Updating the state doesn't immediately trigger an update. Instead, state updates often (but not always) batch up and then trigger a single update.
This article explains the batching mechanism in detail. Up to React 17, state-update batching only occurs within event handlers and within componentDidMount. They give an explicit example where "setState is called inside componentDidMount which causes only one extra update (and not three)".
So React already does what you want. React 18 is going to give you more control over the batching behavior, and more automatic batching. I found this description helpful for understanding what's coming and also how React 17 and lower currently work.
if you don't want to stop calling the setState method in react, use context for small project or redux to keep states and reducers with functional components. Best regards.
Related
I'm new to react and having a super hard time. My most recent problem is trying to set the state of 'favMovies' to an array of strings (movie IDs).
States:
export class MainView extends React.Component {
constructor() {
super();
this.state = {
movies: [],
favMovies: [],
user: null,
};
}
Setting states:
onLoggedIn(authData) {
console.log(authData);
console.log(authData.user.FavoriteMovies);
this.setState({
favMovies: authData.user.FavoriteMovies,
});
this.setState({ user: authData.user });
localStorage.setItem('token', authData.token);
localStorage.setItem('user', JSON.stringify(authData.user));
this.getMovies(authData.token);
this.getUsers();
}
I kind of understand that set state is async and doesn't happen until the next render. The part that I'm confused by is the 'user' that get's set after 'favMovies' works as expected, but 'favMovies' is undefined.
I know this is probly a dumb question, but I'm absolutely lost in react right now and struggling. Any help would be appreciated.
It's alaways better to use single setState if possible, because everytime you call setState it will re-render the view. One more tip for you, when you are dealing with object and arrays try to use spread operstor to assign to the state, instead of direct assignment.
onLoggedIn(authData) {
console.log(authData);
console.log(authData.user.FavoriteMovies);
this.setState({
favMovies: [...authData.user.FavoriteMovies],
user: authData.user
});
//this.setState({ user: authData.user });
localStorage.setItem('token', authData.token);
localStorage.setItem('user', JSON.stringify(authData.user));
this.getMovies(authData.token);
this.getUsers();
}
As we know react setState is asynchronous, state won't reflect immediately. We can use callback with setState where we can access updated state.
this.setState(
{
favMovies: [...authData.user.FavoriteMovies],
user: authData.user
// ...
},
() => {
this.doSomethingAfterStateUpdate();
}
);
I'm using gatsby for server side rendering.
Here's my code:
class BookSearch extends Component {
state = {
search: '',
books: '',
};
componentDidMount() {
this.loadData()
}
loadData () {
axios.get('/books/list')
.then(response => {
this.setState({books: response.data.books});
console.dir(response.data.books);
})
.catch(error => {
this.setState({error: true});
});
}
Unfortunately, this.setState does not work in gatsby. componentDidMount is not being called when I load the page. What should I do?
I think the issue is of binding this to loadData method.
You can bind this in 2 ways.
Bind this in the constructor,
constructor(props){
super(props)
this.state = {
search: '',
books: '',
}
this.loadData = this.loadData.bind(this) //Bind this here
}
Or you can simply use arrow function,
loadData = () => { //Arrow function auto binds `this`
axios.get('/books/list')
.then(response => {
this.setState({
books: response.data.books
});
console.dir(response.data.books);
})
.catch(error => {
this.setState({error: true});
});
}
I think you should have got an error? It's because you have not initialized error state. You must initialize state before you can use them:
state = {
search: '',
books: '',
error: false
};
I hope this may fix the issue. Otherwise, I couldn't see any issue in your code.
You mentioned you're using SSR?
Try using componentWillMount in this case, since componentDidMount is not called in SSR.
In case you're using react version > 16.3:
When supporting server rendering, it’s currently necessary to provide the data synchronously – componentWillMount was often used for this purpose but the constructor can be used as a replacement. The upcoming suspense APIs will make async data fetching cleanly possible for both client and server rendering.
Reference: https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data
In your case, I think it would make more sense to use the getInitialProps static method. (https://nextjs.org/learn/basics/fetching-data-for-pages/fetching-batman-shows)
If you're not very familiar with SSR, Next.js has great tutorials:
https://nextjs.org/learn/basics/getting-started
This may help you out!
I'm having a trouble implementing shouldComponentUpdate on my React app with loadingData state. I want to prevent component to load again when nextProps has not changed from the this.props which is working fine and it messes up loadingData value some how and I can't find the reason why.
Why loadingData ended up to be true even there's no change to the data in Redux(nextProps and this.props)?
constructor(props){
super(props)
this.state = {
loadingData: false,
}
}
async componentDidMount() {
this._isMounted = true;
const { username } = this.state
this.setState({ loadingData: true })
try {
await this.props.getUserData(username)
this.setState({ loadingData: false })
} catch (err) {
console.log(err)
this.setState({ loadingData: false })
}
}
shouldComponentUpdate(nextProps, nextState) {
return this.props !== nextProps
}
render() {
return(
<div>{this.state.loadingData ? "Loading..." : this.props.userData}</div>
)
}
Updated code to show the how I set up loadingData state. Some reason, adding shouldComponentUpdate shows, Loading... on the screen instead of userData from Redux. I'm not sure why...
This will always return false because this.props is a different object than nextProps between each render.
For the simplest example of this:
console.log({} === {}) // false
By default (if using React.PureComponent) React will perform a "shallow equality" check. It will check the equality of each prop (but will not do so recursively because of performance reasons). (see the source).
As a first attempt, have you tried using React.PureComponent in place of React.Component?
If that doesn't help, I would recommend sharing the entire code snippet. shouldComponentUpdate usually is considered a "code smell" and commonly means there's a problem with another part of the code. It should not be used as control flow, but only for performance optimization.
If you still must implement shouldComponentUpdate, take a look at the default shallowEqual helper for a bit of inspiration.
Maybe you should look through this.
var jangoFett = {
occupation: "Bounty Hunter",
genetics: "superb"
};
var bobaFett = {
occupation: "Bounty Hunter",
genetics: "superb"
};
var callMeJango = jangoFett;
// Outputs: false
console.log(bobaFett === jangoFett);
// Outputs: true
console.log(callMeJango === jangoFett);
In the submit method of an Atlaskit Form, I want to change a value of a state property that results in the form being hidden:
<Form onSubmit={data => {
return new Promise(resolve => {
setShowForm(false);
resolve();
})
}}>
</Form>
However, this results in a React error:
Can't perform a React state update 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.
The error disappears when i set that value a little later:
setTimeout(() => setShowForm(false));
So apparently the form is still unmounting while i change state (although i don't know why that should affect on the form, but i am not too familiar with React yet). What is the approach i should be taking here?
This is because you made an asynchronous request to an API, the request (e.g. Promise) isn’t resolved yet, but you unmount the component.
You can resolve this issue by maintaining a flag say _isMounted to see if component is unmounted or not and change the flag value based on promise resolution.
// Example code
class Form extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentDidMount() {
this._isMounted = true;
axios
.get('my_api_url')
.then(result => {
if (this._isMounted) {
this.setState({
data: result.data.data,
});
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
...
}
}
I've recently came across this error in React:
warning.js:36 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. Please check the code for the
BillingDetails component.
After digging I found out that this is caused because I do setState in unmounted component like this:
componentWillMount() {
this.fetchBillings(this.props.userType);
}
componentWillReceiveProps({ userType }) {
if (this.props.userType !== userType) {
this.fetchBillings(userType);
}
}
fetchBillings = userType => {
switch (userType) {
case USER_TYPE.BRAND:
this.props.fetchBrandBillings()
.then(() => this.setState({ isLoading: false }));
return;
default:
}
};
fetchBillings is a redux-axios action creator which returns a promise
export const fetchBrandBillings = () => ({
type: FETCH_BRAND_BILLINGS,
payload: {
request: {
method: 'GET',
url: Endpoints.FETCH_BRAND_BILLINGS,
},
},
});
The problem is that when user moves fast on site, component can be unmounted at the time promise resolves.
I found out lot of places around the project where I do something like this:
componentWillMount() {
const { router, getOrder, params } = this.props;
getOrder(params.orderId).then(action => {
if (action.type.endsWith('FAILURE')) {
router.push(`/dashboard/campaign/${params.campaignId}`);
}
})
}
and now I begin to think that using Promises in components could be anti-pattern as component can be unmounted at any time...
The problem is that when user moves fast on site, component can be unmounted at the time promise resolves.
Since native promises are not interruptible, this is completely natural and should be expected at all times. You can overcome this in various ways, but you will ultimately need to track whether the component is still mounted, one way or another, and just don't do anything when the promise resolves/rejects if it's not.
Also, from the docs regarding componentWillMount:
Avoid introducing any side-effects or subscriptions in this method.
Considering this, I'd suggest using componentDidMount for initiating your fetch instead. Overall:
componentDidMount() {
this._isMounted = true;
this.fetchBillings(this.props.userType);
}
componentWillReceiveProps({ userType }) {
if (this.props.userType !== userType) {
this.fetchBillings(userType);
}
}
componentWillUnmount() {
this._isMounted = false;
}
fetchBillings = userType => {
switch (userType) {
case USER_TYPE.BRAND:
this.props.fetchBrandBillings().then(() => {
if (this._isMounted) {
this.setState({ isLoading: false });
}
});
return;
default:
}
};
Additionally, although this is not directly related to your question, you will need to consider that you will have multiple parallel fetch calls running in parallel, leading to a data race. That is, the following is just waiting to happen at any time:
start fetch0
start fetch1
finish fetch1 -> update
...
finish fetch0 -> update
To avoid this, you can track your requests with a timestamp.