Building Ideabox in React

Learning Goals

  • Build stateful/stateless react components
  • Pass data down, and actions up
  • Be able to explain what a bound function is, and how to make one
  • Be able to destructure props coming into a component

Vocab

  • Data down
  • Actions up
  • Stateless
  • Stateful
  • Controlled form component
  • Passing props
  • Destructure
  • Binding
  • Autobinding

Building Ideabox in React

Today we’re going to explore some React patterns that will become very familiar to us over the next few weeks. While this Ideabox may seem pretty contrived, you’ll find that many application out in the real world are just high end Ideaboxes.

What components do we need?

Consider the following mockup:

// Ideabox mockup

You’ve already worked with React a bit, so you’ve seen components before. What sort of components do you think you’ll need to build this application? Will they be stateful or stateless components? What is the difference between those two things? Talk in your small groups for 5 min, then we’ll discuss as a group.

Controlled Form Component

Let’s start with our form for new ideas. We’re going to use a controlled form component here. All that means is that our component will be stateful, the values of the form will be bound to the state of the component, and the state of the component will update whenever someone types in the form field. Here’s the boilerplate:

// NewIdeaForm.js

import React, { Component } from 'react'

class NewIdeaForm extends Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '',
      description: ''
    }
  }

  render() {
    return (
      <form>
        <input name="name" value={this.state.name} />
        <input name="description" value={this.state.description} />
        <button>Submit</button>
      </form>
    )
  }
}

Notice how the value of each input is defined by whatever is in state? That gets us our first critical piece of what makes up a controlled form, but what about when the user changes the input? Right now nothing would happen, we need to handle that onChange event.

// NewIdeaForm.js

import React, { Component } from 'react'

class NewIdeaForm extends Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '',
      description: ''
    }
  }

  handleNameChange(e) {
    const value = e.target.value
    this.setState({name: value})
  }

  render() {
    return (
      <form>
        <input name="name" value={this.state.name} onChange={this.handleNameChange}  />
        <input name="description" value={this.state.description} />
        <button>Submit</button>
      </form>
    )
  }
}

Now we’re handling the name change with our handleNameChange function. But wait! When we run this code, we get an error: cannot read property 'setState' of undefined. What does that mean?


Turn and Talk: Why did we get that error? What are our options for solving it?


Ok, so once we’ve properly bound our function, it should work for us. Here is one possible binding solution:

// NewIdeaForm.js

import React, { Component } from 'react'

class NewIdeaForm extends Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '',
      description: ''
    }
  }

  handleNameChange = (e) => {
    const value = e.target.value
    this.setState({name: value})
  }

  render() {
    return (
      <form>
        <input name="name" value={this.state.name} onChange={this.handleNameChange}  />
        <input name="description" value={this.state.description} />
        <button>Submit</button>
      </form>
    )
  }
}

This uses the ES6 arrow syntax to automatically bind handleNameChange to this. That allows us to call methods on this inside of handleNameChange. That’s great for our first input, but what about our second input?

We could make another function that specifically handles the description change, but that is going to get repetitious pretty quickly. Instead, let’s use the name property on the input, and take advantage of JavaScript’s dynamic key assignment, refactoring our handleNameChange function to be more generic

// NewIdeaForm.js

import React, { Component } from 'react'

class NewIdeaForm extends Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '',
      description: ''
    }
  }

  handleChange = (e) => {
    const {name, value} = e.target
    this.setState({[name]: value})
  }

  render() {
    return (
      <form>
        <input
          name="name"
          value={this.state.name}
          onChange={this.handleChange}
        />
        <input
          name="description"
          value={this.state.description}
          onChange={this.handleChange}
        />
        <button>Submit</button>
      </form>
    )
  }
}

Great! Now our handleChange function will handle changes to either input, in each case updating state with the correct data.

addNewIdea

Now that we’ve got a form that can handle user input, we need to take that state and create new ideas with it. Let’s look again at our App component:

// App.js

import React, { Component } from 'react'
import NewIdeaForm from './NewIdeaForm'

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

  render() {
    return(
      <div>
        <NewIdeaForm />
      </div>
    )
  }
}

We need some kind of function that can add new ideas to our array. You might be tempted to use the array prototype method push here, but that’s not a great idea. If we did something like this.state.ideas.push(idea), we’re mutating the state in place, and we’re not taking advantage of the React lifecycle methods. Instead, we want to set the state with a new ideas array:

// App.js

import React, { Component } from 'react'
import NewIdeaForm from './NewIdeaForm'

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

  addIdea = (idea) => {
    const newIdea = {...idea, id: Date.now()}
    const ideas = [...this.state.ideas, newIdea]
    this.setState({ ideas })
  }

  render() {
    return(
      <div>
        <NewIdeaForm addIdea={this.addIdea} />
      </div>
    )
  }
}

Notice that our addIdea method also adds an id to each new idea. This will prove useful later when we want to remove ideas.

Also, we’re passing our new addIdea method down to our form as a prop. This gives our form access to this function, but we’re still going to need to make some changes to our form to make it work.

handleSubmit

Our form is getting the addIdea function passed as a prop, but right now we’re not doing anything with it. Lets add another function to handle the onSubmit of the form, and then call our addIdea prop function:

// NewIdeaForm.js

import React, { Component } from 'react'

class NewIdeaForm extends Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '',
      description: ''
    }
  }

  handleChange = (e) => {
    const {name, value} = e.target
    this.setState({[name]: value})
  }

  handleSubmit = (e) => {
    e.preventDefault()
    this.props.addIdea(this.state)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          name="name"
          value={this.state.name}
          onChange={this.handleChange}
        />
        <input
          name="description"
          value={this.state.description}
          onChange={this.handleChange}
        />
        <button>Submit</button>
      </form>
    )
  }
}

Great! Now, onSubmit our handleSubmit function will be called, and call the addIdea function it was passed from App as a prop. This is what we mean when we say ‘data down, actions up’. We’re passing the function down (yes, functions are a data type as well), and then the action that the user takes submitting the form gets passed back up to the App when that function is called.

Display ideas on page

We’re adding our ideas to the App state, but we’re not yet displaying those on the page. Let’s make a new component called IdeasContainer and pass the ideas in state to it as a prop.

// IdeasContainer.js

import React from 'react'
import IdeaCard from './IdeaCard'

const IdeasContainer = ({ideas}) => {

  const ideaCards = ideas.map(idea => <IdeaCard {...idea} />)

  return(
    <div className="IdeasContainer">
      { ideaCards }
    </div>
  )
}

Notice we’re mapping over all our ideas to create an array of IdeaCard components. We haven’t created that component yet though! Let’s fix that.

import React from 'react'

const IdeaCard = ({name, description, id}) => (
  <div className="IdeaCard">
    <h1>{name}</h1>
    <p>{description}</p>
  </div>
)

Great! Now we’re displaying our ideas on the page via our two functional (stateless) components IdeasContainer and IdeaCard. Next up, we’ll work though removing an idea from the page.

removeIdea

Before we show off any solutions, take a moment to think about the problem. If we need to remove an idea from the page, what is the best way to do that? Where do our ideas live? Where should the code to remove them live? Take 5 minutes and discuss how you would solve the problem in small groups.

There are a number of different ways that we could solve this, but for this exercise, let’s create a removeIdea function in the App component, then pass it down as a prop.

// App.js

import React, { Component } from 'react'
import NewIdeaForm from './NewIdeaForm'

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

  addIdea = (idea) => {
    const newIdea = {...idea, id: Date.now()}
    const ideas = [...this.state.ideas, newIdea]
    this.setState({ ideas })
  }

  removeIdea = (id) => {
    const ideas = this.state.ideas.filter(idea => idea.id !== id)
    this.setState({ideas})
  }

  render() {
    return(
      <div>
        <NewIdeaForm addIdea={this.addIdea} />
        <IdeasContainer ideas={this.state.ideas} removeIdea={this.removeIdea}/>
      </div>
    )
  }
}

When removeIdea is called, we’ll iterate over the ideas in state, and filter out any idea whose id doesn’t match the id that the function is called with. Let’s update our IdeasContainer and IdeaCard to call this function when a remove button is clicked.

// IdeasContainer.js

import React from 'react'
import IdeaCard from './IdeaCard'

const IdeasContainer = ({ideas, removeIdea}) => {

  const ideaCards = ideas.map(idea => <IdeaCard {...idea} removeIdea={removeIdea} />)

  return(
    <div className="IdeasContainer">
      { ideaCards }
    </div>
  )
}
import React from 'react'

const IdeaCard = ({name, description, id, removeIdea}) => (
  <div className="IdeaCard">
    <h1>{name}</h1>
    <p>{description}</p>
    <button onClick={() => removeIdea(id)}>Remove</button>
  </div>
)

Wrapping it up

Not too shabby! We’ve built a small application that can take user input, store this in the application, display the user input on the browser, and update the remove the same data. While this might seem contrived, this pattern is the essence of what React is all about, and we’ll see it over and over again as we continue to learn about the library.

References

Important React Concepts
React Docs
Create React App

Lesson Search Results

Showing top 10 results