Today, we are going to build a Wikipedia viewer using React and Webpack in tandem. If you are not familiar with React at all, I recommend that you go through the introductory material first, it’ll save you a headache!
Additionally, we are going to be using ES6 class syntax for creating components, as this is the way React is heading towards. The previous tutorial in this series goes into more detail, so go ahead and take a look if you’d like. Let’s get our hands dirty then…
Environment set-up
Now that we are ready to go, let’s create a new folder for our project and run the following command as usual:
npm init
Let’s also go ahead and install a few development dependencies that we know we’ll use for sure:
npm install --save-dev webpack babel babel-loader babel-preset-es2015 babel-preset-react
Same applies to regular dependencies:
npm install --save react react-dom superagent superagent-jsonp
Now, let’s set a folder structure, here’s what I’ll be doing:
/wikipedia-viewer package.json webpack.config.js /dist index.html /src /components /css entry.js
This is what index.html looks like:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Wikipeda Viewer</title> </head> <body>
http://bundle.js </body> </html>
Now, let’s set up webpack as we did with the weather app:
module.exports = { entry: [ "./src/entry.js" ], output: { path: __dirname + '/dist', filename: 'bundle.js' }, resolve: { modulesDirectories: ['node_modules', 'src'], extensions: ['', '.js', '.jsx'] }, module: { loaders: [ { test: /.jsx?$/, loader: ["babel"], exclude: /node_modules/, query: { presets: ['react', 'es2015'] } } ] } };
This time, we are also going to load scss files through webpack; I’m going to add a new loader to webpack.config.js:
module.exports = { entry: [ "./src/entry.js" ], output: { path: __dirname + '/dist', filename: 'bundle.js' }, resolve: { modulesDirectories: ['node_modules', 'src'], extensions: ['', '.js', '.jsx', '.scss'] }, module: { loaders: [ { test: /.jsx?$/, loader: ["babel"], exclude: /node_modules/, query: { presets: ['react', 'es2015'] } }, { test: /.scss$/, loader: 'style!css!sass' } ] } };
We’re telling webpack to resolve the .scss extension, and to pass these files through 3 different loaders: sass, css and style. These are executed in right to left order (they are concatenated using the exclamation symbol). Let’s download them so we can use them:
npm install --save-dev style-loader css-loader sass-loader node-sass
I’m also bringing node-sass in, since it’s needed for SCSS compilation. We are also going to open up package.json and create a build script this time, here’s what it looks like:
{ "name": "wikipedia-viewer-react", "version": "1.0.0", "description": "A simple wikipedia viewer using Webpack and React", "main": "src/entry.js", "scripts": { "build": "webpack", "test": "echo "Error: no test specified" && exit 1" }, "keywords": [ "wikipedia", "viewer", "react", "webpack" ], "author": "Gorka Hernandez <info@gorkahernandez.com>", "license": "MIT", "devDependencies": { "babel": "^6.5.2", "babel-loader": "^6.2.4", "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", "css-loader": "^0.23.1", "node-sass": "^3.4.2", "sass-loader": "^3.1.2", "style-loader": "^0.13.0", "webpack": "^1.12.14" } }
From now on, running the following command will build our bundle: npm run build
Building the Wikipedia Viewer component
First of all, I’m going to set up the entry.js file, and import the main app component (it doesn’t exist yet!) into it. Then, I’ll use the render method exported by react-dom to render this component into our html.
import React from 'react'; import { render } from 'react-dom'; import WikipediaViewer from './components/WikipediaViewer'; render(<WikipediaViewer />, document.getElementById('wikipedia-viewer'));
Now, let’s also create and bring in the stylesheet for the project. I’m going to create a new file (main.scss) and save it to src/css. Then, import it in entry.js:
import React from 'react'; import { render } from 'react-dom'; import WikipediaViewer from './components/WikipediaViewer'; import './css/main'; render(<WikipediaViewer />, document.getElementById('wikipedia-viewer'));
We do not need to specify the .scss extension since we already told webpack to consider it. Let’s go ahead and create the main app component now:
import React from 'react'; export default class WikipediaViewer extends React.Component { render() { return(
-
- Result 1
- Result 2
- Result 3
- …
); } }
Run npm run build and open up index.html to see the results. I know, it looks absolutely disgusting and does nothing at all, let’s keep going.
We’re going to create a new component that will hold the search form and call it, well, SearchForm. Let’s create it first and then refactor the main WikipediaViewer component.
import React from 'react'; export default class SearchForm extends React.Component { render() { return (
); } }
We are also going to place a link underneath that will open up a random Wikipedia article. This is an easy one, navigating to this url:
http://en.wikipedia.org/wiki/Special:Random
Will always take us to a random article. Let’s add it to the SearchForm component:
import React from 'react'; export default class SearchForm extends React.Component { render() { return (
or visit a random article.
); } }
We need this component to be able to tell the WikipediaViewer what we are looking for. We are going to do this by passing a callback function as a prop, from WeikipediaViewer to SearchForm. When a search happens, we will execute this callback. We also need to keep track of what the search term is.
For this purpose, we first need to initialize the state, and then update it every time the content of the input element changes:
import React from 'react'; export default class SearchForm extends React.Component { constructor() { super(); this.state = { searchTerm: '' }; } handleInputChange(event) { this.setState({ searchTerm: event.target.value }); } render() { return (
or visit a random article.
); } }
We are now calling handleInputChange (remember to bind it to the class instance) every time that the input changes. Now, we just need to execute whatever function that get’s passed to us from WikipediaViewer when we submit the form. We are going to create a new method and call it handleSubmit, this method will call on the method that WikipediaViewer will pass down, and will be called when the search form is submitted:
import React from 'react'; export default class SearchForm extends React.Component { constructor() { super(); this.state = { searchTerm: '' }; } handleInputChange(event) { this.setState({ searchTerm: event.target.value }); } handleSubmit(event) { event.preventDefault(); // Call whatever we pass down from WikipediaViewer this.setState({ searchTerm: '' }); // Reset the search term (this is completely optional, you may not want to do it) } render() { return (
or visit a random article.
); } }
Now, let’s change WikipediaViewer a bit before going any further:
import React from 'react'; import SearchForm from './SearchForm'; export default class WikipediaViewer extends React.Component { handleSearch(searchTerm) { // Use the Wikipedia API here! } render() { return(
-
- Result 1
- Result 2
- Result 3
- …
); } }
We’re now passing down handleSearch to the SearchForm as a prop. We can access this callback function in the search component like so:
import React from 'react'; export default class SearchForm extends React.Component { constructor() { super(); this.state = { searchTerm: '' }; } handleInputChange(event) { this.setState({ searchTerm: event.target.value }); } handleSubmit(event) { event.preventDefault(); let searchTerm = this.state.searchTerm.trim(); // Remove whitespace at the beginning and end. if (!searchTerm) { // If no search term was typed, return early and do nothing. return; } this.props.onSearch(searchTerm); // Execute callback this.setState({ searchTerm: '' }); } render() { return (
or visit a random article.
); } }
And that’s it for SearchForm! We now need to actually fetch the results from Wikipedia, we’ll execute the API call using superagent (and it’s jsonp plugin). Back to WikipediaViewer, we first need to initialize the state, and then fill in the handleSearch method.
import React from 'react'; import SearchForm from './SearchForm'; import superagent from 'superagent'; import jsonp from 'superagent-jsonp'; export default class WikipediaViewer extends React.Component { constructor() { super(); this.state = { results: [] }; // Initialize state } handleSearch(searchTerm) { superagent.get('https://en.wikipedia.org/w/api.php') // Wikipedia API call .query({ search: searchTerm, // The search keyword passed by SearchForm action: 'opensearch', // You can use any kind of search here, they are all documented in the Wikipedia API docs format: 'json' // We want JSON data back }) .use(jsonp) // Use the jsonp plugin .end((error, response) => { if (error) { console.error(error); } else { this.setState({ results: response.body }); // Set the state once results are back } }); } render() { return(
-
- Result 1
- Result 2
- Result 3
- …
); } }
We are now fetching results back from Wikipedia, but doing nothing with them (aside from updating the state). To display the results, we are going to create two new components: ResultList and SingleResult. Names are quite self explanatory, ResultList will take in the whole results array and map them to SingleResults. It’s important that we know the shape of the json data that we’re dealing with. I choose to use Opensearch, this is what a sample search will bring us back:
[ "hello world", [ "Hello world", "Hello World (song)", "Hello World: The Motown Solo Collection", "Hello World (Scandal album)", "Hello World (Information Society album)", "Hello World! (composition)", "Hello World (disambiguation)", "Hello World Jamaica", "Hello World Day" ], [ "This is a redirect from a word (or phrase) to a page title that is related in some way. This redirect might be a good candidate for a Wiktionary link.", ""Hello World" is a song written by Tom Douglas, Tony Lane and David Lee, and recorded by American country music group Lady Antebellum.", "Hello World: The Motown Solo Collection is a 71-track triple disc box set commemorating Michael Jackson's early years with Motown Records.", "Hello World is the sixth studio album by Japanese pop rock band, Scandal. The album was released on December 3, 2014 in Japan by Epic and being distributed in Europe through JPU Records.", "Hello World (stylized _hello world) is an album by American synthpop/freestyle band Information Society.", "Hello World! is a piece of contemporary classical music for clarinet-violin-piano trio composed by Iamus Computer in September 2011. It is arguably the first full-scale work entirely composed by a computer without any human intervention and automatically written in a fully-fledged score using conventional musical notation.", "Hello World may refer to:", "Hello World Jamaica was one of the first Caribbean children's programs to represent the Rastafarian community.", "" ], [ "https://en.wikipedia.org/wiki/Hello_world", "https://en.wikipedia.org/wiki/Hello_World_(song)", "https://en.wikipedia.org/wiki/Hello_World:_The_Motown_Solo_Collection", "https://en.wikipedia.org/wiki/Hello_World_(Scandal_album)", "https://en.wikipedia.org/wiki/Hello_World_(Information_Society_album)", "https://en.wikipedia.org/wiki/Hello_World!_(composition)", "https://en.wikipedia.org/wiki/Hello_World_(disambiguation)", "https://en.wikipedia.org/wiki/Hello_World_Jamaica", "https://en.wikipedia.org/wiki/Hello_World_Day" ] ]
As you can see, we get an array of length 4. This is what each one of these items is:
- Index 0: Original search term/keyword
- Index 1: Array of search result titles.
- Index 2: Array of search result captions/descriptions.
- Index 3: Array of search result URLs.
This is all that we need to properly display these results, but they’ll surely need some formatting, ResultList will take care of this:
import React from 'react'; export default class ResultList extends React.Component { render() { var results = this.props.results[1].map((result, index) => { return (
); }); return (
); } }
We’re merely going through the results that WikipediaViewer will pass down and returning a div with relevant data for each of them. This is how we implement and pass props to ResultList. We are also going to change the state initialization on WikipediaViewer, to make sure that the structure is the same:
import React from 'react'; import SearchForm from './SearchForm'; import ResultList from './ResultList'; import superagent from 'superagent'; import jsonp from 'superagent-jsonp'; export default class WikipediaViewer extends React.Component { constructor() { super(); this.state = { results: [ '', [], [], [] ] }; } handleSearch(searchTerm) { superagent.get('https://en.wikipedia.org/w/api.php') .query({ search: searchTerm, action: 'opensearch', format: 'json' }) .use(jsonp) .end((error, response) => { if (error) { console.error(error); } else { this.setState({ results: response.body }); } }); } render() { return(
); } }
If you run npm run build and search for something you will probably get an error in the JavaScript console. This happens because we are iterating over an array and rendering multiple elements to the page without giving them a unique key. This can be easily fixed like so:
import React from 'react'; export default class ResultList extends React.Component { render() { var results = this.props.results[1].map((result, index) => { return (
); }); return (
); } }
The app works, but we are going to abstract each search result into their own component and call it SingleResult. Let’s update ResultList first:
import React from 'react'; import SingleResult from './SingleResult'; export default class ResultList extends React.Component { render() { var results = this.props.results[1].map((result, index) => { return ( <SingleResult key={index} title={this.props.results[1][index]} description={this.props.results[2][index]} url={this.props.results[3][index]}/> ); }); return (
); } }
Now, we need to create this new component:
import React from 'react'; export default class SingleResult extends React.Component { render() { return ( <a href={this.props.url} className="single-result" target="_blank">
{this.props.title}
{this.props.description}
</a> ) } }
Here, we’re creating a slightly different structure, each result lives within a div (wrapped by the <a> tag) and displays both title and description. Try running npm run build to see what it looks like. Answer: UGLY.
We’ll do some magic by setting some styles in main.scss:
@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300,700); * { margin: 0; padding: 0; box-sizing: border-box; } body { background-color: #2e2e2e; font-family: 'Open Sans', Helvetica, Arial, sans-serif; } a { text-decoration: none; color: #afafaf; &:hover { color: #d6d6d6; } } ::-webkit-input-placeholder { color: #717171; } :-moz-placeholder { color: #717171; opacity: 1; } ::-moz-placeholder { color: #717171; opacity: 1; } :-ms-input-placeholder { color: #717171; } :placeholder-shown { color: #717171; } .wrapper { margin-top: 100px; text-align: center; } .search-box-container { margin-bottom: 10px; } .search-box-text { background-color: transparent; padding: 10px; width: 90%; max-width: 400px; border: 1px solid #dedede; border-radius: 40px; color: #dedede; text-align: center; font-size: 1.3rem; font-weight: 300; &:focus { border-color: #ff6200; outline: none; } } .search-box-submit { border: 1px solid #dedede; background-color: #dedede; cursor: pointer; padding: 10px; border-top-right-radius: 20px; border-bottom-right-radius: 20px; &:active { background-color: #a3a3a3; } &:focus, &active { outline: none; } } .random-text { color: #868686; } .result-list { width: 100%; } .single-result { color: #333; text-decoration: none; width: 100%; max-width: 500px; margin: 0 auto; display: block; background-color: #f4f4f4; padding: 20px; margin-bottom: 5px; text-align: left; border-left: 3px solid #717171; border-right: 3px solid #717171; transition: .1s ease-out all; h3 { font-size: 1rem; margin-bottom: 0.3rem; } p { font-size: 0.8rem; } &:hover { color: #333; border-color: #ff6200; } }
All set and ready to go! Your app should be working with a npm run build. Remember, that you can ask any questions via email, twitter or posting a comment below!