Hello, and welcome to the Marvel API project. This repository includes a React application that consumes and displays data from the Marvel API. Specifically, this application allows users to search for Marvel characters (eg. Ghost Rider) and comics (eg. The Amazing Spider-Man) by name.
To get started, clone or download this repository to your local filesystem.
Once you've cloned or downloaded this repository you your local filesystem,
navigate to the root directory and run the following command: npm install
.
After the dependencies have been installed, boot up the application by running:
npm run start
. This will create a local server that listens for requests on
localhost:3000
. To confirm that everything is working as expected, navigate to
localhost:3000
in your browser of choice.
This repository includes a series of branches, each of which contain a different version of the application:
master
:- Contains a 'featureless' version of the application. All UI components are present, but the search functionality has not been implemented. Use this branch if you're starting the exercise from scratch.
ex-1/characters-search
:- This branch allows users to search for characters by name. Each result includes the name of the character and a thumbnail image, both of which are displayed in a 'card' format.
ex-2/character-details
:- This branch allows user to view details about a specific character.
ex-3/load-more
:- This branch allows users to view additional characters by clicking on a 'Load More' button.
ex-4/comics
:- This branch includes the final version of the application. In addition to the features listed above, this version allows users to search for Marvel comics by name, to 'Load More' comics, and to view details about specific comics.
Before you start this exercise, make sure that the application can be run via
npm run start
.
The goal of this exercise is to register for Marvel developer account, create a new Marvel API key, and add the API key to the application.
Start by navigating to the Marvel API Documentation page and clicking on the 'Get A Key' link located near the top of the page. If you do not already have a Marvel Developer account, you will be prompted to create one.
To create a new Marvel Develop account, following the instructions provided. After you've successfully created an account, the Marvel website should redirect you the 'Get A Key' page. If it doesn't, navigate back to the Marvel API Documentation page and click on the 'Get A Key' link near the top of the page.
At this point you should be taken to the Marvel Developer Account page. You should be presented with a public and private API key by default. If you see the public and private keys, proceed to the next step. If you do not, return to the Marvel API Documentation page and following the instructions for creating an API key.
By default, the Marvel API will only respond to requests that originate from the
marvel.com
website. Since our application will be running on localhost
,
any requests it makes to the Marvel API will fail.
To fix this we must add localhost
to the list of valid domains. Navigate to
the Marvel Developer Account page and
scroll down until you see an area labelled 'Your authorized referrers'. Click
the 'Add a new referrer' link and enter localhost
within the newly created
field. Finally, scroll down and click on the 'Update' link in the bottom right
hand corner of the page. If the referrer was added successfully, an 'Account
Updated' message will be displayed.
Finally, we must add the new API key to our application. To do this, we'll start
by copying the public API key from the
Marvel Developer Account page. Then
we'll open the .env
file in the root of the repository and replace the
<REPLACE_WITH_YOUR_OWN_KEY>
string with the public API key.
Since the application only reads the .env
file when the development server
spins up, make sure to restart the application.
At this point your application should have access to a valid key that will allow it to make up to 3,000 requests against the Marvel API per day.
Before you start this exercise, please make sure that you've registered for a
Marvel API key, added it to the .env
file, and are able to run the
application. An overview of how to do this has been provided in Exercise 0.
The goal of this exercise is to allow users to search for Marvel characters by name. Currently, interacting with the search bar has no effect. When we're done, entering a character's name and clicking 'Search' will retrieve any related data from the Marvel API and display the results in a 'card' format.
This application contains a dedicated 'loading' component (<Loading />
) that
we will display whenever a search request is in flight. Additionally, the
App
's render()
method is already set up to display this component whevener
the application is in a'loading state.
To put the application in a loading state, open the App.js
file, navigate to
the fetchCharacters()
method, and add the following line:
this.setState({ isLoading: true });
This tells the application to enter a loading state whenever the
fetchCharacters()
method is called, meaning that our <Loading />
component
will be displayed. You can test this functionality by booting up the app and
submitting a search via the search bar.
Although the App
has a method called fetchCharacters()
, the actual request
to the Marvel API will be handled by our MarvelService
. The MarvelService
's
responsibility is to interact with the Marvel API, and to make the response data
available to the App
.
With that in mind, we'll update the App
's fetchCharacters()
to call the
MarvelService
's getCharacters()
method. To do this, we'll add the following
line to the fetchCharacters()
method:
this.marvelService.getCharacters();
Great, we're now calling the getCharacters()
method whenever the user submits
a new search. However, there is only small problem: it doesn't actually do
anything. Not to worry, we'll address that in this step.
In order to retrieve character data from the Marvel API, the getCharacters()
method must do the following:
- Prepare an object of request parameters.
- Extract the correct endpoint from the
ENDPOINTS
object. - Dispatch the request using Axios.
Additionally, getCharacters()
will be responsble for extracting the meaningful
data for the response and returning it in the form of a Promise. No big deal?
Alright then, let's get started!
First we'll define a new local variable called params
. For the value, we'll
start with an empty object. Add the following to getCharacters()
:
const params = {};
This object will eventually be passed to Axios, which will convert it into a
series of query string parameters (eg. '?key=value', etc.). With that in mind,
let's update the object so that it's not empty. Within the object literal,
spread out the contents of the config
parameter. This parameter will be
provided whenever getCharacters()
is called, and will contain information that
should be part of each request. At this point our params
assignment should
look something like this:
const params = { ...config };
This is a great start, but we're missing one important piece: the API key. Each
request to the Marvel API must include the API key. If it doesn't, the request
will fail. Luckily our MarvelService
contains a method whose sole purpose is
to return the API key: getAuthConfig()
.
Since getAuthConfig()
returns an object that contains the API key, we can call
this method and spread the result into our params
object. Update the params
assignment so that it looks like the following:
const params = { ...config, ...this.getAuthConfig() };
Great, we now have an object of params that contains everything in config
, as
well as the API key that each request requires.
Next we'll define the local endpoint
variable, which will contain the Marvel
API's characters endpoint. The MarvelService
exposes the ENDPOINTS
class
property, which includes all of the endpoints that we need for this exercise.
For this step, add the following line below the params
assignment:
const endpoint = MarvelService.ENDPOINTS.characters;
Alright, we have our params
and our endpoint
. All that's left to do is
make the request! To do this, we'll call axios.get()
, passing in the endpoint
as the first argument, and an object containing our params as the second
argument. At this point, our code should look something like this:
const params = { ...config, ...this.getAuthConfig() };
const endpoint = MarvelService.ENDPOINTS.characters;
axios.get(endpoint, { params: params });
Hooray, we've made a request to the chracters endpoint using the parameters
provided. However, we said that we also wanted to parse and the response, which
we're not doing at the moment. To fix this, let's add a then()
method the
end of our get()
call. We'll provide a callback that will be invoked with the
response
rom the Marvel API. At this point, the Axios code snippet should look
something like this:
axios.get(endpoint, { params: params })
.then((response) => {
...
});
Now that we have access to the API response, we can inspect, parse, and return
it. As a starting point, trying printing the response
using console.log()
.
You'll see it's a generic Axios response, which contains information like
config
, headers
, and status
. It also includes a property called data
,
which points to the information sent back by the Marvel API.
The Marvel API response contains a lot of information, but we'll only
interested in a small portion of it. Specifically, the API response also
includes a key of data
, which is what we want to make avaialble to our
application. Let's update the Axios code snippet so that it looks like this:
axios.get(endpoint, { params: params })
.then((response) => {
return response.data.data;
});
Great, we're almost done. As a last step, we'll make the Axios Promise available
wherever getCharacters()
was invoked by adding a return
statement just
before axios.get()
. At this point, our code should look something like this:
return axios.get(endpoint, { params: params })
.then((response) => {
return response.data.data;
});
Now that the getCharacters()
method is correctly dispatching our search
request, we should update the App
component's fetchCharacters()
method to
make use of it. To do this, we'll chain a then()
method onto the
getCharacters()
call. We'll also provide a callback that will be invoked with
data
. At this point, the getCharacters()
code snippet should look something
like this:
this.marvelService.getCharacters()
.then((data) => {
...
});
As a sanity check, let's print out the data
using console.log()
. Now,
whenever we submit a search, the console should display an object containing
results
, total
, offset
, and count
.
Now that we've identified where the actual search results are stored (ie. at the
results
property of the data
object), we can add them to our application
state using setState()
. Let's update the then()
callback to do exactly this:
this.marvelService.getCharacters()
.then((data) => {
this.setState({ results: data.results });
});
This will cause the application to re-render using the results of the most
recent search. However, since our application is in loading state, the results
will not actually be displayed. To fix this, we'll tell the application that
it should not longer be in a loading state after we receive search results. We
can make use of our new setState()
call to do this:
this.marvelService.getCharacters()
.then((data) => {
this.setState({ results: data.results, isLoading: true });
});
If you tested the previous step then you may have noticed that something is not quite right. Submitting a new search does fetch and display Marvel Character data. However, each search returns the exact same results, regardless of the search term.
Our application is capturing the latest search term under the searchTerm
property of the application state, but we're not including it as part of our
requests to the Marvel API. To fix this issue we'll have to make a small
adjustment to our fetchCharacters()
method.
Within fetchCharacters()
, we're currently calling the getCharacters()
method
of the MarvelService
. However, notice that we haven't provided any arguments.
If you completed Step 3 recently, you'll remember that getCharacters()
expects
to be called with an object that includes the request parameters.
The Marvel API is fairly restrictive, which means that we can only send valid
request parameters. Sending invalid or unknown parameters will cause our
requests to fail. When we're requesting character data, the Marvel API accepts
a parameter called nameStartsWith
, which it will use tailor the results of the
search. Let's update our use of getCharacters()
to include an object with this
paramter:
this.marvelService.getCharacters({ nameStartsWith: this.state.searchTerm })
.then((data) => {
this.setState({ results: data.results, isLoading: true });
});
Notice that we used this.state.searchTerm
as the value for the
nameStartsWith
parameter. This ensures that each search request includes the
latest search term.
If we re-test the search flow, we should see that the search results now relate to the current search term. Test it out by searching for 'Spider-Man'.
The final step for this exercise is to account for cases where our requests
fail. This will require another slight adjustment to the fetchCharacters()
method.
Let's chain a catch()
method to the existing then()
method. This will be
called if and when the API request fails. We'll pass in a callback function,
which will be invoked with an error object.
this.marvelService.getCharacters({ nameStartsWith: this.state.searchTerm })
.then((data) => {
this.setState({ results: data.results, isLoading: true });
})
.catch((err) => {
...
});
Within the body of the catch()
callback, let's put our application into an
error state by calling setState()
and passing in { hasError: true }
. This
will provide a visual cue to the user that something has gone wrong. At this
point, our code should look something like this:
this.marvelService.getCharacters({ nameStartsWith: this.state.searchTerm })
.then((data) => {
this.setState({ results: data.results, isLoading: true });
})
.catch((err) => {
this.setState({ hasError: true });
});
Excellent, you've successfully added the character search funtionality to the application, now why don't you give it a whirl. If everything is working as expected, you should be able to:
- Search for characters by name.
- See the results in a 'card' format.
Before starting this exercise, please make sure to complete exercise 1.
Alternatively, you may create a branch from ex-1/characters-search
, which
includes the functionality introduced as part of exercises 1.
The goal of this exerise is to allow users to view additional information about a given character. In terms of UX, the user will click on a character 'card', which will cause a modal window with additional information to be displayed. All of the required components exist, but it's up to us to define the methods that will fetch the additional data.
We'll start by updating the fetchCharacter()
method within the App
component. Currently, this method doesn't do anything. However, it does include
some instructions (in the form of 'TODO') notes. We'll following the first note
by invoking the getCharacter()
method of the MarvelService
. At this point,
our code should look something like this:
this.marvelService.getCharacter();
Spoiler: the getCharacter()
doesn't do anything at the moment. Not to worry,
we'll build it out as part of the next step. However, before we do that, we're
going to update our use of it. Notice that getCharacter()
defines an id
paramter, which is the unique identifier for the character that we're interested
in. Let's pass this id
into getCharacter()
as an argument. Update the
getCharacter()
code snippet so that it looks like this:
this.marvelService.getCharacter(id);
Alright, now we can move on to the getCharacter()
method of the
MarvelService
. Just like getCharacters()
, this method will have the
following responsibilities:
- Prepare an object of request parameters.
- Extract the correct endpoint from the
ENDPOINTS
object. - Dispatch the request using Axios.
Let's start with the request parameters. In this case, we can actually copy and
paste the params
assignment from getCharacters()
into getCharacter()
. When
you're done, you should have something like this:
const params = { ...config, ...this.getAuthConfig() };
Then we'll move on to defining the endpoint
variable, which will be a bit
different. We'll start by assigning endpoint
equal to the 'character'
endpoint, like so:
const endpoint = MarvelService.ENDPOINTS.character;
However, since the character ID must be included in the request endpoint, we'll
update our endpoint
assignment to include it. We can use either string
concatenation or a template literal to achieve this, the choice is yours. The
string concatenation approach will give you something like this:
const endpoint = MarvelService.ENDPOINTS.character + '/' + id;
Dont' forget to separate the 'character' endpoint from the id
using a forward
slash!
Now that we have our params
and endpoint
, we'll use axios.get()
to
dispatch the request. Add the following below the params
and endpoint
assignments:
axios.get(endpoint, { params: params });
Since we know that we're going to return the result of this request in the form
of a Promise, let's add the return
keyword just before axios.get()
:
return axios.get(endpoint, { params: params });
Alright, we're making a request for character data, but we're not actually doing
anything with the response. Just like with getCharacters()
, we'll solve this
by adding a then()
method to the end of our axios.get()
call. We'll also
provide a callback function that will be invoked with an Axios response
object. At this point, our Axios code snippet should look like this:
return axios.get(endpoint, { params: params })
.then((response) => {
...
});
Both Axios and the Marvel API are consistent in terms of their responses,
meaning that the body of this callback can be exactly the same as the
getCharacters()
callback. With that in mind, let's return
response.data.data
. Now our Axios code snippet should look something like
this:
return axios.get(endpoint, { params: params })
.then((response) => {
return response.data.data;
});
Great, we're now making a request for specific character data and returning the
result in the form of a Promise. Let's jump back into the fetchCharacter()
method so that we can make use of the new data.
We'll start by chaining a then()
method to the end of getCharacter()
method.
We'll also provide a callback that will be invoked with the Marvel API data
.
Like the data
returned by getCharacters()
, this will be an object with the
following keys: results
; total
; offset
; and count
. At this point, our
code should look something like this:
this.marvelService.getCharacter(id)
.then((data) => {
...
});
We're only interested in the results
property at the moment, but there is a
small quirk. results
is actually an array containing a single item. Since our
application expects the character data to be an object, we'll have to extract
the first item from the array before we update the application state. To do
this, update the body of the then()
callback like so:
this.marvelService.getCharacter(id)
.then((data) => {
const result = data.results[0];
});
Now that we have a single result, we can update the application state by setting
the selectedResult
property. To do use, add a setState()
call within the
body of the callback like so:
this.marvelService.getCharacter(id)
.then((data) => {
const result = data.results[0];
this.setState({ selectedResult: result });
});
Just like the previous exercise, The final step for this exercise is to account
for cases where our requests fail. This will require another slight adjustment
to the fetchCharacter()
method.
Let's chain a catch()
method to the existing then()
method. This will be
called if and when the API request fails. We'll pass in a callback function,
which will be invoked with an error object.
this.marvelService.getCharacter(id)
.then((data) => {
const result = data.results[0];
this.setState({ selectedResult: result });
})
.catch((err) => {
...
});
Within the body of the catch()
callback, let's put our application into an
error state by calling setState()
and passing in { hasError: true }
. This
will provide a visual cue to the user that something has gone wrong. At this
point, our code should look something like this:
this.marvelService.getCharacter(id)
.then((data) => {
const result = data.results[0];
this.setState({ selectedResult: result });
})
.catch((err) => {
this.setState({ hasError: true });
});
Awesome, now users can view additional information about specific characters. Make sure that everything is working as expected by:
- Searching for a character by name (eg. 'Spider-Man').
- Clicking on one of the search results.
- Viewing the additional information within the details modal.
Before starting this exercise, please make sure to complete exercises 1 and 2.
Alternatively, you may create a branch from ex-2/character-details
, which
includes the functionality introduced as part of exercises 1 and 2.
The goal of this exercise is to introduce a 'Load More' feature as part of the character search experience. Currently, users are only able to see the first 20 search results for a particular search term, even if the API is able to return additional character data. Once implemented, the 'Load More' feature will allow users to view additional results for a specific search term.
Since we're going to add a 'Load More' button, we need some way of determining if and when it should be shown. Luckily, the Marvel API gives us just the information we need.
In addition to the results
field, the /characters
endpoint returns values for total
, count
, and offset
. total
represents
the total number of results for the search term, count
represents the number
of results returned as part of the current request, and offset
represents the
number of results that were skipped (more on that later). Using these three
values, we can determine whether the application should display the 'Load More'
button.
Within fetchCharacters()
, the callback function that we pass to our then()
method is setting the results
property of the application state equal to
data.results
. Let's update it to also set a new property: canLoadMore
.
canLoadMore
should contain a boolean value, which our application will use to
show or hide the 'Load More' button. Within our setState()
call, let's add a
key of canLoadMore
. For the value, we'll add an expression that will resolve
to either true or false: data.total > data.offset + data.count
. At this point,
you should have something like this:
this.setState({
results: data.results,
canLoadMore: data.total > data.offset + data.count,
});
In this expression we're asserting that the total number of results is greater
than the offset ('skipped') plus the results returned as part of the cureent
request. If this is true, then there are more results to display. If this is
false, then there aren't. In either case we're captured the correct value within
the canLoadMore
property of the application state.
Now that we know whether or not to display the 'Load More' button, we can add it
to our App
component.
Within the App
component, import the LoadMore
component like so:
import { LoadMore } from './components/LoadMore';
Once the component is available, we'll update the App
's render()
method to
make use of it. Start by defining a new variable called loadMoreElem
. If the
canLoadMore
property of the application state is true
, then loadMoreElem
should contain the <LoadMore />
component. Otherwise, loadMoreElem
should
contain an empty string (this will ensure that it's not displayed in cases where
it shouldn't be). At this point, you should have updated the render()
method
to include something like this:
let loadMoreElem = '';
if (this.state.canLoadMore) {
loadMoreElem = <LoadMore />;
}
Add the loadMoreElem
to the return value of the render()
method, in between
the resultsElem
and the detailsElem
. At this point, the return value should
look something like this:
...
{ resultsElem }
{ loadMoreElem }
{ detailsElem }
...
At this point, our 'Load More' button should be shown or hidden depending on whether the API is able to return additional results. However, clicking on the button doesn't do anything. Let's fix that now!
We'll start by defining a new method in our App
: fetchMoreCharacters()
. This
method will be similar to the existing fetchCharacters()
, except for two main
differences:
- This method will not put the application in a loading state (eg. the existing results will remain visible as we retrieve additional results).
- This method will add the additional results to the existing results.
Let's start by copying the body of the fetchCharacters()
method into our new
fetchMoreCharacters()
method. After that we'll remove the first line that
calls setState()
(ie. the one that sets isLoading
to true
).
In order to work properly, fetchMoreCharacters()
needs to retrieve new
results, rather than what's already on the page. In order to do this, we can
take advantage of the the Marvel API's offset
parameter, which allows us to
'skip' a certain of results. Our servie is already set up to receive the
offset
parameter, we just have to provide the right value.
To do this, we'll add an offset
key to the object that we pass to
getCharacters()
. For the value, we'll use: this.state.results.length
. This
means that the Marvel API will 'skip' the number of results equal to however
many results we've already retrieved. If we've retrieved 20 results, calling
fetchMoreCharacters()
will include an offset
value of 20, etc. At this
point, your getCharacters()
call should look something like this:
this.marvelService.getCharactersa({
nameStartsWith: this.state.searchTerm,
offset: this.state.results.length,
});
Once we've update the getCharacters()
call to include the offset
value,
we'll update the then()
callback. Since we are no longer setting
isLoading
to true
when fetchMoreCharacters()
is called, we can remove the
isLoading: false
key/value pair.
Finally, we can move on to updating the result
handling logic. Instead of setting the results
property of the application
state to data.results
, let's combine the additional and existing results into
a new array. This ensures that existing results remain visible to the user. When
you're done, the setState()
call within the then()
callback should look
something like this:
this.setState({
results: [...this.state.results, ...data.results],
canLoadMore: data.total > data.offset + data.count,
});
Our new fetchMoreCharacters()
method is set up to request additional character
data, but we should always be in the habit of accounting for errors.
If you used fetchCharacters()
as a starting point for fetchMoreCharacters()
,
then the catch()
method should already be in place. If you wrote
fetchMoreCharacters()
from scratch, make sure to add a catch()
method after
then()
. Provide a callback function that accepts an error, prints the error to
the console, and puts the application into an error state. When you're done, you
should have something like this:
...
.then((data) => {
...
})
.catch((err) => {
console.error(err);
this.setState({ hasError: true });
});
Now that we've defined fetchMoreCharacters()
, it's time to make use of it.
Within our App
's render()
method, update the loadMoreElem
assignment as
follows:
let loadMoreElem = '';
if (this.state.canLoadMore) {
loadMoreElem = <LoadMore onClick={ this.fetchMoreCharacters } />;
}
Since the fetchMoreCharacters()
method will be called in the context of a
child component, we must bind it to ensure that this this
value points to the
App
component. To do this, add the following line to the App
's
constructor()
method:
this.fetchMoreCharacters = this.fetchMoreCharacters.bind(this);
Alright, you've completed all of the steps to add the 'Load More' functionality. All that's left to do is test the new feature (and fix anything that may not be working as expected).
To test this feature, try using a search term that is likely to return more that 20 results (eg. 'Spider'). If everything works as expected, the first 20 results should be displayed, followed by the 'Load More' button. Clicking the 'Load More' button should dispatch a request for 'more characters', which will be added to the bottom of the page. This process should repeat until all of the results for the current search term have been retrieved and displayed.
Before starting this exercise, please make sure to complete exercises 1, 2, and
3. Alternatively, you may create a branch from ex-3/load-more
, which includes
the functionality introduced as part of exercises 1, 2, and 3.
The goal of this exercise is to allow users to search for comics for title, 'load more' comics, and view information about a specific comic. Essentially, we're going to be duplicate the existing features, which focus on characters.
This exercise requires quite a few updates, but we'll be able use the existing features and functionality for reference.
Currently, our application only supports character-type searches. Since we're going to support comic-type searches as well, we'll need to update the applicaton state to track which type of search is currently active.
To do this, we'll add the searchType
property to state
object within the
App
constructor()
method. Since character-type searches will be the default,
we'll give it a value of 'Characters' to start. When you're done, the state
assigment in the App
constructor()
should look something like this:
...
this.state = {
searchTerm: '',
searchType: 'Characters',
results: [],
selectedResult: null,
};
...
We need to allow the user to set the type of search. To do this, we'll update
the <SearchBar />
component.
We'll start by passing the searchType
value into <SearchBar />
as a prop.
This will require a slight modification within the App
's render()
method.
When you're done, the <SearchBar />
reference should look something like this:
<SearchBar
...
searchType={ this.state.searchType }
...
/>
Next we'll update the SearchBar
class to render a component that will allow
the user to set the search type: <SearchTypeControls />
. Within
SearchBar.js
, import the SearchTypeControls
as follows:
import { SearchTypeControls } from './SearchTypeControls';
Now we can update the SearchBar
component's render()
method to include a
reference to the new component. To do this, add the <SearchTypeControls />
in
between the <input />
element and the <button>
. Additionally, pass the
searchType
value into the <SearchTypeControls /> as a prop of the same name. When you're done, the
SearchBar
render()` method should include somthing like
this:
...
<SearchTypeControls
searchType={ this.props.searchType }
/>
...
At this point, we should be able to preview the new UI. Booting up the app will
show us that the 'Characters' search is active by default. Since we set the
searchType
property to 'Characters' within the App
constructor()
, this
makes sense. However, clicking on either of the options exposed by the
SearchTypeControls
has no effect. Let's fix this next!
First we'll update the <SearchBar />
component within the App
's render()
method. Currently, the <SearchBar />
is given a function that updates the
searchTerm
whenever it's called. This function is passed into the
<SearchBar />
via the onSubmit
prop. We'll use a similar approach to allow
the user to updated the searchType
property.
We'll start by adding a onSelect
property to the <SearchBar />
component.
For the value, we'll define an arrow function that accepts a searchType
parameter, and uses it to update the searchType
via setState()
. When you're
done, the <SearchBar />
component reference should look something like this:
<SearchBar
...
onSelect={ (seachType) => this.setState({ searchType }) }
...
/>
Next we'll jump back into the SearchBar
class definition, where we'll
update the <SearchTypeControls />
to make use of the new prop. We'll add two
new props to the <SearchTypeControls />
component:
onCharactersClick
.onComicsClick
.
For the values, we'll define two new arrow functions. The
onCharactersClick
arrow function will call this.props.onSelect()
, passing in
the string 'Characters'. The onComicsClick
arrow function will call
this.props.onSelect()
, passing the string 'Comics'. When you're done, your
` should look something like this:
<SearchTypeControls
...
onCharactersClick={ () => this.props.onSelect('Characters') }
onComicsClick={ () => this.props.onSelect('Comics') }
...
/>
Since we're going to be fetching an entirely new type of data — comics — we'll
need to update the MarvelService
to include the appropriate method. Luckily,
the logic should be almost exactly the same as an exsting method:
getCharacters()
.
We'll start by copying and pasting the MarvelService
's getCharacters()
back
into itself, and renaming it to getComics()
. Then we'll update the endpoint
assignment so that it's equal to MarvelService.ENDPOINTS.comics
. (rather than
...ENDPOINTS.characters
).`
Just like with the character-type searches, the App
component will expose a
method that calls the underlying MarvelService
. In this case, we'll call the
new method fetchComics()
.
Let's start by copying and pasting the App
's fetchCharacters()
method back
into itself, and renaming it to fetchComics()
. Next we'll update the new
method so that it calls this.marvelService.getComics()
, rather than
getCharacters()
. Finally, we'll update the object that's passed to
getComics()
, swapping our the nameStartsWith
key for titleStartsWith
. This
last update is required by the Marvel API. When searching for characters, the
API accepts the nameStartsWith
parameter. However, when searching for comics,
the equivalent parameter is titleStartsWith
.
If you used the existing fetchCharacters()
method, then the error handling
logic should already be in place. If not, match sure to account for errors by
adding a catch()
method call after the then()
.
At this point, the fetchComics()
method should look something like this:
fetchComics() {
this.setState({ isLoading: true, hasError: false });
this.marvelService.getComics({
titleStartsWith: this.state.searchTerm,
})
.then((data) => {
this.setState({
results: data.results,
isLoading: false,
canLoadMore: data.total > data.offset + data.count,
});
})
.catch((err) => {
console.error(err);
this.setState({ hasError: true });
});
}
To see our new fetchComics()
method in action, we'll have to update the
exiting componentDidUpdate()
method.
Currently the componentDidUpdate()
method fetches new character data (via
fetchCharacters()
) whenever the user submits a new search time. We'll need to
update the existing expression to also check for the presence of a new
searchType
. To start, create two new variables:
searchType
: This will be equal to the currentsearchType
, extracted fromthis.state
.prevSearchType
: This will be equal to the lastsearchType
, extracted fromprevState
.
At this point, you should have added something like this:
const searchType = this.state.searchType;
const prevSearchType = prevState.searchType;
Next, we'll update our expression to include the searchType
and
prevSearchType
values. To describe the logic in plain terms, we want to fetch
new data when either of the following are true:
- A search term exists and the current search term is different from the last search term.
- A search term exists and the current search type is different from the last search type.
We can implement this as follows:
if (
searchTerm
&& (searchTerm !== prevSearchTerm || searchType !== prevSearchType)
) { ... }
Notice the searchTerm
and searchType
assertions are grouped within the same
set of parentheses.` This is important to ensure that expression is evaluated in
the correct order.
At this point, the application should correctly fetch new data whenever the
search term changes or the search type changes. However, changing the search
type will cause the application to fetch character data, which is not what we
want. To correct this, we'll update the body of our if
statement as follows:
if (
searchTerm
&& (searchTerm !== prevSearchTerm || searchType !== prevSearchType)
) {
if (searchType === 'Characters') {
this.fetchCharacters();
} else {
this.fetchComics();
}
}
Now the application will fetch character data when the searchType
is
'Characters', and comic data when the searchType
is 'Comics'.
Our application is coming along nicely, but it looks like we've introduced a defected. When we search for comics the card elements don't display the title of each result. The Marvel API returns this data, but not under the same property as the character results.
To fix this issue we need to update the ResultsList
. If the current
searchType
is 'Characters', then the ResultsList
will extract the name
property of the each result and pass it down to the ResultCard
component.
Alternatively, if the current searchType
is 'Comics', the ResultsList
will
extract and provide the title
property.
We'll start by passing the searchType
down from the App
into the
<ResultsList />
as a prop of the same name. Our <ResultsList />
component
should now look something like this:
<ResultsList
...
searchType={ this.state.searchType }
...
/>
Next we'll update the ResultsList
class definition. Within the render()
method, we'll update the loop which creates the <ResultCard />
components.
We'll update the value of the title
prop to assert that the current
searchType
is 'Characters'. If it is, then we'll pass in the result.name
as
normal. If it's not, then we're displaying comic results and we can pass in
result.title
.
At this point, the <ResultCard />
-related code should look something like
this:
<ResultCard
title={ this.props.searchType === 'Characters' ? result.name : result.title }
/>
Now that we've fixed the title defect, we can move on to implementing 'Load More' funtionality for comic-type searches. This step is a variaton of the 'Load More' exercise, with a few minor adjustments.
We'll start by copying and pasting the fetchMoreCharacters()
method from the
App
back into itself, and renaming it to fetchMoreComics()
. We'll also
change the nameStartsWith
property to titleStartsWith
, to account for the
slight difference in the Marvel API. At this point, our new method should look
something like this:
fetchMoreComics() {
this.marvelService.getComics({
titleStartsWith: this.state.searchTerm,
offset: this.state.results.length,
})
.then((data) => {
this.setState({
results: [...this.state.results, ...data.results],
canLoadMore: data.total > data.offset + data.count,
})
})
.catch((err) => {
console.error(err);
this.setState({ hasError: true });
});
}
Since this method will be used within the <LoadMore />
component, me must bind
it to ensure that the this
value always refers to the App
component. To do
this, add the following line to the App
's constructor()
method.
this.fetchMoreComics = this.fetchMoreComics.bind(this);
Finally, we'll update the <LoadMore />
component reference so that it receives
either fetchMoreCharacters()
or fetchMoreComics()
based on the current
searchType
. To do this, we'll update the onClick
prop as follows:
<LoadMore
onClick={
this.state.searchType === 'Characters'
? this.fetchMoreCharacters
: this.fetchMoreComics
}
/>
Now the 'Load More' functionality should work for both character and comic-type searches.
Alright, we're down to the last feature: allowing users to view details about a specific comic. Luckily this feature already exists for character-type searches, so we have a point of reference.
We'll start by copying and pasting the MarvelService
's getCharacter()
back
into itself, and renaming it to getComic()
. Then we'll update the endpoint
assignment so that it's equal to MarvelService.ENDPOINTS.comic
. (rather than
...ENDPOINTS.character
).` Since this method performs basically the same
operation, we don't need to make any other changes.
Next we'll copy and paste the App
's fetchCharacter()
method back
into itself, and rename it to fetchComic()
. Then we'll update the new
method so that it calls this.marvelService.getComic()
, rather than
getCharacter()
. As both getCharacter()
and getComic()
search by ID
(rather than by the nameStartsWith
or titleStartsWith
parameters), no
further changes are necessary.
Since this method will be used within a child component, me must bind
it to ensure that the this
value always refers to the App
component. To do
this, add the following line to the App
's constructor()
method.
this.fetchComic = this.fetchComic.bind(this);
As a last step, we'll update the <ResultsList />
component so that eithe
receives either the fetchCharacter()
or fetchComic()
method based on the
current searchType
. If the searchType
is equal to 'Characters', then we'll
pass in fetchCharacter()
. Alternatvely, if the searchType
is 'Comics', then
we'll pass in fetchComic()
. To do this, update the <ResultsList />
component
reference so that the onResultClick
prop looks like the following:
<ResultsList
...
onResultClick={
this.state.searchType === 'Characters'
? this.fetchCharacter
: this.fetchComic
}
...
/>
At this point, clicking on a comic-type search result should fetch additional data about that comic, and display the result within the result details modal.
That's it, you made it! All that's left to do is test to make sure everything works as expected. Oh, and to fix anything that doesn't of course!
To test the new features, boot up the application and complete the following:
- Perform a comic-type search by clicking on the 'Comics' button within the search bar, entering a search term, and clicking 'Search'. If everything is working as expected, the search results should be comics (rather than characters).
- Load more comics by performing an initial search for 'Spider', scrolling to the bottom of the results, and clicking on the 'Load More' button. If everything is working as expected, additional comic-type results should be loaded.
- View comic details by performing a comic-type search and clicking on any of the results. If everything is working as expected, the details modal should contain information about the selected comic.
- Marvel API:
- Account
- Documentation
- API Key Registration
- Please note: this requires a free Marvel developer account.
- Axios:
Concept and material prepared by Jesse R Mykolyn for General General Assembly's Bitmaker.
All character and comic information is the property of their respective owners.