- Import and use third-party node modules into React using npm (Node Package Manager)
- Use
BrowserRouter,Link,Route,Redirect, andSwitchto allow for navigation and URL manipulation - Define the React component lifecycle and use component methods to time API calls
- Use
axiosto 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 git@github.com:ga-wdi-exercises/react-translator.git
cd react-translator
npm install
atom .
npm run startTake 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
BrowserRouterasRouterhere 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 enclosingdivtag aroundnavandmain)
- 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 atoproperty, 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
pathin the URL with the relevant component torenderat that location (similar to Angular ui-router'sui-viewor 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 mustreturna 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
SearchfromApp. 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 aLinkandRoutefor it inApp's render method. - Have the
Resultscomponent display thetranslationproperty inApp's state by passing it toResultsvia props. - Finally, remove the
translationdata fromSearchrender 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 ResultsTo 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 byRouterand apply each of its properties as props on theSearchcomponent. To see what exactly thepropsobject is here, you can add aconsole.logwithin 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
voiceparam whose value corresponds to a chosen voice from the API's list of voices - a
textparam 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
constructormethod in order to usestateonResults
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!
