Now that we have defined our queries and that we have generated code for those using Apollo, we can start using them.
We will now create and use an Apollo Client with the help of Apollo-Boost. Apollo-Boost isn't strictly necessary, but it will heavily simplify our implementation.
We will use the Apollo Client within our Home component.
When the search function gets invoked by the Search component, we will use the Apollo Client to search for artists and songs. Also, when the user selects a song, we will use it again to retrieve its lyrics.
First of all, let's create our Apollo Client instance.
Let's get started:
- Open the frontend/src/pages/home.tsx file.
- Then, add the following declaration right after the import statements:
import ApolloClient from 'apollo-boost';
const apolloClient = new ApolloClient({ uri: 'http://localhost:4000/graphql' });
As you can see, we simply had to instantiate the ApolloClient class provided by Apollo-Boost and point it to the GraphQL endpoint that we want to send our queries to.
Before we start sending our queries, we need to add some states using the useState hook. We will use that state to store the search results.
Add the following declarations at the beginning of the Home page (within the function's body):
const [foundArtists, updateFoundArtists] = useState<FindArtists_artists[]>([]); const [foundSongs, updateFoundSongs] = useState<FindSongs_songs[]>([]);
Notice that we have specified the type of our state using the generic argument of useState. In the case of the foundArtists state, we know that we will get back an array of FindArtists_artists objects. For the foundSongs state, we will store an array of FindSongs_songs objects.
Then, add the following imports to make it work:
import { useState } from 'react';
import { FindArtists_artists } from '../generated/FindArtists';
import { FindSongs_songs } from '../generated/FindSongs';
Now that we have our state, we can finally execute our queries and store the results that we get back.
Modify the search function in the Home view, as follows:
const search = (searchText: string) => { console.log(`Home: searching for ${searchText}`); apolloClient.query<FindArtists, FindArtistsVariables>({ query: FindArtistsQuery, variables: {value: searchText} }).then((result: ApolloQueryResult<FindArtists>) => { console.log('Home: found artists: ', result.data); updateFoundArtists(result.data.artists); }); apolloClient.query<FindSongs, FindSongsVariables>({ query: FindSongsQuery, variables: {value: searchText} }).then((result: ApolloQueryResult<FindSongs>) => { console.log('Home: found songs: ', result.data); updateFoundSongs(result.data.songs); }); };
And add the following imports at the top of the file:
import { FindArtists, FindArtistsVariables } from '../generated/FindArtists';
import { FindSongs, FindSongsVariables } from '../generated/FindSongs';
import { FindArtistsQuery, FindLyricsQuery, FindSongsQuery } from '../graphql/queries';
import { ApolloQueryResult } from 'apollo-boost';
Here, we execute two different GraphQL queries one after another using our Apollo Client: the first to find artists and the second to find songs. This mimics what we did in the first version of the application. Note that the responses to each query will be received asynchronously, hence the .then(...) function that we define.
As you can see from the preceding, the query method of the Apollo Client accepts some generic parameters:
- The first one corresponds to the shape of the response.
- The second one corresponds to the shape of the variables that we need to provide along with our query.
Also, notice that we pass an object to the query method, containing the following:
- It contains a key called query whose value is the GraphQL query that we have defined earlier as a tagged template literal in our queries.ts file.
- It also contains a key called variables that defines the different query parameters along with their respective values. Note that this object must respect the type specified as the variables generic argument.
In our case, we have a single variable to provide: the search term, which we have received from our Search component when the search operation was triggered.
In the case of the query to find songs and assuming that the happy path is followed, when the response is received, our .then function will be called with the result of the query. In this case, it should receive an object of the ApolloQueryResult<FindSongs> type.
Finally, within the callback, we extract the data property of the query result and pass the Song objects that it contains to our updateFoundSongs state update function to update our component accordingly.
From that point on, our template can use the updated state. Since we have used the state update function (which is mandatory!), then our component and its children get re-rendered.
If you want to give it a try right away, then you can replace the code right after the searchInputCleared function with the following:
const foundArtistsList = foundArtists.map(item => <li key={item.id}>{item.name}</li>); const foundSongsList = foundSongs .filter(item => item.hasLyrics) .map(item => <li key={item.id}>{item.name}</li>); return ( <Container className='lf-home'> <Search searchCleared={searchInputCleared} searchTriggered=
{search}/> <ul> {foundArtistsList} </ul> <ul> {foundSongsList} </ul> </Container> );
And import Container from react-bootstrap:
import { Container } from 'react-bootstrap';
Here, we have constructed a list of li HTML elements by looping over the list of found artists as well as another one for the list of songs. We then simply render those insides of a ul element within the template.
In the case of the songs list, we have added a filter to ignore songs for which no lyrics are available. In a real-world scenario, you should directly filter the result set in the backend (to avoid unnecessary network traffic).
Alternatively, you could instead adapt the user interface to either visually make the distinction between songs that do have lyrics and those that don't, or provide a toggle to show/hide songs without lyrics. These are important UI/UX considerations but this is out of the scope of this book.
At this point, you have seen the whole process, from defining the backend GraphQL type using a code-first approach to writing queries, generating code, sending actual queries from the client-side, and handling the results.
This indeed eludes many important design questions, such as what happens when the backend is unavailable or how to deal with errors in general, but it at least gives you a good overview of how you can create and use a GraphQL API to integrate a frontend application to a backend system.
We won't go any further with GraphQL and Apollo here, but rest assured that there is a whole lot more to learn. For instance, here we have only performed read operations, but GraphQL also supports modifications through mutations.
GraphQL APIs are getting really popular lately, so make sure to check out the following references if you want to continue learning about those.
Check out these references:
- Apollo GraphQL client: https://www.apollographql.com/docs/react
- React Apollo: https://www.apollographql.com/docs/react
- Great GraphQL tutorial: https://www.howtographql.com
- Official GraphQL documentation: https://graphql.org/learn
- Generating types with Apollo: https://www.leighhalliday.com/generating-types-apollo
- Relay: https://relay.dev