React II: the How - building IdeaBox

Learning Goals

  • Create class components
  • Create function components
  • Use state and props to manage application data
  • Create a controlled form
  • Use JSX
  • Understand conditional rendering

Create React App

For this lesson, we will be using create-react-app. This is a single command line that sets up a boilerplate React application for us. Out of the box, it comes with some handy scripts (such as start and test).

Many of the dependencies and configuration files will be obscured, since we won’t need to worry about them. This is a fast way to set up a React application so we can concentrate on building out the app, instead of spending our time and energy on tooling.

Note: A good intermission or post-graduation project is to learn how to configure React. Hand-rolling a React application will make you more familiar with how React works.

Get started with Create React App

To set up a new react application, simply open your terminal and run the command:

$ npx create-react-app NAMEOFYOURAPP

For this lesson, we’ll be rebuilding a Mod 1 project in React! Let’s run the following script:

$ npx create-react-app ideabox

You will see something like this start to run in your terminal:

Creating a new React app in /Users/leta/mod3/ideabox.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts...

⸨  ░░░░░░░░░░░░░░░░⸩ ⠸ fetchMetadata: sill resolveWithNewModule pify@4.0.1 chec...

When the script is finished running, you will see the following message in your terminal:

> node scripts/postinstall || echo "ignore"

+ react-dom@16.8.6
+ react@16.8.6
+ react-scripts@3.0.1
added 1541 packages from 747 contributors and audited 888986 packages in 52.528s
found 0 vulnerabilities


Initialized a git repository.

Success! Created ideabox at /Users/leta/Turing/TA/MOD3/m3curriculum/ideabox
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd ideabox
  npm start

Happy hacking!

As the instructions say, let’s change directories into our new ideabox application and run npm start to see our new boilerplate application!

Exploring the boilerplate

Take a few minutes and look around the boilerplate. Hint: start at the src/ folder!

What happens if you:

  • Change some of the HTML you see in the App.js file?
  • Change some of the CSS you see in the App.css file?

You’ll also notice an index.js file. What is going on in there?

ReactDOM.render(<App />, document.getElementById('root'));

This is the React magic happening. You can read more about what’s happening here, but in a nutshell: at our HTML element with an ID of “root”, we’re rendering a React element onto the browser DOM.

Okay. Enough with the nitty gritty. Let’s get to building our IdeaBox application!

Ideabox

Let’s get rid of all the boilerplate inside the App.js file. Yep. Just ERASE THAT CODE!

You’ll notice that as soon as we save our changes, the browser re-renders. Look into the terminal, and you’ll see another build kick off. This is because create-react-app gives us hot-reloading. Any time we make a change, the browser will update to show our changes.

If we look at the browser now that we’ve deleted our App.js code, you’ll see an error that reads:

“Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. You likely forgot to export your component from the file it’s defined in, or you might have mixed up default and named imports.”

Back in our index.js file we’re trying to render a React component: App. However, we’re no longer exporting a valid React component! The error is trying to be helpful to let us know what’s going on.

Let’s start building out our App component.

Planning our app

Let’s figure out what should be a component in our app.

IdeaBox wireframe

We have an App component. That should probably hold onto our list of ideas.

We need a Form component so we can create new ideas. The Form component will have to be able to add new ideas to App’s list of ideas.

We need a Card component to display our ideas.

And let’s create an Ideas component to hold all of our Cards.

IdeaBox wireframe with component boxes

App.js

Let’s bring back some code.

Because we want App to hold onto our list of ideas, let’s think about how we’re going to store that information.

In our React I lesson, we learned about state. In order to use component state, we need our component to be a class.

So: let’s import React and the parent Component class, and create our child App component! Think back to your OOP knowhow from Mod 2.

import React, { Component } from 'react';

class App extends Component {
  constructor() {
    super();
  }
}

export default App;

When we save that, our error now says “TypeError: instance.render is not a function”. This is because React expects our class-based component to have a method called “render”!

The class method render comes from the parent class, Component.

Just like any other parent class in OOP, when we inherit from it to create a child class, we have access to all of the parent’s methods and properties. This is why the first thing we have to put in our constructor is the familiar super function.

If you look under the hood, the React “Component” is just a class that contains various methods (render, setState, componentDidMount). We will learn more about these in this and the next lesson.

Let’s keep writing our App component!

import React, { Component } from 'react';

class App extends Component {
  constructor() {
    super();
  }

  render() {
    return (
      <h1>IdeaBox</h1>
    )
  }
}

export default App;

Save this code and go check your browser. What do you see?

Let’s take a minute and examine that return statement in our render method.

Understanding JSX

It looks like our render method is returning some HTML! Very easy to read, right?

What we’re actually writing here is known as JSX. It’s an abstraction that makes React easier to write!

JSX is “JavaScript and XML” - it’s a handy mashup language that allows us to write HTML with a bit of JavaScript injected in. You can read more on it here (and a bit more in depth here). But in the meantime, we’ll see how JSX makes our lives easier throughout this lesson!

Okay. Now try to add a paragraph tag. What happened?

If you saw this error, don’t panic!

Failed to compile.

./src/App.js
  Line 12:  Parsing error: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?

  10 |     return(
  11 |       <h1>IdeaBox</h1>
> 12 |       <p>Hi!</p>
     |       ^
  13 |     )
  14 |   }
  15 |

Let’s figure out what this error is saying. “Adjacent JSX elements must be wrapped in an enclosing tag.”

If we think about this logically, it makes sense! The method render is just a function - a regular old class method. And how many things can a function return at once? Just one! So: in order to return multiple JSX elements, we have to wrap them in a single JSX element!

Since this is our App component, let’s wrap everything in a <main> tag!

NOTE: If you’re just looking for an unflavored container for your elements (aka they are not semantically related to one another, and the best element to use would be a <div>), then instead, use a <React.Fragment>! You can read more about Fragments here.

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor() {
    super();
    this.state = {
      ideas: []
    }
  }

  render() {
    return(
      <main className='App'>
        <h1>IdeaBox</h1>
        <p>Hi!</p>
      </main>
    )
  }
}

export default App;

You’ll notice that instead of “class”, we’re using a “className” attribute on our <main> element. Why do you think this is?

The answer lies with JSX: because we’re writing JavaScript + XML, and not true HTML, we can’t use “class” because “class” is already a reserved JavaScript word! So, we’re using “className” instead.

App.js state

Okay. Let’s come back to our App constructor method and create state.

  constructor() {
    super();
    this.state = {
      ideas: []
    }
  }

Let’s review. What is the keyword this doing here? What is “state”?

Just like any other class, the keyword this refers to the instance of App that will be created when the constructor method is invoked! So: this.state is merely an instance variable for the App component. State is an object that will hold whatever information we want it to hold!

In our case, we want to create a list (aka an array) of ideas!

Let’s start out with a couple of default ideas, just so we can have something to look at when we begin building out the rest of our components.

  constructor() {
    super();
    this.state = {
      ideas: [
        { id: 1, title: 'Prank Travis', description: 'Stick googly eyes on all his stuff' },
        { id: 2, title: 'Make a secret password app', description: 'So you and your rideshare driver can both know neither one of you is lying' },
        { id: 3, title: 'Learn a martial art', description: 'To exact vengeance upon my enemies' },
      ]
    }
  }

When I look in my React dev tools, I can see that App now has state:

App component state

Neat!

Consider the following…

Okay. Let’s pause for a second. App.js is a class-based component, rather than a function-based component, because we want it to have its own state.

(React Hooks is a new feature that was released at the end of 2018 and allows function-based components to access and manipulate state - we’ll learn more about this in a future lesson, but for now, it’s important to understand the uses of and distinctions between class-based and function-based components.)

A basic rule of thumb for determining if a component should be function-based or class-based is this:

If a component needs to keep track of and display data, and if that component itself will update or change the data it’s displaying, we need to use a class-based component.

In general, function-based components are lighter than class-based components. You’ll find that most of your applications will be made up of simple function-based components, getting their data from a few heavier class-based components.

Ideas.js

Let’s skip the Form component for now. We’ll come back to it later! Let’s focus on getting our two ideas to render!

We already said that we want to have a container for all of our idea Cards. So let’s create that component! In your terminal, touch two new files: $ touch src/Ideas.js src/Ideas.css.

Okay. Hang on. Our App component was capitalized, and now our Ideas component is capitalized, too. Why do you think this is? What did you learn about the naming conventions for JS classes in Mod 2?

It’s convention to capitalize class names so that, at a glance, a developer knows they are dealing with a class. We capitalize component names for the exact same reason. If you named this component ideas instead of Ideas, it would still work. But for reasons of readability and developer empathy, we capitalize component names. Okay, back to your regularly scheduled programming!

Will this Ideas component need to have state? What do you think?

Since the Ideas component will just be rendering Card components, it will not need to have its own state, and can therefore be a function-based component! Exciting.

As always, we will need to import React, but this time, we will not need to import a Component. For now, let’s create a function called Ideas that returns an h2 that reads “Ideas go here!”

// Ideas.js

import React from 'react';
import './Ideas.css';

const Ideas = () => {
  return (
    <h2>Ideas go here!</h2>
  )
}

export default Ideas;

Then, back in our App.js, we can import our shiny new Ideas component and add it to our render!

// App.js

import React, { Component } from 'react';
import Ideas from './Ideas';
import './App.css';

class App extends Component {
  constructor() {
    super();
    this.state = {
      ideas: [
        { title: 'Prank Travis', description: 'Stick googly eyes on all his stuff' },
        { title: 'Make a secret password app', description: 'So you and your rideshare driver can both know neither one of you is lying' },
        { id: 3, title: 'Learn a martial art', description: 'To exact vengeance upon my enemies' },
      ]
    }
  }

  render() {
    return(
      <main className='App'>
        <h1>IdeaBox</h1>
        <Ideas />
      </main>
    )
  }
}

export default App;

When we look at our browser, we should see our h2! That’s nice and all, but not very useful.

We have to pass our ideas array from the App component to our Ideas component. We accomplish that by using props.

Props

Props (along with state) are the heart and soul, the meat and potatoes, of React. They are what allow us to pass information between components. Let’s take a look at how that might work.

Let’s start with an example, just to keep things simple. For now, since our Ideas component just contains an h2, let’s make that h2 say something different than “Ideas go here!”

In our App component, let’s add something to our render method.

// App.js

  render() {
    return(
      <main className='App'>
        <h1>IdeaBox</h1>
        <Ideas name='Travis' />
      </main>
    )
  }

Now, let’s adjust our Ideas component.

// Ideas.js

const Ideas = (props) => {

  return (
    <h2>Hello, {props.name}!</h2>
  )
}

What are those curly brackets doing? In JSX, whenever we’re writing something that is JavaScript (aka “not HTML”), we have to wrap it in curly brackets. In this case, “name” acts like a variable. It’s not a string that reads “name” - it’s a placeholder that represents the value of the property (in this case, “Travis”)! Because it’s a variable, we have to surround it in curly brackets to tell the JSX to treat the contents like JavaScript.

In your browser, you should see “Hello, Travis!” In App.js, add another Ideas component to the render method, but pass in a different name. What do you see in the browser? Try creating new props to use!

Okay, so just WHAT exactly is going on here?

Similar to how state is an object that contains key-value pairs, props is the name of an object that contains key-value pairs. From our above example, the key is “name”, and the value is “Travis”. So, in our Ideas component, we can access the value by writing props.name (which gives us a string of “Travis”). This is the same dot notation we learned in Mods 1 and 2 to access data stored in objects.

If, in the render method of our App component, we called the property “potato” instead of “name”, we would have to access it by (inside the Ideas component) writing props.potato.

We can even destructure the props object, because it’s just a regular object!

// Ideas.js

const Ideas = (props) => {
  const { name } = props;

  return (
    <h2>Hello, {name}!</h2>
  )
}

In this example, destructuring is a bit over-engineered, yes. However, we’ll see in our Card component that destructuring will save us from having to type props. over and over again.

And here’s YET ANOTHER super-fancy way to destructure:

// Ideas.js

const Ideas = ({name}) => {

  return (
    <h2>Hello, {name}!</h2>
  )
}

React lets us destructure props ON THE WAY IN. Whoa! It’s accomplishing the same thing as destructuring on a separate line, like in the previous example.

Mapping over the ideas array

All right. We don’t actually want to render an h2 in our Ideas component. We want to render some Cards with some gosh dang IDEAS!

Let’s create a Card component to use. Will it be function- or class- based?

Create your files: $ touch src/Card.js src/Card.css

// Card.js

import React from 'react';
import './Card.css';

const Card = () => {
  return (
    <div className='card'>
      <h3>Card!</h3>
    </div>
  )
}

export default Card;

Then, in your Ideas component, let’s just try to get these hooked up properly.

// Ideas.js

import React from 'react';
import Card from './Card';
import './Ideas.css';

const Ideas = (props) => {
  const { name } = props;

  return (
    <div className='ideas-container'>
      <Card />
      <Card />
      <Card />
    </div>
  )
}

export default Ideas;

And let’s throw some CSS in just to make our cards look like not a hot garbage fire:

/* Ideas.css */

.ideas-container {
  padding: 20px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-gap: 20px;
}

And in your Card css file:

/* Card.css */

.card {
  box-sizing: border-box;
  border: 3px solid black;
  padding: 10px;
}

Okay! Hopefully your app looks like this:

screenshot of IdeaBox so far

All right, friends. Let’s get to passing some PROPS! Let’s go all the way back to our App component and pass our list of ideas to the Ideas container component, so that it can then create Card components out of each individual idea.

// App.js

render() {
  return(
    <main className='App'>
      <h1>IdeaBox</h1>
      <Ideas ideas={this.state.ideas} />
    </main>
  )
}

Let’s unpack what we’re doing here. We created a new prop called “ideas”, and the value we’re passing in is our array of ideas which lives in the App component’s state. Remember, this.state.ideas is JavaScript, not HTML, so we need to wrap it in curly brackets!

Go look at the Ideas component in your React dev tools in the browser. You should see that the props contain a key of “ideas” with a value of the array of ideas from App state!

We now want to iterate through our array and create a Card component, passing it the information it needs to display the proper information!

// Ideas.js

const Ideas = ({ideas}) => {

  const ideaCards = ideas.map(idea => {
    return (
      <Card
        title={idea.title}
        description={idea.description}
        id={idea.id}
        key={idea.id}
      />
    )
  })

  return (
    <div className='ideas-container'>
      {ideaCards}
    </div>
  )
}

Okay - we’ve made a few changes! You’ll notice that we’re now destructuring the props as we pass them in! W H A T. I know. Then, we’re mapping over the ideas array and creating a new array of Card components, each with props of title and name. The key property is something that React requires when using map() to create elements. In this case, I’m just using the unique id that each idea has.

If you look in your React dev tools, you’ll see that both Card components now has props of “title” and “description”! Go ahead a change Card so we display that information.

// Card.js

const Card = ({ title, description, id }) => {
  return (
    <div className='card'>
      <h3>{title}</h3>
      <p>{description}</p>
      <button>🗑</button>
    </div>
  )
}

I created a button to delete the Card, but we’ll get to that later. For now, high five the people at your table, because we just got this sucker to display some ideas!!

Form.js

Let’s move on to our Form component. We’re going to create what is known as a controlled form.

Create the Form component files: $ touch src/Form.js src/Form.css

/* Form.css */

form {
  margin: auto;
}

input, button {
  margin: 5px;
  width: 30%;
  border: 2px solid black;
  font-size: 18px;
}

Our Form will start like this:

// Form.js

import React, { Component } from 'react';
import './Form.css';

class Form extends Component {
  constructor() {
    super();
    this.state = {
      title: '',
      description: ''
    }
  }

  render() {
    return (
      <form>
        <input
          type='text'
          placeholder='Title'
          name='title'
          value={this.state.title}
        />

        <input
          type='text'
          placeholder='Description'
          name='description'
          value={this.state.description}
        />

        <button>SUBMIT</button>
      </form>
    )
  }
}

export default Form;

But we need to write some functions. Let’s start by making sure that when we type into our inputs, they update the Form’s state.

// Form.js

  handleChange = event => {
    this.setState({ [event.target.name]: event.target.value });
  }

  render() {
    return (
      <form>
        <input
          type='text'
          placeholder='Title'
          name='title'
          value={this.state.title}
          onChange={event => this.handleChange(event)}
        />

        <input
          type='text'
          placeholder='Description'
          name='description'
          value={this.state.description}
          onChange={event => this.handleChange(event)}
        />

        <button>SUBMIT</button>
      </form>
    )
  }

What is this setState business? It’s a method that comes from the parent Component class. It does a few things: it takes in an object (with a key that matches a key in state, and the updated value), it updates state with the new information, and it triggers a re-render (it literally runs the render method again) to keep our displayed data up to date!

Take a look at your React dev tools - is the state updating as you type into the inputs?

When we click the submit button, what do we want to happen? We want to create an object out of the new idea and add it to the list of ideas that App is holding onto in state. How do we access App’s state from inside our Form component?

Passing bound methods

In App, we’re going to have to create a method that updates App’s state:

// App.js

  addIdea = (newIdea) => {
    this.setState({ ideas: [...this.state.ideas, newIdea] });
  }

  render() {
    return(
      <main className='App'>
        <h1>IdeaBox</h1>
        <Form addIdea={this.addIdea} />
        <Ideas ideas={this.state.ideas} />
      </main>
    )
  }

You’ll notice that we’re using an arrow function to create this class method. Why do you think that is? We know that - unlike keyword function functions - the context of this is set when an arrow function is declared, not when it is invoked. This means that, no matter where we call addIdea, no matter what component invokes it, the this from this.setState will refer to the App component.

This is good, because we’re passing it down as a prop to the Form component!

Before we had ES6 arrow functions at our disposal, we had to manually bind our functions in the constructor, like this:

// App.js

class App extends Component {
  constructor() {
    super();
    this.state = {
      ideas: [
        { id: 1, title: 'Prank Travis', description: 'Stick googly eyes on all his stuff' },
        { id: 2, title: 'Make a secret password app', description: 'So you and your rideshare driver can both know neither one of you is lying' },
        { id: 3, title: 'Learn a martial art', description: 'To exact vengeance upon my enemies' },
      ]
    }

    this.addIdea = this.addIdea.bind(this);
  }

  addIdea(newIdea) {
    this.setState({ ideas: [...this.state.ideas, newIdea] });
  }

  render() {
    return(
      <main className='App'>
        <h1>IdeaBox</h1>
        <Form addIdea={this.addIdea} />
        <Ideas ideas={this.state.ideas} />
      </main>
    )
  }
}

This took place in the constructor for a few reasons: you wouldn’t have to remember to bind it every place you wanted to pass it in the render, and it would only be declared once, when the constructor method was called. (Conversely, render is called every time the component state updates; there’s no need to have a bunch of identical bound addIdeas floating around in memory!)

You can see that using the arrow function is much shorter syntactically. However, it’s good to know the REASON behind using it, especially since some legacy codebases will still be using manually bound class methods.

Now, in the Form component, let’s make use of the addIdea method we passed as a prop. In class-based components, we reference props with this.props. If you remember from earlier, function-based components merely use the keyword props.

// Form.js

  submitIdea = event => {
    event.preventDefault(); // prevents the page from refreshing when the form submits
    const newIdea = {
      id: Date.now(),
      ...this.state // spreading in the title and description
    }
    this.props.addIdea(newIdea); // using the addIdea method from App that we passed as a prop to Form
    this.clearInputs(); // invoking the method I wrote below to reset the inputs
  }

  clearInputs = () => {
    this.setState({ title: '', description: '' });
  }

  render() {
    return (
      <form>
        <input
          type='text'
          placeholder='Title'
          name='title'
          value={this.state.title}
          onChange={event => this.handleChange(event)}
        />

        <input
          type='text'
          placeholder='Description'
          name='description'
          value={this.state.description}
          onChange={event => this.handleChange(event)}
        />

        <button onClick={event => this.submitIdea(event)}>SUBMIT</button> // adding the event listener to the button
      </form>
    )
  }
}

Deleting a Card

Now that you know how to use a method and props to allow a different component to update App’s state, see if you can get each Card’s delete button to work.

First, write the App method to delete an idea from state and pass it to the Ideas component:

// App.js

import React, { Component } from 'react';
import Ideas from './Ideas';
import Form from './Form';
import './App.css';

class App extends Component {
  constructor() {
    super();
    this.state = {
      ideas: [
        { id: 1, title: 'Prank Travis', description: 'Stick googly eyes on all his stuff' },
        { id: 2, title: 'Make a secret password app', description: 'So you and your rideshare driver can both know neither one of you is lying' },
        { id: 3, title: 'Learn a martial art', description: 'To exact vengeance upon my enemies' },
      ]
    }
  }

  addIdea = (newIdea) => {
    this.setState({ ideas: [...this.state.ideas, newIdea] });
  }

  deleteIdea = (id) => {
    console.log(id);
    const filteredIdeas = this.state.ideas.filter(idea => idea.id != id);

    this.setState({ ideas: filteredIdeas });
  }

  render() {
    return(
      <main className='App'>
        <h1>IdeaBox</h1>
        <Form addIdea={this.addIdea} />
        <Ideas ideas={this.state.ideas} deleteIdea={this.deleteIdea} />
      </main>
    )
  }
}

export default App;

Second, pass the deleteIdea function to each Card that the Ideas component creates:

import React, { Fragment } from 'react';
import Card from './Card';
import './Ideas.css';

const Ideas = ({ideas, deleteIdea}) => {

  const ideaCards = ideas.map(idea => {
    return (
      <Card
        title={idea.title}
        description={idea.description}
        id={idea.id}
        key={idea.id}
        deleteIdea={deleteIdea}
      />
    )
  })

  return (
    <div className='ideas-container'>
      {ideaCards}
    </div>
  )
}

export default Ideas;

Third, create an event listener on the Card button:

import React from 'react';
import './Card.css';

const Card = ({ title, description, id, deleteIdea }) => {
  return (
    <div className='card'>
      <h3>{title}</h3>
      <p>{description}</p>
      <button onClick={() => deleteIdea(id)}>🗑</button>
    </div>
  )
}

export default Card;

Voila! You’ve created a React application!

Review

Take a few minutes to journal:

  • What “aha” moments did you have?
  • Which concepts are the fuzziest for you right now?
  • Which concepts are the clearest for you right now?
  • What do you know about class-based components?
  • What do you know about function-based components?
  • What do you know about state?
  • What do you know about passing props?

Suggested homework

We strongly suggest that you try building IdeaBox (or a Grocery List app, or a To Do List app, or something similar) using React tonight, on your own.

Lesson Search Results

Showing top 10 results