Inferno JS: Build and Authenticate an App - Part II
In Part II of this series, we are going to create a loading component, list component, and a detail component!
Join the DZone community and get the full member experience.
Join For FreeDid you miss Part I? Catch up here.
Create a Loading Component
In our App.js
file, we're simply showing <p>Loading...</p>
when there isn't any dinosaur data. However, this should be an error instead if the API isn't available. You may have noticed that we put error
in the state but didn't use it in render()
yet. Let's create a small component that conditionally shows a loading image or an error message.
Create a new folder: src/components
. All components from now on will go into this folder. Next create a subfolder called Loading
to contain our new component's files.
Let's add a loading image. You can grab the raptor-running.gif
from GitHub here. Download the image into the src/components/Loading
directory.
Create an empty file for the loading CSS: Loading.css
.
Loading Component JS
Next, add a new JS file called Loading.js
:
// src/components/Loading/Loading.js
import Inferno from 'inferno';
import Component from 'inferno-component';
import loading from './raptor-loading.gif';
import './Loading.css';
class Loading extends Component {
render(props) {
return(
<div className="Loading"> { !props.error ? ( <img className="Loading-img" src={loading} alt="Loading..." /> ) : ( <p className="alert alert-danger"><strong>Error:</strong> Could not retrieve data.</p> ) } </div> ); } } export default Loading;
Let's import the loading image and CSS. Whenever we use our Loading
component, we're going to pass the parent component's error
state as a property. We can then pass props
to our render()
function to access properties without needing this
. We'll render the loading image if there's no error
, and show an alert if there is.
Loading Component CSS
Add the following CSS to Loading.css
:
/* src/components/Loading/Loading.css */
.Loading-img {
display: block;
margin: 10px auto;
}
Let's verify that our src
file structure looks like this:
public
server
src
|-components |-Loading
|-Loading.css |-Loading.js
|-running-raptor.gif |-utils
|-ApiService.js |-App.css
|-App.js |-App.test.js
|-index.css |-index.js
Add Loading Component to App
Now we can use this component in our App.js
and replace the <p>Loading...</p>
element we added earlier:
// src/App.js
...
import Loading from './components/Loading/Loading';
...
render(props, state) {
...
{
state.dinos ? (
...
) : (
<Loading error={state.error} /> ) }
Now while data is being fetched from the API, the running dinosaur gif is shown. If we stop the API server and reload the app, we'll see our error message instead of an infinite loading state:
Create a List Component
Let's explore creating a component that displays the list of dinosaurs and lets users select one to show additional details. This will replace the simple unordered list that we put in the App.js
file.
Create a new folder containing CSS and JS files for the DinoList
component:
src
|-components |-DinoList
|-DinoList.css |-DinoList.js
List Component JS
Add the following code to DinoList.js
:
// src/components/DinoList/DinoList.js
import Inferno, { linkEvent } from 'inferno';
import Component from 'inferno-component';
import ApiService from './../../utils/ApiService';
import Loading from './../Loading/Loading';
import './DinoList.css';
/* This function is pulled out of the class to demonstrate how we could easily use third-party APIs */
function getDinoById(obj) {
const id = obj.id;
const instance = obj.instance;
// Set loading state to true while data is being fetched
// Set active state to index of clicked item
instance.setState({
loading: true,
active: id
});
// GET dino by ID
// On resolve, set detail state and turn off loading
ApiService.getDino(id)
.then(
res => {
instance.setState({
detail: res,
loading: false,
error: false
});
},
error => {
instance.setState({
error: error,
loading: false
});
}
);
}
class DinoList extends Component {
constructor() {
super();
// Set default loading state to false
this.state = {
loading: false
};
}
render(props, state) {
return(
<div className="DinoList"> <div className="col-sm-3"> <ul className="DinoList-list"> { props.dinos.map((dino) => ( <li key={dino.id}> <a className={state.active === dino.id ? 'active' : ''} onClick={linkEvent({id: dino.id, instance: this}, getDinoById)}> {dino.name} </a> </li> )) } </ul> </div> <div className="col-sm-9"> { !state.loading && !state.error && state.detail ? ( <p>{state.detail.name}</p> ) : ( <Loading error={state.error} /> ) } </div> </div> ); } } export default DinoList;
We'll import linkEvent
from inferno
. linkEvent()
is an excellent helper function unique to Inferno. It allows attachment of data to events without needing bind(this)
, arrow functions, or closures.
The render()
function will use the props
and state
parameters. We'll pass the dinosaurs array to our DinoList
component from App.js
as a property.
The getDinoById(obj)
function at the top of the file is the event handler for when a user clicks a dinosaur's name to get dino details. This is not a method on the DinoList
class. The function is pulled out to demonstrate how components can easily leverage methods from third-party APIs with linkEvent
. The obj
parameter comes from the linkEvent()
in render()
like so:
<a
className={state.active === dino.id ? 'active' : ''}
onClick={linkEvent({id: dino.id, instance: this}, getDinoById)}>
{dino.name}
</a>
linkEvent
can pass data (as well as the event) to a handler. We're using it here to pass the clicked dinosaur's id
to call the API and apply a class to the active dinosaur in the list. We're also passing the instance (this
) so we can use instance.setState()
in our getDinoById()
function without context errors or binding.
List Component CSS
Next, add the following to the DinoList.css
file to style the list of dinosaurs:
/* src/components/DinoList/DinoList.css */
.DinoList a {
cursor: pointer;
}
.DinoList a.active {
font-weight: bold;
text-decoration: underline;
}
Add List Component to App
In the App.js
file, let's replace our unordered list with the new DinoList
component:
// src/App.js
...
import Loading from './components/DinoList/DinoList';
...
render(props, state) {
...
{
state.dinos ? (
<DinoList dinos={state.dinos} /> ) : ( ... ) }
At this point, when a dinosaur in this list is clicked, the only "detail" we're showing is the dinosaur's name. Also, because we don't make an API call automatically on load, the UI will show the loading image in the details area until the user clicks on a dinosaur in the list. Clearly this isn't ideal. We'll create a DinoDetail
component next to display this in a much nicer way.
Create a Detail Component
Let's make a new folder for our DinoDetail
component: src/components/DinoDetail
. We'll only use Bootstrap to style this component, so a CSS file won't be necessary.
Detail Component JS
Let's build the DinoDetail.js
:
// src/components/DinoDetail/DinoDetail.js
import Inferno from 'inferno';
import Component from 'inferno-component';
class DinoDetail extends Component {
render(props) {
let dino = props.dino;
return(
<div className="DinoList">
{
dino ? (
<div className="list-group">
<div className="list-group-item list-group-item-info">
<h3 className="list-group-item-heading text-center">{dino.name}</h3>
</div>
<div className="list-group-item">
<h4 className="list-group-item-heading">Pronunciation</h4>
<p className="list-group-item-text">{dino.pronunciation}</p>
</div>
<div className="list-group-item">
<h4 className="list-group-item-heading">Meaning of Name</h4>
<p className="list-group-item-text">"{dino.meaningOfName}"</p>
</div>
<div className="list-group-item">
<h4 className="list-group-item-heading">Period</h4>
<p className="list-group-item-text">
{dino.period} ({dino.mya} million years ago)
</p>
</div>
<div className="list-group-item">
<h4 className="list-group-item-heading">Diet</h4>
<p className="list-group-item-text">{dino.diet}</p>
</div>
<div className="list-group-item">
<h4 className="list-group-item-heading">Length</h4>
<p className="list-group-item-text">{dino.length}</p>
</div>
<div className="list-group-item">
<p
className="list-group-item-text lead"
dangerouslySetInnerHTML={{__html: dino.info}}></p>
</div>
</div>
) : (
<p className="lead">
<em>Select a dinosaur to see details.</em>
</p>
)
}
</div>
);
}
}
export default DinoDetail;
Despite the large amount of JSX, this is a very simple component. All it does is take a dino
property and display data. If there is no dino
available, it shows a message that instructs the user to select a dinosaur.
Note: The API returns HTML in some dinosaurs'
info
properties. We render this usingdangerouslySetInnerHTML
. You can read more about this in the DOM Elements section of the React docs.
Add Detail Component to List Component
Now we'll replace the detail dinosaur name in the DinoList
component with our new DinoDetail
component:
// src/components/DinoList/DinoList.js
...
import DinoDetail from './../DinoDetail/DinoDetail';
...
render(props, state) {
...
{
!state.loading && !state.error ? (
<DinoDetail dino={state.detail} /> ) : ( ... ) }
Note that we've also changed the expression to !state.loading && !state.error
. We no longer want to check for state.detail
here because we still want to display the DinoDetail
component even if there is no detail
information available yet. We only added this in the previous step to avoid errors.
When no dinosaur is selected, our app now looks like this:
When a dinosaur is clicked, its details are fetched from the API and displayed. The selected dinosaur receives an .active
class in the list, which we styled as bold and underlined in the DinoList
CSS previously:
Stay tuned for the final part in this series.
Published at DZone with permission of Kim Maida, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments