React: how to fix this spinner? - reactjs

I have a spinner but it works while the page is rendering and I also need it to work when I click the LoadMore button, I don't understand how
Here is the link Spinner

You can call this.setState with setting status to pending in first lines of loadMore method, or better call it in fetchImageApi, just like this:
fetchImageApi = () => {
const { searchbar, page } = this.state;
this.setState({status: 'pending'}, () => {
imageApi(searchbar, page)
.then((images) => {
if (images.total === 0) {
this.setState({ error: "No any picture", status: "rejected" });
} else {
this.setState((prevState) => ({
result: [...prevState.result, ...images.hits],
status: "resolved",
page: prevState.page + 1,
searchbar: searchbar,
}));
}
})
.catch((error) => this.setState({ error, status: "rejected" }));
}
};
There is callback as a second argument of setState method, which means that your callback code will be executed right after react updates its state, so you don't have to write await logic for your code.

Related

Async await api call inside useEffect being called twice

enter image description here
When I click on the dropdown button, it'll trigger API calls and it works fine. However, if I click the dropdown button again (without refreshing the page), API calls(not the whole fetchData function but only the await cityAPIS calls ) will call twice(ideally it should only be called once.)
Does anyone know why is this happening?
PS. I didn't use React.strictMode.
import CityApi from '#Api/CityApi';
export default function useFetchCityApi(
initState,
loadState,
imgDispatch,
selectCountry
) {
useEffect(() => {
if (initState) {
return;
}
async function fetchData() {
imgDispatch({ type: 'FETCH_DATA', fetching: true });
let data = await CityApi(
loadState.load === 0 ? 0 : loadState.load - 30,
selectCountry
);
if (data === 'error') {
imgDispatch({ type: 'FETCH_DATA', fetching: true });
return;
}
imgDispatch({ type: 'STACK_IMAGE', data });
}
fetchData();
}, [loadState.load]);
}
whole codebase:
github codebase

Mocking a promise, and testing 'setState' in '.then()'

I'm writing unit tests for a React application. A click-handler calls a simple promise, and updates the state inside of '.then()'. I have successfully mocked the promise and am able to enter the correct if/else block using mock data returned from the resolved promise.
It seems that using a console.log to test the data shows that the data is partially incorrect. I'm also unable to test that the state has changed, as (I suppose) setState is asynchronous.
I have tried using .update(), .forceUpdate(), setTimeout(), and setImmediate(). I'm not sure what else to try to be able to test that the state has changed correctly.
Here is the method being tested:
this.handleClick = () => {
sendMessage(this.urlParams.data1, this.urlParams.data2, this.urlParams.data3)
.then((data) => {
if (data.messageType === 'error') {
console.log(data.message);
this.setState({
error: data.message,
}, () => {
console.log(data);
});
} else {
this.doSomething();
}
});
};
Here is the mock of the 'sendMessage' method, which is working as expected:
export const sendPnrgovMessage = () => Promise.resolve({ data: { messageType: 'error', message: 'asdf' } });
Here is the test itself:
it('should send the message when the button is clicked', () => {
renderedInstance = rendered.instance();
renderedInstance.handleClick();
renderedInstance.forceUpdate(() => {
expect(rendered.state('error')).toEqual('asdf');
});
});
I've also tried without the '.forceUpdate()', the result is the same.
There are two issues with the result.
1.) 'data' in the '.then()' evaluates to this:
{ messageType: 'error', message: undefined }
Why would message be undefined, while messageType is correct?
2.) The expected value of this.state.error is 'asdf', or in the above case, 'undefined', but it is actually 'null', which is the initialized value. I assumed that this is because setState is asynchronous and is not being updated by the time the test has finished. I have not found a way around this.
Have you tried mocking the function implementation using jest?
import { sendMessage } from './file';
describe('should test component', () => {
beforeAll(() => {
sendMessage = jest.fn().mockResolvedValue({ data: { message, Type: 'error', message: 'asdf' } });
});
afterAll(() => sendMessage.mockRestore());
it('should send the message when the button is clicked', () => {
renderedInstance = rendered.instance();
renderedInstance.handleClick();
renderedInstance.forceUpdate(() => {
expect(rendered.state('error')).toEqual('asdf');
});
});
});
You made 2 mistakes in your code.
The return value of sendMessage is { data: { messageType: 'error', message: 'asdf' } }
so you should receive the message as {data}. curly brack is required.
When we use the arrow function, we must avoid the 'this' context because it doesn't bind own this.
The answer is as follows:
this.handleClick = () => {
const ref = this;
sendMessage(this.urlParams.data1, this.urlParams.data2, this.urlParams.data3)
.then(({data}) => {
if (data.messageType === 'error') {
console.log(data.message);
ref.setState({
error: data.message,
}, () => {
console.log(data);
});
} else {
ref .doSomething();
}
});
};

How to get react test using moxios to update the DOM before running later part of the test

I am trying to write a test using jest and react-testing-library for a component. The test needs to wait for useEffect to update the state following an axios request. I'm using moxios to mock the api call but I can't get the test to wait for moxios to return before it fires a click event handler that again sends a delete request to the api. The test fails saying that the DOM element I'm trying to click on doesn't exist yet because it's only produced once the useEffect request updates.
I've tried using flushEffect to wait and I've also tried wrapping the click inside the initial moxios request but both don't work
I've cut out any code that is not relevant.
This is the component. Once loaded it sends a get request to an api to grab a json response of some benefits. The BenefitsTable component takes in the benefits and for each one produces a table which includes a delete button. I'm trying to first load in the benefits and then click on the delete button. No delete button exists before they are loaded.
const Benefits = props => {
const [benefits, setBenefits] = useState([])
const [editing, setEditing] = useState(false)
const [editingBenefit, setEditingBenefit] = useState({id: null, name: '', category: ''})
useEffect(() => {
axios.get('/api/v1/benefits')
.then(response => {
setBenefits(response.data)
})
}, [])
const deleteBenefit = benefit => {
const id = benefit.id
axios.delete(`/api/v1/benefits/${id}`)
.then(response => {
setBenefits(benefits.filter(benefit => benefit.id !== id))
warning('Benefit Deleted')
})
.catch(error => {
warning('Benefit could not be deleted. Please try again')
})
}
return(
<div>
<Section>
<BenefitsTable
benefits={benefits}
deleteBenefit={deleteBenefit}
editBenefit={editBenefit}
/>
</Section>
</div>
)
}
My test is as follows:
it('deletes a benefit when the delete button is clicked', () => {
const { getByTestId } = render(<Benefits />)
moxios.wait(() => {
const request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
response: benefits
}).then(() => {
done()
})
})
fireEvent.click(getByTestId('deleteButton1'))
moxios.wait(() => {
const request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
}).then(() => {
expect(document.querySelectorAll('tbody > tr').length).toBe(1)
done()
})
})
})
The output is Unable to find an element by: [data-testid="deleteButton1"] and I get that it's because the axios request is async but I've tried wrapping the fireevent and subsequent axios request inside the then clause of the first axios request and the although the test passes, it passes with any value meaning it isn't being correctly processed.
Would waiting for the element to be present work?
it('deletes a benefit when the delete button is clicked', async () => {
const { getByTestId } = render(<Benefits />)
moxios.wait(() => {
const request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
response: benefits
})
})
await waitForElement(getByTestId('deleteButton1'))
fireEvent.click(getByTestId('deleteButton1'))
moxios.wait(() => {
const request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
}).then(() => {
expect(document.querySelectorAll('tbody > tr').length).toBe(1)
done()
})
})
})

React accessing state before ComponentDidMount

When I try to access a state variable which is set in ComponentDidMount, react throws an undefined error. This is because I believe when I'm calling the fetch api and setState in ComponentDidMount, the value isn't ready yet (async stuff). Is there a proper way to either delay the render until the setState call is done or some other way to get the state updated fully before render is called?
I think the code below will give you a basic idea how fetch data and render work.
class App extends Component {
state = {
data:{},
loading:true,
error:null,
}
componentDidMount = () => {
fetch('https://example.com/api/article')
.then((response) => {
return response.json();
})
.then((json) => {
this.setState({
data:json,
loading:false,
})
.catch(error => {
this.setState({
error,
loading:false,
})
});
});
}
render() {
const {data,error,loading} = this.state;
if(loading){
return "Loading ..."
}
if(error){
return "Something went wrong."
}
return 'your actual render component or data';
}
}
export default App;

How to wait in enzyme for a promise from a private function?

I'm a novice at react and any javascript testing frameworks.
I have a simple component that retrieves an item from the API and shows them to the screen.
The function getItems() is called from componentWillMount.
Is it possible to wait until getItems() has finished before making my assertions?
ItemDetails.js
class ItemDetails extends Component {
constructor(props) {
super(props);
this.state = {
details: ''
}
}
componentWillMount() {
this.getItem();
}
getItem() {
const itemId = this.props.match.params.id;
fetch(`/api/items/${itemId}`)
.then(res => res.json())
.then(details => this.setState({ details }));
}
render() {
const details = this.state.details;
return (
<div>
<h1>{details.title}</h1>
...
</div>
);
}
}
export default ItemDetails;
ItemDetails.test.js
describe('ItemDetails', () => {
it('should render a div with title', () => {
const details = {
_id: 1,
title: 'ItemName'
};
fetch.mockResponseOnce(JSON.stringify(details));
const wrapper = mount(<ItemDetails match={{ params: {id: 1} }} />);
expect(wrapper.find('div').find('h1').text()).toBe('ItemName');
});
});
The answer above works, but it invites to test implementation details:
as a rule of thumb, accessing the instance of a wrapper (.instance()) and calling update() are smells of tests that know too much about the way the code under test works in my opinion, it is not testing a behavior, it is testing an implementation
having the method return a promise and wait on that promise before executing the expectation breaks the privacy of the method: it make the code hard to refactor
What you really want is to wait on all promises to be resolved, without accessing a handle to those promises (that would break privacy). Try this
const wait = () => new Promise(resolve => setTimeout(resolve));
it('does something', () => {
renderFnThatCallsAPromiseInternally();
return wait().then(() => {
expect(somethingDependentOnPromiseExecution).to.be.present();
});
});
This will wait for all inner promises to resolve, provided wait is called after the code that enqueues promises (rendering the component) is called. The reason lies in how the JS event loop works, it's finicky, but very well explained by Jake Archibald in this talk: https://www.youtube.com/watch?v=cCOL7MC4Pl0.
Hope that helps.
Could you try:
describe('ItemDetails', () => {
it('should render a div with title', () => {
const details = {
_id: 1,
title: 'ItemName'
};
fetch.mockResponseOnce(JSON.stringify(details));
const wrapper = shallow(<ItemDetails match={{ params: {id: 1} }} />);
// manually call function
wrapper.instance().getItem();
// update to re-render component
wrapper.update();
expect(wrapper.find('div').find('h1').text()).toBe('ItemName');
});
});
If it doesn't help I think you will need to return Promise from your function (base on this example):
getItem() {
const itemId = this.props.match.params.id;
return fetch(`/api/items/${itemId}`)
.then(res => res.json())
.then(details => this.setState({ details }));
}
describe('ItemDetails', () => {
it('should render a div with title', () => {
const details = {
_id: 1,
title: 'ItemName'
};
fetch.mockResponse(JSON.stringify(details)); //response gets called twice
const wrapper = mount(<ItemDetails match={{ params: {id: 1} }} />);
// return Promise so Jest will wait until it's finished
return wrapper.instance().getItem().then(() => {
wrapper.update();
}).then(() => {
expect(wrapper.find('div').find('h1').text()).toBe('ItemName');
})
});
});

Resources