At this point we have a full app. It's a mini-app, but it's still fully-functional. However, in it's currently structure, as more features are added, maintaining it will become very challenging. This is because all the functionality is in the one App
component. The form UI, results UI, app state, and API call all live within App
. For such a small app, having everything in one place has been convenient, but it'll scale poorly.
🏅 So, the goal of this step is to practice creating and composing React components. Components let you split the UI into independent, reusable pieces, and think about each piece in isolation. Components can refer to other components in their output. This lets us use the same component abstraction for any level of detail. A button, a form, a dialog, a screen, etc. In React apps, all of these are commonly expressed as components.
Conceptually, components are like JavaScript functions. They accept arbitrary inputs (called "props") and return React elements describing what should appear on the screen.
As always, if you run into trouble with the tasks or exercises, you can take a peek at the final source code.
Help! I didn't finish the previous step! 🚨
If you didn't successfully complete the previous step, you can jump right in by copying the step.
Complete the setup instructions if you have not yet followed them.
Re-run the setup script, but use the previous step as a starting point:
npm run setup -- src/05-form-submit
This will also back up your src/workshop
folder, saving your work.
Now restart the app:
npm start
After some initial compiling, a new browser window should open up at http://localhost:3000/, and you should be able to continue on with the tasks below.
Concepts | Tasks | Exercises | Elaboration & Feedback | Resources
- Creating and composing React components
- Configuring components via passing props
Start by creating a new src/workshop/Results.js
file to contain a new Results
component:
import React from 'react'
const Results = () => {
return null
}
export default Results
Next, move over all the results UI code:
const Results = () => {
return (
results.length > 0 && (
<section className="callout primary">
{results.map((item) => (
<section
key={item.id}
className="card"
style={{
width: '300px',
display: 'inline-block',
marginRight: '16px',
}}
>
<video src={item.previewUrl} alt={item.title} loop autoPlay />
<section className="card-section">
<h5>
<a href={item.url} target="_blank" rel="noopener noreferrer">
{item.title}
</a>{' '}
({item.rating})
</h5>
</section>
</section>
))}
</section>
)
)
}
Back in App.js
, we'll import Results.js
at the top of the file:
import React, { useState, useEffect } from 'react'
import { getResults } from './api'
import Results from './Results' // 👈🏾 new import
In place of where the results display code used to be, we'll render <Results />
passing the results
as items
:
return (
<main>
<h1>Giphy Search!</h1>
<form>...</form>
<Results items={results} />
</main>
)
We need to update Results.js
to support this new items
prop:
const Results = (props) => { // 👈🏾 new `props` arg
const { items } = props // 👈🏾 new `items` prop
return (
items.length > 0 && ( // `results` ➡️ `items`
<section className="callout primary">
{items.map((item) => ( // `results` ➡️ `items`
...
))}
</section>
)
)
}
Let's turn our attention to the search form. Start by creating a new src/workshop/SearchForm.js
file to contain a new SearchForm
component:
import React, { Fragment, useState } from 'react'
const SearchForm = () => {
return null
}
export default SearchForm
Next copy over all of the form-related code:
const RATINGS = [
{ value: '', label: 'All' },
{ value: 'g', label: 'G' },
{ value: 'pg', label: 'PG' },
{ value: 'pg-13', label: 'PG-13' },
{ value: 'r', label: 'R' },
]
const LIMITS = [6, 12, 18, 24, 30]
const SearchForm = () => {
const [inputValue, setInputValue] = useState('')
const [searchQuery, setSearchQuery] = useState('')
const [showInstant, setShowInstant] = useState(false)
const [searchRating, setSearchRating] = useState('')
const [searchLimit, setSearchLimit] = useState(12)
const realSearchQuery = showInstant ? inputValue : searchQuery
const handleSubmit = (e) => {
e.preventDefault()
setSearchQuery(inputValue)
}
return (
<form onSubmit={handleSubmit}>
<section className="input-group">
<input
type="search"
...
/>
...
</section>
<section>
<input
type="checkbox"
...
/>
...
</section>
<hr />
<fieldset>
<legend>Choose a rating</legend>
...
</fieldset>
<hr />
<label>
# of Results
...
</label>
</form>
)
}
Back in App.js
, we'll import the SearchForm
component at the top of the file:
import React, { useState, useEffect } from 'react'
import { getResults } from './api'
import Results from './Results'
import SearchForm from './SearchForm' // 👈🏾 new import
And in place of the <form>
tag we'll render <SearchForm />
:
return (
<main>
<h1>Giphy Search!</h1>
<SearchForm />
<Results items={results}>
</main>
)
Add a new formValues
state variable and new onChange
handler for <SearchForm />
:
const App = () => {
const [formValues, setFormValues] = useState({}) // 👈🏾 NEW!
const [results, setResults] = useState([])
useEffect(() => {
const fetchResults = async () => {
try {
// pass single state variable object 👇🏾
const apiResponse = await getResults(formValues)
setResults(apiResponse.results)
} catch (err) {
console.error(err)
}
}
fetchResults()
}, [formValues]) // 👈🏾 sole useEffect dependency
return (
<main>
<h1>Giphy Search!</h1>
<SearchForm onChange={setFormValues} />
<Results items={results} />
</main>
)
}
We now need SearchForm
to have a new onChange
prop that it calls whenever its fields change, passing the same object properties that getResults
expects (searchQuery
, limit
& rating
):
const SearchForm = (props) => {
// new `props` arg
const { onChange } = props // 👈🏾 new `onChange` prop
const [inputValue, setInputValue] = useState('')
const [searchQuery, setSearchQuery] = useState('')
const [showInstant, setShowInstant] = useState(false)
const [searchRating, setSearchRating] = useState('')
const [searchLimit, setSearchLimit] = useState(12)
const realSearchQuery = showInstant ? inputValue : searchQuery
useEffect(() => {
// Call `onChange` prop in `useEffect()` that is
// similar to where we called `getResults()` in `App`
onChange({
searchQuery: realSearchQuery,
rating: searchRating,
limit: searchLimit,
})
}, [onChange, realSearchQuery, searchRating, searchLimit])
const handleSubmit = (e) => {
e.preventDefault()
setSearchQuery(inputValue)
}
return <form onSubmit={handleSubmit}>...</form>
}
NOTE:
SearchForm
has an<input type="search">
element for the query search field, an<input type="checkbox">
element for the instant results toggle, multiple connected<input type="radio">
elements for the rating picker, and a<select>
for the number of results switcher. Normally you would use a component library like Material-UI that would have those reusable components for you.
- From
Results
, pull out aResultsItem
component intosrc/workshop/ResultsItem.js
with 5 props:id
,title
,url
,rating
&previewUrl
. - Use the React Developer Tools to inspect the component hierarchy, including the props being passed to the
<SearchForm />
&<ResultsItem />
components.
After you're done with the exercise and before jumping to the next step, please fill out the elaboration & feedback form. It will help seal in what you've learned.
Go to Step 7 - Prop Types.
Got questions? Need further clarification? Feel free to post a question in Ben Ilegbodu's AMA!