Let’s create the form that adds a new product.

The AddProduct component is the piece of the app responsible for showing the form and intercepting the form submit event.

We now have this:

import React from 'react'

export default () => {
  return <div>AddProduct</div>
}

We want to have a form where we can enter the product name, the product description, the price and a link to a picture (not uploads now, as we don’t want to get in o the backend part).

First, we define the markup of the form:

import React from 'react'

export default props => {
  return (
    <div>
      <form>
        <h1>Add Product</h1>
        <div>
          <label>Name:</label>
          <input required />
        </div>
        <div>
          <label>Price in $:</label>
          <input required />
        </div>
        <div>
          <label>Description:</label>{' '}
          <textarea required />
        </div>
        <div>
          <label>Image URL:</label>
          <input required />
        </div>
        <input type="submit" value="Add" className="button" />
      </form>
    </div>
  )
}

We need to handle state, and we do this using Hooks. Import useState:

import React, { useState } from 'react'

and then define 4 state values:

const [name, setName] = useState()
const [price, setPrice] = useState()
const [description, setDescription] = useState()
const [image, setImage] = useState()

When an input element changes, we need to alter those state values. Check out how to handle forms in React in https://flaviocopes.com/react-forms/.

import React, { useState } from 'react'

export default props => {
  const [name, setName] = useState()
  const [price, setPrice] = useState()
  const [description, setDescription] = useState()
  const [image, setImage] = useState()

  const handleChangeName = e => {
    setName(e.target.value)
  }

  const handleChangePrice = e => {
    setPrice(e.target.value)
  }

  const handleChangeDescription = e => {
    setDescription(e.target.value)
  }

  const handleChangeImage = e => {
    setImage(e.target.value)
  }

  return (
    <div>
      <form>
        <h1>Add Product</h1>
        <div>
          <label>Name:</label>
          <input required onChange={handleChangeName} />
        </div>
        <div>
          <label>Price in $:</label>
          <input required onChange={handleChangePrice} />
        </div>
        <div>
          <label>Description:</label>{' '}
          <textarea required onChange={handleChangeDescription} />
        </div>
        <div>
          <label>Image URL:</label>
          <input required onChange={handleChangeImage} />
        </div>
        <input type="submit" value="Add" className="button" />
      </form>
    </div>
  )
}

Then when the form is submitted, we need to handle that as well. We pass an event handler to the onSubmit event on the form element. In this, we call the addProduct() prop, passing to the parent component (App) the values that we stored in the local state:

import React, { useState } from 'react'

export default props => {
  //...

  const addProduct = e => {
    e.preventDefault()
    props.addProduct({ name, price, description, image })
  }

  //...

  return (
    <div>
      <form onSubmit={addProduct}>
        //...
      </form>
    </div>
  )
}

In App.js we add the addProduct prop. Notice how we are using a different syntax here with React router, using the render prop, which I explain in https://flaviocopes.com/react-pass-props-router/:

<Route
  path="/add-product"
  render={() => <AddProduct addProduct={addProduct} />}
/>

which calls the addProduct function. In this function we update the App products array, which we manage using Hooks as well:

import React, { useState } from 'react'
//...

const App = () => {
  const [products, setProducts] = useState([])

  const addProduct = product => {
    setProducts([...products, product])
  }

  return (
    <Router>
      <div id="app">
        //...
        <main>
          //...
          <Route
            path="/add-product"
            render={() => <AddProduct addProduct={addProduct} />}
          />
          //...
        </main>
      </div>
    </Router>
  )
}

export default App

Note that we don’t persist the products data anywhere, so every time you refresh the page, the data is lost. We’ll persist this data later.

We must do something else now, however. We need to calculate a product slug, which will be part of the product URL.

Every product has a URL like /product/product-name. We calculate this slug part when the product is added, based on its name.

Here’s the function that computes the slug:

const slugify = str => {
  str = str || ''
  const a =
    'àáäâèéëêìíïîòóöôùúüûñçßÿœæŕśńṕẃǵǹḿǘẍźḧ·/_,:;άαβγδεέζήηθιίϊΐκλμνξοόπρσςτυϋύΰφχψωώ'
  const b =
    'aaaaeeeeiiiioooouuuuncsyoarsnpwgnmuxzh------aavgdeeziitiiiiklmnxooprsstyyyyfhpoo'
  const p = new RegExp(a.split('').join('|'), 'g')

  return str
    .toString()
    .trim()
    .toLowerCase()
    .replace(/ου/g, 'ou')
    .replace(/ευ/g, 'eu')
    .replace(/θ/g, 'th')
    .replace(/ψ/g, 'ps')
    .replace(/\//g, '-')
    .replace(/\s+/g, '-') // Replace spaces with -
    .replace(p, c => b.charAt(a.indexOf(c))) // Replace special chars
    .replace(/&/g, '-and-') // Replace & with 'and'
    .replace(/[^\w\-]+/g, '') // Remove all non-word chars
    .replace(/\-\-+/g, '-') // Replace multiple - with single -
    .replace(/^-+/, '') // Trim - from start of text
    .replace(/-+$/, '') // Trim - from end of text
}

You don’t need to understand it. I picked it from StackOverflow and it does its job in my tests. It accepts a string like A mug and returns a-mug, but you don’t have to believe me: copy/paste it in the developer tools, and test some strings.

Paste it in the AddProduct component, and use it in the addProduct function:

props.addProduct({ name, price, description, image, slug: slugify(name) })

Cool! The form should be working fine now.

There’s just one thing missing. When we add a product, it would be nice to go back to the list of products, right? We must do two things to make this work.

First, from the App component we must pass the history prop to AddProduct. This object is made available by React Router as a parameter to the render prop:

<Route
  path="/add-product"
  render={({ history }) => (
    <AddProduct addProduct={addProduct} history={history} />
  )}
/>

Then we can call its push method in the addProduct function in the AddProduct component:

const addProduct = e => {
  e.preventDefault()
  props.addProduct({ name, price, description, image, slug: slugify(name) })
  props.history.push('/')
}

and once the product is added we’re moved back to the products list (with the updated URL, too)

The final code for `src/components/AddProduct.js looks like this:

import React, { useState } from 'react'

const slugify = str => {
  str = str || ''
  const a =
    'àáäâèéëêìíïîòóöôùúüûñçßÿœæŕśńṕẃǵǹḿǘẍźḧ·/_,:;άαβγδεέζήηθιίϊΐκλμνξοόπρσςτυϋύΰφχψωώ'
  const b =
    'aaaaeeeeiiiioooouuuuncsyoarsnpwgnmuxzh------aavgdeeziitiiiiklmnxooprsstyyyyfhpoo'
  const p = new RegExp(a.split('').join('|'), 'g')

  return str
    .toString()
    .trim()
    .toLowerCase()
    .replace(/ου/g, 'ou')
    .replace(/ευ/g, 'eu')
    .replace(/θ/g, 'th')
    .replace(/ψ/g, 'ps')
    .replace(/\//g, '-')
    .replace(/\s+/g, '-') // Replace spaces with -
    .replace(p, c => b.charAt(a.indexOf(c))) // Replace special chars
    .replace(/&/g, '-and-') // Replace & with 'and'
    .replace(/[^\w\-]+/g, '') // Remove all non-word chars
    .replace(/\-\-+/g, '-') // Replace multiple - with single -
    .replace(/^-+/, '') // Trim - from start of text
    .replace(/-+$/, '') // Trim - from end of text
}

export default props => {
  const [name, setName] = useState()
  const [price, setPrice] = useState()
  const [description, setDescription] = useState()
  const [image, setImage] = useState()

  const addProduct = e => {
    e.preventDefault()
    props.addProduct({ name, price, description, image, slug: slugify(name) })
    props.history.push('/')
  }

  const handleChangeName = e => {
    setName(e.target.value)
  }

  const handleChangePrice = e => {
    setPrice(e.target.value)
  }

  const handleChangeDescription = e => {
    setDescription(e.target.value)
  }

  const handleChangeImage = e => {
    setImage(e.target.value)
  }

  return (
    <div>
      <form onSubmit={addProduct}>
        <h1>Add Product</h1>
        <div>
          <label>Name:</label>
          <input required onChange={handleChangeName} />
        </div>
        <div>
          <label>Price in $:</label>
          <input required onChange={handleChangePrice} />
        </div>
        <div>
          <label>Description:</label>{' '}
          <textarea required onChange={handleChangeDescription} />
        </div>
        <div>
          <label>Image URL:</label>
          <input required onChange={handleChangeImage} />
        </div>
        <input type="submit" value="Add" className="button" />
      </form>
    </div>
  )
}

and here’s App.js:

import React, { useState } from 'react'
import './App.css'
import { BrowserRouter as Router, Link, Route } from 'react-router-dom'
import AddProduct from './components/AddProduct.js'
import ProductsList from './components/ProductsList.js'
import SingleProduct from './components/SingleProduct.js'

const App = () => {
  const [products, setProducts] = useState([])

  const addProduct = product => {
    setProducts([...products, product])
  }

  return (
    <Router>
      <div id="app">
        <aside>
          <Link to={`/`}>Products</Link>
          <Link to={`/add-product`}>Add product</Link>
        </aside>

        <main>
          <Route exact path="/" component={ProductsList} />
          <Route
            path="/add-product"
            render={({ history }) => (
              <AddProduct addProduct={addProduct} history={history} />
            )}
          />
          <Route path="/product/:slug" component={SingleProduct} />
        </main>
      </div>
    </Router>
  )
}

export default App

Go to the next lesson