Except from the main product, Skroutz provides various internal tools to its people. These tools are developed in-house and are highly customized for our specific needs. One of these tools provides its users with statistics related to orders. Let’s refer to this page as the statistics page from now on.
The problem
The statistics page is rendered using React and is responsible for getting specific data from the backend and display them through various charts to the end user. The problem is that all required data come from a unique database query which is quite heavy and takes some time to finish (it depends on the requested period of time and number of shops) on the one hand and, on the other hand, the way React handles this waiting.
The following image shows what user sees while waiting for the page to finish loading.
This is not the best user experience because we have to wait for all data to be available before React starts to render the children components that are going to display the desired data. Furthermore, there is too much blank space on the screen while page loads.
The following illustration depicts the way that the current implementation is organized. Each solid-lined rectangle represents a React component and each arrow represents data that flow from one component to another.
There is a wrapper component (the outer rectangle) that is responsible for fetching the data from the backend. Inside the wrapper component there is another component (the intermediate rectangle) that holds various children components (the colored rectangles) which are going to display the respective data.
When the data are available, the wrapper component updates its internal state and all children components get re-rendered because we pass the data as props to each one of them through the intermediate component.
The components are colored this way on purpose. Different color means different data. We can see that some components request different data from each other, but others, like A and B, or C and E, request the same data, only they display them in a slightly different way.
Proposed solution
As mentioned before, the main problem with the initial implementation is the fact that React waits for a heavy query to finish in order to return all the required data.
What if each component requests their own data independently from the backend? We could split the one heavy query to smaller ones that would get called from the respective components. We may end up with multiple network requests instead of one, but we can render each component as soon as it has its data available.
What do we want to achieve with these changes?
- Better user experience, because the user will see various components in a loading state (instead of a full page loader) and, gradually, each one of them will render the respective chart, as soon as it gets its data. This is a valid benefit here because the nature of the specific page is to provide various, independent metricts that can be consumed individually by the user and provide a valuable insight. In other words, the user doesn’t have to view all the data that the page, eventually, will render in order to extract a conclusion.
- Avoiding single point of failure, by executing multiple requests, we avoid the case when a failed request prevents all the components to be rendered, leaving a black page with an error message. With multiple, independent components we can render the ones that have data, while in the ones where an error has occurred, we can render an error message with a retry button.
Implementation
Now let’s move on with the implementation. We are not going to dive into much detail here and we are not going to provide the full code as this is not the goal of this article. The purpose of this article is to highlight the most interesting parts of the current implementation and briefly explain the changes we made to achieve the final result.
Names of classes, methods and components may have changed. Many parts of code have been omitted for reasons of simplicity.
Create the API
First of all we have to provide the API that the React components are going to use in order to fetch their data. Until now, we had an endpoint that, as soon as it gets called, it executes a heavy query to the database in order to return a hash containing all the required data.
After adding the appropriate methods to DataClass
in order to return the respective portion of data, we make stats_data
action to accept the metric
param in order to be able to call the respective DataClass
method.
Now, each component will be able to call stats_data
, providing its own metric
param to get the desired data.
How it used to work initially
Let’s take a look at the initial state. There are two wrapper components, Stats
and StatsMetrics
as we saw in image 2. Stats
component fetches the data and passes them to StatsMetrics
, as we can see in the following snippet.
StatsMetrics
, on its turn, gets the data from its parent and renders the children components, passing the respective data to each one of them. You can see a comment after each component that indicates the respective rectangle from image 2 (and 3).
As we explained earlier, some components require the same data as other components do, like component A, which requires data.order.all
, just like component B does. The same goes for components C and E which require the data.order.billed
part.
Taking a look at one of the children components, let’s say StatsOrderGroup
, we can see that it takes the data prop and displays parts of the data object via helper components.
Move responsibility of data fetching to children components
As we explained in the previous section, the plan is to assign the responsibility of data fetching to each one of the children components. So, the first step is to remove the useEffect
hook from the Stats
component and pass the searchUri
prop to StatsMetrics
component, instead of statsData
.
Then, we change the StatsMetrics
component to receive the searchUri
prop in order to pass it to the children components.
We need a loading state and error reporting
Until now, children components got rendered if and only if they had their data available. Their parent was passing the data to them and they were ready to render their markup.
Now, the situation is different. Each child component gets rendered immediately on page load and it waits until it has its data available from the backend in order to display them to the user.
So, we need to have a loading state for each component and an error reporting state in case of an error response from the api call. For this reason, we have introduced some wrapper components that are responsible for the following:
- Fetch data from the backend
- Render a loading state while waiting for data to be available
- Display an error message in case of error, and a retry button (for on-demand data fetching)
Introducing children components wrappers
We are going to need a wrapper for each component that needs to fetch its data, so we create three wrappers:
StatsOrderGroupWrapper
forStatsOrderGroup
StatsRatiosGroupWrapper
forStatsRatiosGroup
CancellationGroupWrapper
forCancellationGroup
We did not create wrappers for StatsOrderCountLine
and StatsAverageGroup
because these components will get their data, indirectly from other components (pairs A - B and C - E).
The following snippet shows the StatsOrderGroupWrapper
component in its final form. This component (wrapper) fetches the data it needs and it renders a loading state while waiting and displaying the data as soon as they are available, or displays an error message in case of unsuccessful fetch.
As you can see, all the functionality is delegated to two custom hooks, useGetCpsOrderStats
and useCpsOrdersStats
. The other two wrappers use the same hooks and are similarly organized.
Let’s see what’s going on inside cpsOrdersStatsContext
which exposes the useGetCpsOrderStats
hook alongside with another two context components that we are going to see later in action:
First of all we create a context object and store it in the CpsOrdersStatsContext
constant.
After that, we declare the cpsOrdersStatsReducer
function that we are going to use as a reducer inside CpsOrdersStatsProvider
component, which we create immediately after.
What CpsOrdersStatsProvider
does is that it provides a value to its children components, notifying them about changes and providing the dispatch
function in order for them to update the context state.
But, in order for CpsOrdersStatsProvider
’s children components to be informed about changes in our context (the stats data), they need to be wrapped inside a context consumer component. For this reason we create the CpsOrdersStatsConsumer
component, which does exactly that.
Finally, we create the useCpsOrdersStats
custom hook to be used by our wrappers (like StatsOrderGroupWrapper
) in order to have access to our context, and especially the dispatch
function. StatsOrderGroupWrapper
calls the dispatch
function (through useGetCpsOrderStats
hook) every time it needs to inform its siblings components that it has the data they need.
Now let’s take a look at the contents of useGetCpsOrderStats
:
In this custom hook we keep the logic of fetching the desired data (metric
), update the stats context by calling the dispatch
function for the specified metric and define the loading state and if we need to show an error message or not.
Prevent new requests until all components have finished loading
There are two buttons under the stats page filters, Search
and Clear
buttons. Every time we click on each one of them, all components should request their data again. We need to prevent the user from clicking either of those buttons until all components have finished loading their data. Otherwise, we might end up having multiple asynchronous requests to compete with each other from which one will finish first.
For this reason we introduce the LoadingInspectionContext
, which is responsible for keeping the loading state of the whole page, in other words, it checks to see if there is at least one component in the page that still waits for its request to finish.
As we can see, we expose the LoadingInspectionProvider
and LoadingInspectionConsumer
components from the above file. The way we use these goes like this: we wrap the filters and the main page (that contains the stats components) with a LoadingInspectionProvider
. Then, we wrap each one of the wrapper components (the components that fetch the data) with a LoadingInspectionConsumer
. When a stats component’s data are available, we call the updateRequestsState
function that is provided by the context object of the LoadingInspectionConsumer
components, in order to update the page’s loading state.
Combining them all together
Now, let’s see how all the above work together.
As we said earlier, LoadingInspectionProvider
wraps the filters and the StatsMetrics
components. If we take a look inside the StatsMetrics
component, we can see how we use the LoadingInspectionConsumer
and the CpsOrdersStatsProvider
and CpsOrdersStatsConsumer
components.
To sum up, we can say that the flow goes something like this:
- Wrapper components (like
StatsOrderGroupWrapper
) render with a loading state enabled useGetCpsOrderStats
requests the data- When the data are available, the
dispatch
method is called in order to update the context - The component’s loading state becomes
false
- The wrapper notifies the
LoadingInspectionProvider
context component that it has finished loading its data - The stats contexts has been updated, so
CpsOrdersStatsConsumer
s notify their children to render the desired data
Final result
It is time to take a look at our final result. As soon as the document loads, each component sends a request to the backend and renders a placeholder, indicating that it waits for its data to be available. Now, the user can have a better idea of what this page is going to render.
In case of unexpected error in a specific component, an error message gets rendered alongside with a retry button that gives the user the opportunity to request the data for this component again. The other components that managed to retrieve their data successfully should be able to visualize them via the respective chart.
Now let’s take a look at the network tab to figure out what has changed. The next two images illustrate the time it took for each request to be completed.
All measurements were made in development environment
In the first image we can see that, in the initial case, it took about 34 seconds for one (and ony) request to be completed.
In the second image we see that it takes about 42 seconds for all requests to be completed. Moreover, instead of 1 request, we have 6 requests that run concurrently. At first glance this does not seem so efficient, on the contrary, it seems to have made things worse.
But if we take a second look, we can see that the first request responses in less than 12 seconds. This means that in 12 seconds from the initial load, the first component renders its data. After about 5 seconds, a second component renders its own data, and so on. It seems that the page loads progressively!
Comparing the two cases, (in the first one all components render their data after 34 seconds, and in the second case each component renders its data as soon as they are available), we see that the second case provides a better user experience, even if the last component gets rendered after 42 seconds (vs 34 seconds that took in the first case).
The fact that the page has finished loading the first batch of information in 12 seconds (instead of 34) reduces the TTI. As we can see in the following lighthouse report, the page starts to get interactive at 1.8 seconds.
Summary and next steps
In this post we examined the implementation of a progressive react app and we provided some technical details. We saw the use of React’s context
object and how it helped us to achieve specific functionality. Finally, we presented some performance metrics and saw that the final solution is a bit slower than the initial one, but the user experience is clearly better.
But there is always room for improvement. In our case, we can implement a mechanism that would give the ability to the user to change the applied filters without having to wait for all data to get fetched. As soon as we use axios
to fetch our data, we can use the CancelToken
object provided by the library.
Below we can see the getStats
function, the function that we call to fetch the stats data for a specific metric. getStats
uses the get
function which is a custom implementation of axios that we call SkroutzAxios
.
It’s pretty straightforward to implement the cancellation functionality here, we just have to pass the cancelToken
property to the options object.
Then, we can just call source.cancel();
when we want to cancel the pending requests. For more details about the usage of CancelToken
visit the axios docs.
Finally, as we can see from the previous performance report, we can take some actions in order to improve the overall score:
- Reduce the initial server response time because React waits for the document to get served in order to start fetching the data
- Remove potential unused javascript because they affect the network activity
- Eliminate render-blocking resources and deliver non-critical assets asynchronously