Issues with state-changes in submit method of AtlasKit Form - reactjs

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() {
...
}
}

Related

Component did update works only after second click

My code adds a new item in the firebase databse when i click a button, then i want the list of objects in my page to automatically update, because i don't want to manualy reload the page. So i came up with this code
constructor(props){
super(props);
this.state = {
groups: [],
code:'',
name:'',
update:true
}
}
async fetchGroups (id){
fetchGroupsFirebase(id).then((res) => {this.setState({groups:res})})
};
async componentDidUpdate(prevProps,prevState){
if(this.state.update !== prevState.update){
await this.fetchGroups(this.props.user.id);
}
}
handleCreateSubmit = async event => {
event.preventDefault();
const{name} = this.state;
try{
firestore.collection("groups").add({
title:name,
owner:this.props.user.id
})
.then((ref) => {
firestore.collection("user-group").add({
idGroup:ref.id,
idUser:this.props.user.id
});
});
this.setState({update: !this.state.update});
}catch(error){
console.error(error);
}
What i was thinking, after i add the new item in firebase, i change the state.update variable, which triggers componentDidUpdate, which calls the new fetching.
I tried calling the fetchGroups function in the submit function, but that didn't work either.
What am i doing wrong and how could i fix it?
ComponentDidUpdate will not be called on initial render. You can either additionally use componentDidMount or replace the class component with a functional component and use the hook useEffect instead.
Regarding useEffect, this could be your effect:
useEffect(() => {
await this.fetchGroups(this.props.user.id);
}, [update]);
Since you can't use useEffect in class components so you would need to rewrite it as functional and replace your this.state with useState.

Why won't my changes to state show in my componentDidMount lifecycle?

I am building an app using React and for my homepage, I set state in the componentDidMount lifecycle:
export default class HomePage extends Component {
state = {
posts: [],
token: '',
};
//Display posts when homepage renders
componentDidMount() {
//If token exists, run lifecycle event
if (this.props.location.state.token) {
this.setState({ token: this.props.location.state.token });
}
Axios.get('http://localhost:3000/api/posts/all')
.then((req) => {
this.setState({ posts: req.data });
})
.catch((err) => {
console.log(err.message);
throw err;
});
console.log(this.state);
}
However when I run the console log at the end of the lifecycle method, it shows posts and token as still being empty. I know they are being populated because the posts from the req.data show up in my JSX. Why does it show state being empty when I console log inside the method?
React setState is asynchronous!
React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component.
Think of setState() as a request rather than an immediate command to update the component.
this.setState((previousState, currentProps) => {
return { ...previousState, foo: currentProps.bar };
});

How do I make Component rerender when props passed from parent and state changed?

I'm making a Todolist. The Submit component to handle input from user and make PUT request to the server to update the database. The Progress component shows the progress. When user submits, Submit will send props to Progress and Progress will call axios.get to get data and update the state. The props is passed successfully and the Progress state's variables do change but the component is not re-rendered.
submit.js
import Progress from './progress';
export default class Submit extends Component {
constructor(props) {
super(props);
this.state = {
updateProgress: 'No'
}
}
handleSubmit = (e) => {
// ...
this.setState({updateProgress: 'Yes'}));
}
render() {
return (
<div>
<div style={{visibility: 'hidden'}}>
<Progress update={this.state.updateProgress} />
</div>
// Form
</div>
)
}
}
progress.js
export default class Progress extends Component {
constructor(props) {
super(props);
this.state = {
tasksDone: 0
};
}
getData = () => {
axios.get('/task')
.then(res => {
let resData = res.data[0];
this.setState({tasksDone: resData.done});
})
.catch(err => console.log(err));
}
componentDidUpdate(prevProps) {
if (this.props.update != prevProps.update) {
console.log('Props changed');
this.getData();
}
}
componentDidMount() {
this.getData();
}
render() {
return (<div>You have done: {this.state.tasksDone}</div>)
}
}
I know getData() won't be called again after the first submit because the props passed doesn't change but that's not the case because it doesn't work the first submit already.
When I use console.log(this.state.tasksDone) in componentDidUpdate() the tasksDone is updated but the div is not re-rendered, even I tried using forceUpdate() after calling getData().
I have read other questions and they said mutating state doesn't trigger re-render. Is my setState just mutating? I see changing state this way is very common. What am I missing?
Reproduce problem: https://codesandbox.io/s/determined-curie-ipr6v
The problem lies in your use for Progress.
The Progress you are updating is not the Progress that is shown on the display.
https://codesandbox.io/s/vigilant-ardinghelli-vd9nl
this is the working code you provided.

re-render triggered in componentDidMount

In the life cycle of a component, if a re-render is triggered by some synchronous operation in componentDidMount(), would the user have a chance to see the first render content on browser?
e.g. If I toggle a start downloading boolean flag in componentDidMount() through redux, which then causes the re-render because the flag is mapped to redux for the component.
-------Update Info-----
The sync operation is just changing the start downloading flag to true, and the flag is mapped to the component, where the flag is checked to determine the JSX contents in render(). In redux, right after the flag is set to true, then the downloading operation begins. When downloading is completed, redux sets the flag to false.
Consider the following lifecycle sequence:
render() //JSX A
componentDidMount() // the flag is set
render() // re-render JSX B
Will JSX A be displayed in the browser, regardless of how quick it is?
the action creator called in componentDidMount():
export const downloadArticleList = () => {
return (dispatch, getState) => {
// set start flag to true synchronously, before axios.get
dispatch(listDownloadStart());
axios.get('/articles')
.then(response => {
//set the flag to false and update the data
dispatch(saveArticleList(response.data))
})
.catch(err => {
dispatch(serverFail(err))
console.log("[downloadArticleList]] axios", err);
})
}
}
It is a SPA, no SSR.
It depends on a few things:
How long sync operation takes
Are you doing SSR (thus there will be time dedicated for DOM rehydrating)
Generally, I'd consider this as an antipattern
As we discuss in the comment here is the example :
interface ExampleComponentProps {
}
interface ExampleComponentState {
loading: boolean;
}
export class ExampleComponent extends React.Component<ExampleComponentProps, ExampleComponentState>{
constructor(props, context) {
super(props, context);
this.state = { loading: true };
}
componentDidMount() {
//some method {}
//after get result
this.setState({
loading: false
})
}
render() {
return (
<div>
<Spin spinning={this.state.loading} >
//Your COmponent here
</Spin>
</div>
)
}
}
If your project is complicated, the easiest way is using
setTimeout(() => {
this.setState({
// your new flag here
})
}, 0);

Should Promises be avoided in React components?

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.

Resources