API Calls & Displaying Data #6
Andrei15193
announced in
Guides and Tutorials
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Most single-page applications offer a user interface for one or multiple APIs, Model-View-ViewModel can be used to handle this case quite easily.
In this tutorial we will write a simple application that we perform calls to the GitHub REST API to search for repositories. This provides a basic sample for getting started with
react-model-view-viewmodel
and handle the most common of cases.A key concept about ViewModels is that we can pass them around between components. We can present the same ViewModel in multiple components, or just parts of it, even at the same time. The requirements is that ViewModels are decoupled from a component, we can instantiate them in one place and subscribe to them in other places.
To get started, configure a project to run React with TypeScript and add
react-model-view-viewmodel
as a dependency. Or better yet, start a sandbox based on the React MVVM Template on CodeSandbox: https://codesandbox.io/s/react-mvvm-vwsqlv.Making the User Interface
The UI is fairly simple, we display an input and as we type, after a short delay, we want to perform the API call. For this we will define a component and bind
onChange
event to handle the search.We will track the timeout handle so we can clear it if we navigate away from the page or if we are still typing something and the timeout has not yet elapsed.
Instead of using
console.log
we would perform the API call. There are a number of issues that come with this as we generally want to display a spinner or something similar that lets the user know that something is going on.Furthermore, we need to handle mappings between the API result and a data transfer object (DTO) that is more appropriate for our use case as well as decoupling our UI from the API structure.
Last but definitely not least, we need to handle errors. Sometimes the backend times out or there are some errors that fault our request, this needs to be handled and the React components are hardly the place to do all this.
Introducing ViewModels
ViewModels exist to take the burden of logic out of UI components simplifying them in the process. ViewModels handle the logic behind pages and components, they perform the API calls, let us know that they are busy, perform validation and so on.
These objects are observable, this means we can change their state and notify watchers, the UI in most cases, so they can update in consequence. There are cases when ViewModels watch one another for changes, however these are more specific cases and we need to be careful around event subscriptions to not cause any memory leaks.
In this example, the view model will expose two properties to begin with. One is a flag that indicates whether the view model is performing an asynchronous operation, such as an API call, and the other is the search term.
The
isBusy
flag is a read-only property, we do not want other objects to tell us whether we are busy or not.On the other hand,
searchTerm
is a get/set property. Once we change it we notify any observers. This works both ways, if we change the value from outside the ViewModel or from within. The observers will know about the change and the same information will be displayed everywhere.Binding the ViewModel
The next part is to create an instance of our newly defined class and bind our search input to it.
In Mode-View-ViewModel, the lifecycle of a ViewModel is decided by the View. They instantiate such objects depending on their need. This can happen directly or indirectly through dependency injection mechanisms.
In our case, we will use the
useViewModel
hook and pass the class definition to get an instance. All constructor arguments are treated as deps thus whenever one of them changes we get a new instance. If we need finer control over this, we can use theuseViewModelMemo
hook which requires a callback for creating the ViewModel and needs the deps specified explicitly, just like React'suseMemo
.The search delay is a UI concern, this is why we keep the component responsible for it. The ViewModel does not exactly know how it is being used by the component, it only exposes a number of properties and a method that performs the search.
We can split this up in as many components as we want to, the base one ensures the ViewModel instance while the other ones subscribe to it.
Invalidation of API Calls ("Cancellation")
If we play a little bit around, we can see that the search is acting a little bit strange at times. For instance, if we type something then pause for about a second and then type some more, the ViewModel starts a new search.
This is a valid use-case, sometimes we type something and see that the search is taking longer to complete, then we change what we typed in hopes of getting results faster.
The issue here is that we have parallel searches going on and depending on when they finish, an older search may overwrite the result of our latest search because it took longer to complete. On the other hand, the
isBusy
flag gets set tofalse
although there is a search going on.This is commonly known as cancellation, we need a way to cancel or abandon the search that is in progress if we start a new one. This can be done in a number of cases, we do not really expect this to happen often and generally API calls should be rather fast. We will use an asynchronous operation token approach for this.
In essence, we store an object that acts as our search token before we start the asynchronous operation. This is stored by the ViewModel instance and thus, it changes every time we start a search. When we finally get our results, we compare the token we had when we started the search with the one we currently have.
If the tokens are the same we are handling the results for current search, otherwise it means that a new search was triggered and our search token is no longer valid. We will log this in the console and observe how this changes the behavior.
With these changes, if we try to run parallel searches we will notice that only the latest one is taken into account when the result would be processed and we only update the
isBusy
flag for the latest search.While not an actual cancellation, the API calls would still be carried out, it does the job and in most cases this would be more than enough as these calls tend to be rather fast.
Making the Call, Adding Dependencies
It is time to introduce dependencies for our ViewModel. We need an object that knows how to make HTTP calls, for this we will use Axios.
We will request an
AxiosInstance
in the constructor and store it at the instance level, later on we will use it to call the GitHub REST API and get some results. Documentation for the search endpoint can be found here: https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-repositories.We need to update the component as well, we will create and store an
AxiosInstance
outside of the component to avoid reinitialization of our ViewModel for each render. Generally we would want to have a single instance per application as it would hold authentication information as well and other common headers.In our console log we can now see API results, this is useful as we know what we are getting and how to map this to our data transfer objects.
Mapping Results
On our UI we will display the name of each repository as a link to the GitHub web page and the description. We will define an interface to describe this information and then map the API result to this structure.
For storing our data we will use an observable collection. They provide most of the known JavaScript array methods, they are intended to be used interchangeably for most cases. Keep in mind that a great number of these methods return a new array and do not always mutate the collection!
The library is explicit about types, trying to assign a plain JavaScript array to an observable collection will not work. This is to raise awareness of potential unintended changes.
As a rule of thumb, properties or fields that are observable collections should be marked as
readonly
, this will further reduce the chance of assigning a new observable collection to a field rather than updating the underlying collection.Displaying Search Results
One last thing, we need to display the repositories. We will do this in a separate component and pass it the observable collection of repositories. Whenever the collection changes so will our list.
Here, we will not initialize a ViewModel, we will only subscribe to one. Observable collections are a particular case of ViewModels and for this we will use the
useObservableCollection
hook instead. Such collections should generally be maintained by other ViewModels that expose them rather than creating instances of them in components.Now, we only need to update our base component.
Conclusions
In this tutorial you have learned the basics of using
react-model-view-viewmodel
, made an API call to GitHub to get a list of repositories based on a search term, and handled the general asynchronous programming challenges by marking the ViewModel as busy while performing the API call and invalidating requests as we make new ones in parallel.This covers a large portion of what web applications do on the front-end side, there are still plenty to learn and even work on. The code samples themselves can be improved, however their intent is to showcase how Model-View-ViewModel can be applied using this library with a concrete example.
Check the other tutorials in this category for learning more about this library and how to tackle different challenges.
The code can be viewed, edited and run on CodeSandbox here: https://codesandbox.io/s/react-mvvm-simple-api-call-example-r927tj.
There is a CodeSandbox template for getting trying out
react-model-view-viewmodel
as well, https://codesandbox.io/s/react-mvvm-vwsqlv.Beta Was this translation helpful? Give feedback.
All reactions