React Clean Code - mocking network scenarios
Handling network requests is complicated. There are too many cases you have to consider on top of the asynchronised process. And testing these code can be even more challenging...
In my book Maintainable React, I introduced a feature I had worked on a while ago. That feature is interesting in many ways, and I selected it as it involves several states in the view - which is one of the reasons why building UI is complicated.
They are not typical UI states but server cache as described in Kent’s article here. All the network requests could go wrong, timeout, too many requests or even service downtime. In the React views, we need to reflect these network statuses correspondingly.
The feature Direct to boot here is that at the beginning, when the user is landed on the page, we need to check the current status of an order. If it’s in progress, we need to show a disabled button
I'm here and some hint messages. And at some point, when the order is ready to pick up, the button needs to be enabled so we can click it to notify the store. When the button is clicked, a notification is sent and the order will be delivered right into the boot of your vehicle. In addition, if anything goes wrong, we show a number as a fallback so we can call the store by phone.
So we need to consider the following things at least:
- Fetch data from the server
- Retry if something goes wrong in the network
- Retry if the data is returned but not what we wanted
- Stop retrying when we fail too many times
- Update data to the server
- Handling errors
If we draw a
statechart for the description above, it will be something like the diagram below. Note that the happy path (idle → ready → notified) is only one of the branches. Before the order is
ready, we need to retry several times. And the notification might have another retry counter (we’re not doing it in this chart). Also, each network request could lead to an error.
The happy path
We can start with the happy path as that’s the easiest step and the most important thing we want to ensure to happen.
In test, the happy path for (initialised → ready) can be described as:
Mirage.js for mocking
In my book I used
msw as the mocking server, and it worked pretty well. I’m using
mirage.js here just for simplicity. It doesn’t matter much here anyway, and you are safe to use either one.
For example, we can define a
get API for checking the order status in the test. It intercepts requests send to
/api/orders/<id> and always returns an object with
And then, we create a server at the beginning of each test, and shut it down at the end.
The API looks intuitive and works just as expected. We can then in our component to send requests and consume the response like it would for the actual APIs.
Use react-query to simplify the logic
In my previous article, I have discussed in detail how many trivial things you need to consider for making a “simple” network request as well as how simpler if you use
react-query instead of implementing it manually.
react-query we’ll need to define a
query function. Note here if the
res.data.status is not
ready, it will throw an error. And
react-query can detect that error and trigger a
refetch if configured.
Now with the
fetchQuery function, I can then call
useQuery, and wrap the whole logic inside a hook
I set retry as 5, so whenever an actual error happens (say, 500 from the server side) or when
res.data.status is not
react-query will retry. And it doesn’t retry right away but wait for a little while as a delay between each failure.
Simulate an error
mirage.js, simulating an error for the test to catch is fairly straightforward. I also found it might be helpful to have several
ids that would trigger errors so you can test different error-handling logic.
For example, we can define a list of
ids that indicate errors when used.
And then, in our test, we can use either
timeout-id as the
orderId to simulate the error:
In our feature, we also want to simulate the
long-running order to make sure the UI won’t be blocked by the initial fetch. We can simulate it by defining a variable with
initialised state and later on using a timer to update the value.
Then after several retries, the view finally gets the
ready status and is ready for notifying the store. Note in the console mirage had done three retries in this case.
In this article, we have discussed how to use
mirage.js to simplify the network mocking for either impossible or difficult cases if you talk to real APIs in your frontend application. We looked at the happy path, error handling and retries with
mirage.js , and also how easy it is to use
react-query to simplify the logic of implementing the network-related code.