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);
Related
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 };
});
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 have this component (simplified version):
export default class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
data: {}
};
}
componentDidUpdate(prevProps, prevState) {
if(this.props.time && this.props.time !== prevProps.time){
this.setState({
isLoading: true
})
fetch(...).then(data => {
this.setState({
data: data
isLoading:false
}
}
}
render(){
{isLoading, data} = this.state;
return (isLoading ? /*show spinner*/ : /* show data*/);
}
}
This component works: it shows a spinner while fetching data, then it shows the data.
I'm trying to test it using jest and enzyme:
test('Mounted correctly', async() => {
let myComponent = mount(<MyComponent time='01-01-18'/>);
myComponent.setProps({time: '02-01-18'}); //necessary to call componentDidUpdate
expect(myComponent.state()).toMatchSnapshot();
}
From my knowledge, in order to call componentDidUpdate you have to call setPros (link). However, following the debugger, the call end when hitting:
this.setState({
isLoading: true
})
Which is kinda of expected, the problem is that the snapshot is:
Object {
"isLoading": true
"data": {}
}
Which is, of course, something that I don't want. How can I solve this?
UPDATE: I found a(n ugly) solution!
The problem is that what we want to test is this setState is completed:
this.setState({
data: data
isLoading:false
}
Now, this doesn't happen even by setting await myComponent.setProps({time: '02-01-18'}); (as suggested in one of the answers), because it doesn't wait for the new asynchronous call created by the setState described above.
The only solution that I found is to pass a callback function to props and call it after setState is completed. The callback function contains the expect that we want!
So this is the final result:
test('Mounted correctly', async() => {
let myComponent = mount(<MyComponent time='01-01-18'/>);
const callBackAfterLastSetStateIsCompleted = () => {
expect(topAsins.state()).toMatchSnapshot();
}
myComponent.setProps({time: '02-01-18', testCallBack: callBackAfterLastSetStateIsCompleted}); //necessary to call componentDidUpdate
expect(myComponent.state()).toMatchSnapshot();
}
And modify the component code as:
this.setState({
data: data
isLoading:false
},this.props.testCallBack);
However, as you can see, I'm modifying a component in production only for testing purpose, which is something very ugly.
Now, my question is: how can I solve this?
All you need to do here to test is make use of async/await like
test('Mounted correctly', async () => {
let myComponent = mount(<MyComponent time='01-01-18'/>);
await myComponent.setProps({time: '02-01-18'}); //necessary to call componentDidUpdate, await used to wait for async action in componentDidUpdate
expect(myComponent.state()).toMatchSnapshot();
}
I'm using React Native 0.43. I've one component, named ApiComponent. In componentWillMount method of this component, I'm fetching some results from an API and I want this result in my render method. I'm using following code (abridged version) in my component:
export default class ApiComponent extends Component {
constructor(props) {
super(props);
this.state = {
statement: {},
};
}
componentWillMount() {
fetch('http://example.com/api_url/')
.then(response => response.json())
.then(data => this.setState({ statement: data }))
.catch(error => console.error(error));
}
render() {
return (
<Text>{'Rendering API: ' + console.log(this.state.statement)}</Text>
);
}
}
Now, when I run this code I get an empty result in my console Rendering API: {}. As per my understanding, the render method executes before the results are returned from the API and therefore the state is not being updated with the results.
My question is, how I can make sure that my render method only executes when the code in my componentWillMount completes its execution?
You can use a ternary operation to ensure the text only renders if this.state.statement is true
return (
{ this.state.statement ? <Text>{'Rendering API: ' + console.log(this.state.statement)}</Text> : null }
)