Skip to content

catalyst/react-intro

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 

Repository files navigation

React Introduction

Why React?

  • It's incredibly popular
  • There is a good community
  • You can transition to mobile apps with a only a small learning curve
  • The performance is pretty good
  • Development backed by a company with deep pockets
  • It's incredibly popular

Differences from MV* frameworks

By itself, React is not a MV* framework - it's not even a framework at all. It's a views library (just the V in MVC).

We bolt other libraries on the side to provide deeper functionality - of course you could write your own, but there are lots of good established patterns already, don't invent your own.

React is strongly based on components - modular units of functionality that you can bolt together and pass application state between.

A common stack is:

  • ES6
  • React
  • React Router
  • Redux
  • Thunk
  • Jest

Today we'll be covering the first three of those, by building a simple app which displays a searchable list of MP's in New Zealand, and their email addresses.

Getting started

If you have your own github account already, you might prefer to fork this repository and clone that instead. If you don't just run the following commands on a terminal or command line interface (assuming that your machine already has git available):

https://github.com/catalyst/react-intro.git
cd react-intro

Install node, npm and project dependencies

First, we need to install node.js and its package manager, npm.

Ubuntu/Debian/Mint instructions

Mac instructions

Windows instructions

Next, from within the react-intro/tutorial folder, run:

npm install

Code editor

If you have a favourite code editor feel free to use that, but I recommend Atom plus the language-javascript-jsx package.

To install Atom: sudo apt-get install atom

In Atom, right click in the left panel, select Add Project Folder and open the react-intro/tutorial folder. You should see a number of already existing files.

Project starter files

To get to the starter set of files, I ran (so you don't need to):

npm init
npm install --save-dev react react-dom

I also set up a Webpack based development build process.

If you want to refer to a finished, working version of today's project, have a look at the example folder.

In the tutorial folder you should have the following files already present:

  • src/components/homepage/homepage.jsx our basic React component
  • src/data.js raw data we'll use in our web app
  • src/index.html a template for the HTML for our SPA (single-page-app)
  • src/index.js our root JS file
  • src/styles.css our (very simple) CSS styles
  • .babelrc this is configuration for the transpilation from ES6 to ES5
  • .eslintrc.json this is configuration for the JS linter
  • .gitignore this keeps our node_nodules and dist folders out of version control
  • README.md this file
  • webpack.config.js configuration for the build process - note that this is a simple version that doesn't tackle production appropriate builds

We keep all of our project code in the src folder.

We could have used the create-react-app utility to get our project started, but it hides a bit of stuff under the hood that I want to show you explicitly.

Development builds

In a terminal in the react-intro folder, run:

npm start

Right click on the link to open in a browser. This terminal process will watch for changes and automatically recompile and reload the pages in your browser. to quit from it use Control-C.

Your first component

Open src/components/homepage/homepage.jsx. We're going to modify this file to replace the [table goes here] with a new table component.

First, we need to get the data for the table. At the top of the file add a line:

import data from 'data';

We can check that's working as we expect by adding a console.log(data); statement temporarily.

We also want to import the new component that we're adding, for the table.

It's a good idea to only have one component per file, so we should create a new file.

At the top of the file, add:

import Table from 'components/table/table';

In place of the [table goes here] we'll add in the reference to our new component:

<Table mpData={data} />

You can see that we're passing down the data we loaded into our new component as a property.

If you look at the terminal, you'll see that the app tried to recompile and is now throwing an error, because the file we're trying to import doesn't exist yet.

Create a new file, src/components/table/table.jsx. In the new file, we're going to add the basic HTML (JSX!) structure for the new component:

import React, { Component } from 'react';

class Table extends Component {
  render () {
    return (
      <table className="table">
        <thead>
          <tr>
            <th>Name</th>
            <th>Party</th>
            <th>Electorate</th>
            <th>Email</th>
          </tr>
        </thead>
        <tbody>
        </tbody>
      </table>
    );
  }
}

export default Table;

JSX

So what is JSX?

It's a lot closer to XML than regular HTML (the parser is very strict about closing things properly). There are also a few subtle changes to attribute names compared to regular HTML: className instead of class, for example.

You can use a .jsx file extension to indicate that a file contains JSX, but it's optional, you can just use a regular .js extension.

JSX lets you encapsulate the whole of your component, including the markup, into one file. This only works well if you do a good job of keeping your components small and modular.

React dev tools

In your browser, open developer tools (usually F12) and go to the console tab. Any errors rendering the application will show up here. You'll also note there is a link to install the React Dev Tools. Install them and you'll get another tab that lets you poke around in the JSX structure more easily.

PropTypes

React comes with a rudimentary type checking system that is good to use, called PropTypes.

PropTypes are added to every component and they validate that props that are being passed in conform to a particular type (like, a string, or an array). Let's add the PropTypes to our Table component now (you can see that the linter is complaining at us for not having it).

First, in a terminal in the tutorial directory, run:

npm install --save-dev prop-types

Change:

import React, { Component } from 'react';

to:

import React, { Component, } from 'react'; 
import PropTypes from 'prop-types';

and underneath the Table class (before the export default Table; line), add:

Table.propTypes = {
  mpData: PropTypes.array.isRequired
};

Logic in your render method

So far we have a table header... and no data. Let's fix that. We need to add each MP as a new row of the table.

Rendering variables into JSX is really easy, let's temporarily try it now:

render () {
  
  let test = 'MP';
  
  return (
    <table className="table">
      <thead>
        <tr>
          <th>{test} Name</th>
          <th>Party</th>

You can get rid of that, but it shows how you can directly use variables and other JS in your JSX.

We don't want to have to manually output every row of data though, we want to use a loop:

render () {
  let rows = [];
  
  this.props.mpData.forEach(mp => {
    rows.push(
      <tr>
        <td>{mp.name}</td>
        <td></td>
        <td></td>
        <td></td>
      </tr>
    );
  });
  
  return (
<tbody>
  {rows}
</tbody>

Props

We used a special variable reference to get the data that was passed in from the Homepage component: this.props. All the data passed in (the props) are immutable: you cannot change their value within the component. By passing data in as a prop, you are indicating that the parent component owns it rather than this component. There is a different type of special value that is mutable and specific to the current component, state, which we'll look at more soon.

Arrays of elements need keys

You can see in the web dev tools console that React is complaining that arrays of elements need unique keys. We can fix that by using an index parameter on our forEach loop:

this.props.mpData.forEach((mp, index) => {
  rows.push(
    <tr key={index}>
      <td>{mp.name}</td>

Reformatting the names

We can use normal JS to perform transformations on the data.

this.props.mpData.forEach((mp, index) => {
  let name = mp.name.split(',');
  name = name.reverse().join(' ');
  
  rows.push(  
    <tr key={index}>
      <td>{name}</td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  );
});

Adding the rest of the columns

this.props.mpData.forEach(mp => {
  let name = mp.name.split(',');
  name = name.reverse().join(' ');
  
  rows.push(  
    <tr key={mp.email}>
      <td>{name}</td>
      <td>{mp.party}</td>
      <td>{mp.electorate}</td>
      <td><a href={'mailto:' + mp.email}>{mp.email}</a></td>
    </tr>
  );
});

Calling methods on the class

Our render method is getting a bit long and unruly: let's refactor it a bit. We're going to change the current code:

class Table extends Component {
  render () {
    let rows = [];
    
    this.props.mpData.forEach((mp, index) => {
      let name = mp.name.split(',');
      name = name.reverse().join(' ');
      
      rows.push(  
        <tr key={index}>
          <td>{name}</td>
          <td>{mp.party}</td>
          <td>{mp.electorate}</td>
          <td><a href={'mailto:' + mp.email}>{mp.email}</a></td>
        </tr>
      );
    });
    
    return (

to:

class Table extends Component {
  renderRows () {
    let rows = [];
    
    this.props.mpData.forEach((mp, index) => {
      let name = mp.name.split(',');
      name = name.reverse().join(' ');
      
      rows.push(  
        <tr key={index}>
          <td>{name}</td>
          <td>{mp.party}</td>
          <td>{mp.electorate}</td>
          <td><a href={'mailto:' + mp.email}>{mp.email}</a></td>
        </tr>
      );
    });
    
    return rows;
  }
  
  render () {
    const rows = this.renderRows();
    
    return (

Searching the table

Add a new file src/components/search/search.jsx.

import React, { Component } from 'react';

class Search extends Component {
  render () {  
    return (
      <div className="mb-3">
        <label>
          Search: 
          <input
            className="ml-2"
            type="search"
            autoComplete="off" 
          />
        </label>
      </div>
    );
  }
}

export default Search;

and in src/components/homepage/homepage.jsx, import the new component at the top of the file, and replace the {/* some controls here later? */} line:

import Search from 'components/search/search';

...

<Search />

Controlled form elements

We want to be able to take the value of the form field and use it elsewhere in our application. Because we want React to manage that form value for us, we must set it up as a controlled component. In a controlled component, the value attribute is assigned to a variable in the components state.

State

Unlike props, state is mutable within the component. But you should never directly set the value of this.state other than in the class constructor. Instead, use the setState function. You can also specify a callback that will execute once the state change has re-rendered.

Implementing our controlled search box

First, we set up the initial value of our field in the class constructor. We then assign this state variable to the value of the field:

import React, { Component } from 'react';

class Search extends Component {
  constructor (props) {
    super(props);
    
    this.state = {
      searchFieldValue: ''
    };
  }
  
  render () {  
    return (
      <div className="mb-3">
        <label>
          Search:
          <input
            className="ml-2"
            type="search"
            autoComplete="off"
            value={this.state.searchFieldValue}
          />
        </label>

The constructor (props) { super(props) bit is boilerplate for adding a constructor to a class.

Now we need to set up a trigger so as the user interacts with the field their input values are reflected in the state:

class Search extends Component {
  constructor (props) {
    super(props);
    
    this.state = {
      searchFieldValue: ''
    };

    this.searchFieldChange = this.searchFieldChange.bind(this);
  }
  
  searchFieldChange (event) {
    this.setState({
      searchFieldValue: event.target.value
    });
  }
  
  render () {  
    return (
      <div className="mb-3">
        <label>
          Search:
          <input
            className="ml-2"
            type="search"
            autoComplete="off"
            onChange={this.searchFieldChange}
            value={this.state.searchFieldValue}
          />
        </label>

When a change event is called, the method searchFieldChange is executed, which then sets the value of searchFieldValue in the component state.

There was one other non-obvious thing we needed to do here in order to make this code work: using ES6 classes in React, the reference to this gets broken when called from and event in the JSX, so we need to add the line this.searchFieldChange = this.searchFieldChange.bind(this); in the constructor to preserve that reference. There are a couple of alternate ways of doing this but this is the most broadly used method.

Communication between components

We need to get the search value back up into the Homepage component, and from there down to the Table component.

The action to grab that data from the Search component need to come from the top down - the Homepage component needs to reach in and pull the data out (data transmission in React is top to bottom - child components can't directly affect their parents). We can do this by setting up a method in the Homepage component (plus some more constructor boilerplate):

class Homepage extends Component {
  constructor (props) {
    super(props);
    
    this.state = {
      searchFieldValue: ''
    };
    
    this.searchValueEntered = this.searchValueEntered.bind(this);
  }
  
  searchValueEntered (value) {
    this.setState({
      searchFieldValue: value
    });
  }
<Search userInput={this.userInput} />

Now we've passed this down to to Search we can pick it up there and use it (not forgetting to set up a PropType for userInput):

import React, { Component, } from 'react'; 
import PropTypes from 'prop-types';

...
  
  searchFieldChange (event) {
    this.setState({
      searchFieldValue: event.target.value
    });
    this.props.userInput(event.target.value);
  }

...

Search.propTypes = {
  userInput: PropTypes.func.isRequired
};

export default Search;

And finally back in Homepage we now want to pass that value down to the Table component...

<Table mpData={data} searchFieldValue={this.state.searchFieldValue} />

... and set up a corresponding PropType in Table, too:

Table.propTypes = {
  mpData: PropTypes.array.isRequired,
  searchFieldValue: PropTypes.string
};

There are much more elegant ways of managing this transmission of data, such as Redux which is covered in the intermediate course.

Using the searchFieldValue to filter the data

Unlike a lot of frameworks, React doesn't give you off the shelf filtering. It expects you to use another tool, or write your own filter code in JS.

We'll transform each row into a string and use indexOf() to see if the search value is contained in it, and then only push our row of data if 1) there was no search string supplied or 2) the row does contain the search value.

this.props.mpData.forEach((mp, index) => {
  const fulltext = Object.values(mp).join().toLowerCase();
  const searchValue = this.props.searchFieldValue.toLowerCase();

  if (!searchValue || fulltext.indexOf(searchValue) > -1) {
    let name = mp.name.split(',');
    name = name.reverse().join(' ');
    
    rows.push(
      <tr key={index}>
      <td>{name}</td>
      <td>{mp.party}</td>
      <td>{mp.electorate}</td>
      <td><a href={'mailto:' + mp.email}>{mp.email}</a></td>
      </tr>
    );
  }
});

Note that Object.values() is only supported by recent browsers

That should now filter the table!

It's nice to have a message if nothing has been found, let's add a check after our loop:

if (rows.length === 0) {
  rows = (<tr><td colSpan="4"><i>Nothing found</i></td></tr>);
}

Routes using React Router

We're building a Single Page App (SPA) (it's totally fine to use React for just individual bits on a page that is otherwise rendered by the way) but we don't yet have any other routes. We can add an About page using React Router.

First, as we're adding a project dependency we need to install it:

npm install --save-dev react-router-dom

First, let's make a component for our About page at src/components/about/about.jsx:

import React, { Component } from 'react';

class About extends Component {
  render () {  
    return (
      <div className="container-fluid">
        <div className="row mb-3">
          <div className="col">
            <h1>About</h1>
          </div>
        </div>
        <div className="row">
          <div className="col">
            <p>Get the most up to date details at <a href="https://www.parliament.nz/en/get-involved/have-your-say/contact-an-mp/">https://www.parliament.nz/en/get-involved/have-your-say/contact-an-mp/</a></p>
          </div>
        </div>
      </div>
    );
  }
}

export default About;

Now we'll add the navigation and routes. It's common to put the routes at the top level of the app, so we'll going to be editing src/index.jsx:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';

import 'styles';
import Homepage from 'components/homepage/homepage';
import About from 'components/about/about';

ReactDOM.render(
  <Router>
    <div>
      <Route exact path="/" component={Homepage}/>
      <Route path="/about" component={About}/>
    </div>
  </Router>,
  document.getElementById('app')
);

You can now directly access /about through the URL bar, be we want some user interface. First, add NavLink to the module imports:

import { BrowserRouter as Router, Route, NavLink } from 'react-router-dom';

Then add the NavLinks above the routes:

  <Router>
    <div>
      <ul className="nav nav-pills mb-3">
        <li className="nav-item">
          <NavLink to="/" className="nav-link" activeClassName="active" exact>Home</NavLink>
        </li>
        <li className="nav-item">
          <NavLink to="/about" className="nav-link" activeClassName="active">About</NavLink>
        </li>
      </ul>

      <Route exact path="/" component={Homepage}/>
      <Route path="/about" component={About}/>
    </div>
  </Router>,

Component lifecycles

There are many component lifecycle events that trigger on particular mount, update or unmount events. The most common ones are:

  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount()

React Hooks

React hooks are the latest substantial change in React. It allows us to avoid using classes altogether, and use functions to set and control state and its side effects. They're still relatively new, so most older projects will follow the format we did, and might have a spattering of hooks in newer components.

Further reading

You can define default props for a component using defaultProps.

React's own tutorial is worth doing.

Read about the different types of synthetic events React supports (we only used onChange, there are loads more).

A searchable list of react components, and a list of other lists.

About

Intro training course for React

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 97.0%
  • HTML 2.0%
  • CSS 1.0%