- Import and use third-party node modules into React using npm (Node Package Manager)
- Use
BrowserRouter
,Link
,Route
,Redirect
, andSwitch
to allow for navigation and URL manipulation - Define the React component lifecycle and use component methods to time API calls
- Use
axios
to query APIs for data
Up to this point, our React applications have been limited in size, thus allowing us to use basic conditional logic in our components' render methods for changing component views. However, as our React applications grow in size and scope, we will want an easier and more robust way to set up navigation to different component views. Additionally, we will want the ability to set information in the url parameters to make it easier for users to identify where they are in the application.
React Router, while not the only, is the most commonly-used routing library for React. It is relatively straightforward to configure and integrates with the component architecture nicely (itself being nothing but a collection of components). Once configured, it essentially serves as the root component in a React application and renders other application components within itself depending on the path in the url.
git clone [email protected]:ga-wdi-exercises/react-translator.git
cd react-translator
npm install
atom .
npm run start
Take 10 minutes and read through the code to familiarize yourself with the codebase with a partner, or in groups of 3. Prepare to discuss your answers the following questions:
- What dependencies is the application currently using? Where can I find information on them?
- What is the purpose of
ReactDOM.render()
? What file is this method being called in? - Where are the components of our application located? Why might we want to separate them into their own folders?
- Where is state(s) located in our application? How is state being passed down to other components?
- Is data flowing up from child components to parent components anywhere in our application? How is this happening?
- Where is our application getting data from? How is it accomplishing this?
You may have noticed our application currently uses a module named axios
. Axios is a node module commonly used with React to send HTTP requests to an API. It functions much like jQuery's Ajax method. Some benefits to using Axios:
- It is a promise-based library with an interface for simpler and cleaner syntax
- It is lightweight and focused solely on handling HTTP requests (as opposed to jQuery which brings in an extensive set of new functions and methods)
- It is flexible and has a number of very useful methods for doing more complex requests from one or multiple API endpoints
Note: Axios is just one of many Javascript libraries that we could use for handling requests. One of the big selling points of Node is the ability to mix and match technologies according to preference. Other commonly-used tools for handling requests are Fetch and jQuery.
To load in the Axios module...
// If you are using Babel to compile your code
import axios from 'axios'
// In standard vanilla Javascript
let axios = require('axios')
To use Axios to query an API at a given url endpoint...
axios.get('url')
.then((response) => {
console.log(response)
})
.catch((error) => {
console.log(error)
})
You can also append values to the parameters by passing in a second input to .get()
...
axios.get('url', {
params: {
key1: value1,
key2: value2
}
})
.then((response) => {
console.log(response)
})
.catch((error) => {
console.log(error)
})
Which would result in a GET request to: url?key1=value1&key2=value2
We will be using Axios to query the IBM Watson API in this exercise. Take 5 minutes to read and test out the Language Translator API at the link below.
General IBM Watson API Explorer
Currently, we are rendering the response from Watson's Language Translator API service within the existing Search
component. Let's bring in React Router and set up a separate component to display the results.
First, we need to install react-router-dom
and save it as a dependency to package.json
...
npm install --save react-router-dom
Then, in App.js
, we need to import all of the components we want to use from React Router...
// src/components/App/App.js
import {
BrowserRouter as Router,
Route,
Link,
} from 'react-router-dom'
Note that we are aliasing
BrowserRouter
asRouter
here for simplicity
Now that we have access to these components, we need to modify the App
component's render()
method to set up navigation. The basic structure we will use is...
// src/components/App/App.js
render() {
return (
<Router>
<div>
<nav>
<Link to=""></Link>
<Link to=""></Link>
</nav>
<main>
<Route path="" render={}/>
<Route path="" render={}/>
</main>
</div>
</Router>
)
}
- Router - the root component inside of which all
Link
's andRoute
's must be nested. It can only have one direct child element (thus the need for the enclosingdiv
tag aroundnav
andmain
)
- Link - a component for setting the URL and providing navigation between different components in our app without triggering a page refresh (similar to Angular ui-router's
ui-sref
). It takes ato
property, which sets the URL to whatever path is defined within it. Link can also be used inside of any component that is connected to aRoute
.
- Route - a component that connects a certain
path
in the URL with the relevant component torender
at that location (similar to Angular ui-router'sui-view
or erb'syield
)
Now let's customize App
's render method to provide a link to Search
...
// src/components/App/App.js
render() {
return(
<Router>
<div>
<nav>
<h1>React Translator</h1>
<Link to="/search">Search</Link>
</nav>
<main>
<Route
path="/search"
render={() => {
return (
<Search
translation={ this.state.translation }
setTranslation={ (data) => this.setTranslation(data) }
/>
)
}}
/>
</main>
</div>
</Router>
)
}
Notice that we are writing an anonymous callback function in the value for the
Route
's render property. This callback function mustreturn
a react component.
So long as we use an ES6 arrow function, this callback will preserve context, allowing us to pass down data and functions into
Search
fromApp
. You can use an ES5 anonymous function, but you will then need to use.bind()
to preserve context.
10 minute exercise / 5 minute review
- Using the above instructions as a guide, set up React Router in your own application.
- Once you have the setup described above, create a new component named
Results
, import it intoApp.js
, and set up aLink
andRoute
for it inApp
's render method. - Have the
Results
component display thetranslation
property inApp
's state by passing it toResults
via props. - Finally, remove the
translation
data fromSearch
render method (as we are now rendering it inResults
).
Solution
To set up a new Results
component...
// src/components/Results/Results.js
import React, { Component } from 'react'
class Results extends Component {
render () {
return (
<div>
<h3>Translation: </h3>
<p>{this.props.translation}</p>
</div>
)
}
}
export default Results
To import it in App.js
...
// src/components/App/App.js
import Results from '../Results/Results.js'
To setup a Link
and matching Route
...
// src/components/App/App.js
render() {
return(
<Router>
<div>
<nav>
<h1>React Translator</h1>
<Link to="/search">Search</Link>
<Link to="/results">Results</Link>
</nav>
<main>
<Route
path="/search"
render={() => {
return (
<Search
translation={ this.state.translation }
setTranslation={ (data) => this.setTranslation(data) }
/>
)
}}
/>
<Route
path="/results"
render={() => {
return (
<Results
translation={ this.state.translation }
/>
)
}}
/>
</main>
</div>
</Router>
)
}
Currently, we have to manually click on /results
after submitting a search to render the Results
component and see the translation. Let's use React Router to force a redirect to /results
once the search is finished. To do so, we need to use the history
API that is included as a dependency of React Router.
The history
object is provided automatically via props from Router
. To expose and use it, all we need to do is to modify the render
prop on any Route
whose rendered component will need access to it. In our case, this will be the Search
component:
// src/components/App/App.js
<Route
path="/search"
render={(props) => {
return (
<Search
{...props}
translation={ this.state.translation }
setTranslation={ (data) => this.setTranslation(data) }
/>
)
}}
/>
The
...
(spread operator) is allowing us to "destructure" the props object being passed byRouter
and apply each of its properties as props on theSearch
component. To see what exactly theprops
object is here, you can add aconsole.log
within the callback function ofRoute
's render prop.
Now that we have passed the history
object down to Search
via props, we can use it to programmatically set the url path from within Search
thereby causing Results
to be rendered:
// src/components/Search/Search.js
handleSearchSubmit(e) {
e.preventDefault()
axios.get('https://watson-api-explorer.mybluemix.net/language-translator/api/v2/translate', {
params: {
source: this.state.sourceLang,
target: this.state.targetLang,
text: this.state.searchPhrase
}
})
.then((response) => {
this.props.setTranslation(response.data.translations[0].translation)
this.props.history.push('/results')
})
.catch((err) => {
console.log(err)
})
}
Note that we are calling
this.prop.history.push()
within.then()
so that we don't redirect before the response has come back from the Watson API.
15 min exercise / 5 min review
Using the instructions above as a guide, expose and pass the history
object to Search
and set-up your own app to redirect to Results
when a user submits a search.
Currently, if the user does a hard refresh while at /results
, the component will simply render a blank translation. How could we use the history
object to tell it to redirect to /search
(similar to how we did above) if the translation
prop is null
?
Hint: Look into React's component lifecycle hook componentDidMount
Solution
First, we need to pass history
into Results
just like we did with Search
:
// src/components/App/App.js
<Route
path="/results"
render={(props) => {
return (
<Results
{...props}
translation={ this.state.translation }
/>
)
}}
/>
Then, within Results
, we can use componentDidMount
to check the translation prop once the component has initialized. If it is null
, we can use history
to redirect to /search
:
// src/components/Results/Results.js
componentDidMount () {
if (!this.props.translation) {
this.props.history.push('/search')
}
}
Another edge case we want to control for is when the user submits a request at a url that we have not set up a Route
for. To handle this, React Router provides a Redirect
component that when returned, tells the browser to change the url to that of one of our recognized routes:
To import the Redirect
component, update your import
statement in App.js
as such:
// src/components/App/App.js
import {
BrowserRouter as Router,
Route,
Link,
Redirect
} from 'react-router-dom'
Then set up a catch-all Route
in App
's render method that will render it. Make sure to add it below the other Route
definitions:
// src/components/App/App.js
<Route
path="/*"
render={() => {
return (
<Redirect to="/search" />
)
}}
/>
Once we refresh the page, we will see an error in the console saying that we are trying to redirect to the same route that we are currently on. This is because, by default, React Router is checking ALL of our routes for matches with the url and then rendering any matching Route
components independently. To force React Router to treat our routes as unique and only render the first matching route, we need to use the Switch
component:
First, import it from react-router-dom
:
// src/components/App/App.js
import {
BrowserRouter as Router,
Route,
Link,
Redirect,
Switch
} from 'react-router-dom'
Then, wrap ALL of our Route
components within a Switch
component:
// src/components/App/App.js
<Switch>
<Route
path="/search"
render={(props) => {
return (
<Search
{...props}
setTranslation={ (data) => this.setTranslation(data) }
/>
)
}}
/>
<Route
path="/results"
render={(props) => {
return (
<Results
{...props}
translation={ this.state.translation }
/>
)
}}
/>
<Route
path="/*"
render={() => {
return (
<Redirect to="/search" />
)
}}
/>
</Switch>
Now, when we submit a random url, React Router successfully redirects us, but not when we submit a recognized url.
Wouldn't it be cool if, in addition to showing the text translation, our app could also provide an audio translation with the words being pronounced? Fortunately, the Watson API also provides a Text-to-Speech service for this purpose. By combining this with the react lifecycle method we saw earlier, we can have the Results
component automatically fetch this audio for us and then render it.
In order to use get audio from the /v1/synthesize
endpoint of the Text-to-Speech service, we must provide two pieces of information in the request url:
- a
voice
param whose value corresponds to a chosen voice from the API's list of voices - a
text
param whose value corresponds to the translation we want pronounced
To provide a selected voice in the request, we will first need to retrieve the list of possible voices from the API's /v1/voices
endpoint. First, let's import axios
in Results
and then let's send the request from Results
's componentDidMount
method:
// src/components/Results/Result.js
import axios from 'axios'
// src/components/Results/Results.js
componentDidMount () {
if (!this.props.translation) {
this.props.history.push('/search')
}
else {
axios.get('https://watson-api-explorer.mybluemix.net/text-to-speech/api/v1/voices')
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
})
}
}
It is typically best practice to send any initial API requests for data using
componentDidMount
If we inspect the API response in the console, we can see that the voices
array is nested inside of data
object and that each voice's name
is prepended with the two letter abbreviation of the language it represents. In order to be able to select the voice that matches our translation, we will need Results
to know what language the text was translated to. To achieve this, we will need Search
to set a second property on App
's state reflecting the language that the translation is in. Let's think about how we might modify App
's setTranslation
method to do this...
Solution
One of the easiest ways to do this would be to simply add a second argument to setTranslation
representing the language of the translation:
// src/components/App/App.js
setTranslation (data, language) {
this.setState({
translation: data,
language: language
})
}
We also will need to add this second argument to the callback we pass to Search
:
// src/components/App/App.js
<Route
path="/search"
render={(props) => {
return (
<Search
{...props}
setTranslation={ (data, language ) => this.setTranslation(data, language ) }
/>
)
}}
/>
Then we can update Search
's handleSearchSubmit
method to provide this second argument to this.props.setTranslation
:
// Search/Search.js
handleSearchSubmit(e) {
e.preventDefault()
axios.get('https://watson-api-explorer.mybluemix.net/language-translator/api/v2/translate', {
params: {
source: this.state.sourceLang,
target: this.state.targetLang,
text: this.state.searchPhrase
}
})
.then((response) => {
console.log(response)
this.props.setTranslation(response.data.translations[0].translation, this.state.targetLang)
this.props.history.push('/results')
})
.catch((err) => {
console.log(err)
})
}
Now that the we have a language
property on the App
's state, let's pass it via props into Results
so we can use it to filter the array of voices we are retrieving:
// src/components/App/App.js
<Route
path="/results"
render={(props) => {
return (
<Results
{...props}
translation={ this.state.translation }
language={ this.state.language }
/>
)
}}
/>
Then let's update Results
's componentDidMount
method to use this information to find the voice that relates to the language we chose:
// src/components/results/Results.js
constructor () {
super()
this.state = {
voiceAudioSource: null
}
}
componentDidMount () {
if (!this.props.translation) {
this.props.history.push('/search')
}
else {
axios.get('https://watson-api-explorer.mybluemix.net/text-to-speech/api/v1/voices')
.then((res) => {
let selectedVoice = res.data.voices.find((voice) => voice.name.includes(this.props.language))
this.setState({
voiceAudioSource: `https://watson-api-explorer.mybluemix.net/text-to-speech/api/v1/synthesize?text=${this.props.translation}&voice=${selectedVoice.name}`
})
})
.catch((err) => {
console.log(err)
})
}
}
Note that we must add a
constructor
method in order to usestate
onResults
Finally, let's update Results
's render
method to create an audio
element with a src
equal to the voiceAudioSource
set on state
:
// src/components/Results/Results.js
render () {
let audio =
this.state.voiceAudioSource?
<audio controls>
<source type="audio/ogg" src={this.state.voiceAudioSource}/>
</audio> :
null
return (
<div>
<h3>Translation</h3>
<p>{ this.props.translation }</p>
{ audio }
</div>
)
}
Now when we submit a search, it automatically loads in the audio pronunciation along with the translation!