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();
}
Related
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.
The problem lays in the code below
class GroupsPage extends React.Component {
constructor(props){
super(props);
this.state = {
groups: [],
}
}
async fetchGroups (){
fetchGroupsFirebase().then((res) => {this.setState({groups:res})})
};
async componentDidMount() {
await this.fetchGroups();
}
render(){}
const mapStateToProps = createStructuredSelector({
user: selectCurrentUser
})
export default connect(mapStateToProps)(GroupsPage);
As you see , i call fetchGroups to get some data from firebase, it works allright but i want to get specific data for my current user, my problem is that i can't send de currentUser id as a param to the fetchGroupsFirebase functions, because at the time of the call, this.props.user is still null , and it gets the value from mapStateToProps only after the component mounted.
I hope that i am clear enough, i know it is messy
TLDR: I need the user id but when i get it it's too late
what you would need to do is to first check if the user prop is available right after the mount - if not skip the api call and wait untill the user prop gets updated using componentDidUpdate lifecycle method.
This way, your api call will be made as soon as the user prop gets injected to the component.
class GroupsPage extends React.Component {
constructor(props) {
super(props);
this.state = {
groups: [],
};
}
async fetchGroups(id) {
fetchGroupsFirebase(id).then((res) => {
this.setState({ groups: res });
});
}
async componentDidMount() {
if (this.props.user) {
await this.fetchGroups(this.props.user);
}
}
async componentDidUpdate(prevProps) {
if (this.props.user && this.props.user !== prevProps.user) {
await this.fetchGroups(this.props.user);
}
}
render() {}
}
const mapStateToProps = createStructuredSelector({
user: selectCurrentUser,
});
export default connect(mapStateToProps)(GroupsPage);
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);
I'm curious whether React batches updates to props in some rare cases? There is no mention of this in the docs, but I couldn't come up with any other explanation of the following situation.
I have an equivalent to the following code:
// Connected component
class MyComponent extends React.Component {
state = {
shouldDisplayError: false,
};
componentDidUpdate(prevProps) {
console.log("componentDidUpdate: " + this.props.dataState);
if (
prevProps.dataState === "FETCHING" &&
this.props.dataState === "FETCH_FAILED"
) {
this.setState(() => ({ shouldDisplayError: true }));
}
}
render() {
return this.state.shouldDisplayError && <p>Awesome error message!</p>;
}
}
const mapStateToProps = (state) => {
const dataState = getMyDataStateFromState(state);
// dataState can be "NOT_INITIALIZED" (default), "FETCHING", "FETCH_SUCCEEDED" or "FETCH_FAILED"
console.log("mapStateToProps: " + dataState);
return {
dataState,
};
};
export default connect(mapStateToProps)(MyComponent);
// A thunk triggered by a click in another component:
export async const myThunk = () => (dispatch) => {
dispatch({ type: "FETCHING_DATA" });
let result;
try {
result = await API.getData(); // an error thrown immediately inside of here
} catch (error) {
dispatch({ type: "FETCHING_DATA_FAILED" });
return;
}
dispatch({type: "FETCHING_DATA_SUCCEEDED", data: result.data});
}
// Let's say this is the API:
export const API = {
getData: () => {
console.log("> api call here <");
throw "Some error"; // in a real API module, there's a check that would throw in some cases - this is the equivalent for the unhappy path observed
// here would be the fetch call
},
}
What I would expect to see in the console after triggering the API call (which immediately fails), is the following:
mapStateToProps: FETCHING
componentDidUpdate: FETCHING
> api call here <
mapStateToProps: FETCH_FAILED
componentDidUpdate: FETCH_FAILED
However, I can see the following instead:
mapStateToProps: FETCHING
> api call here <
mapStateToProps: FETCH_FAILED
componentDidUpdate: FETCH_FAILED
So the MyComponent component never received the "FETCHING" dataState, although it has been seen in the mapStateToProps function. And thus never displayed the error message. Why? Is it because such fast updates to a component's props are batched by React (like calls to this.setState() in some cases)???
Basically, the question is: If I dispatch two actions, really quickly after each other, triggering a component's props updates, does React batch them, effectively ignoring the first one?
The first time, a component is rendered, componentDidUpdate is NOT called. Instead, componentDidMount is called. Log to console in componentDidMount as well to see the message.
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 }
)