Up to this point, we've assumed that the Giphy API will quickly respond and never fail. But no matter how great the uptime of an API is, the user's internet connection can determine how long it takes to get a response and if the request fails or not.
🏅 The goal of this exercise is to add loading and error states to our app.
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/09-custom-hook
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
- Managing loading & error states
- Using
useReducer
- Leveraging ES6+ to maintain application state
Update useGiphy
to keep track of the status
of the API response and return it along with results
& setSearchParams
:
const useGiphy = () => {
const [searchParams, setSearchParams] = useState({})
const [results, setResults] = useState([])
const [status, setStatus] = useState('idle') // 👈🏾 NEW
useEffect(() => {
const fetchResults = async () => {
try {
// 👇🏾 before API request
setStatus('pending')
const apiResponse = await getResults(searchParams)
setResults(apiResponse.results)
// 👇🏾 after successful API request
setStatus('resolved')
} catch (err) {
console.error(err)
}
}
fetchResults()
}, [searchParams])
// 👇🏾 returning `status` & `results` together as an object
return [{ status, results }, setSearchParams]
}
Update App
with the new return value from useGiphy()
and pass status
to <Results />
:
const App = () => {
// Converted to object literal destructuring in order to get
// out the 3 properties 👇🏾
const [{ status, results }, setSearchParams] = useGiphy()
return (
<main>
<h1>Giphy Search!</h1>
<SearchForm
onChange={setSearchParams}
initialSearchQuery="friend"
initialLimit={24}
/>
{/* add status to Results 👇🏾 */}
<Results items={results} status={status} />
</main>
)
}
Now display the loading indicator in Results
:
// new `status` prop added 👇🏾
const Results = ({ items, status }) => {
const containerEl = useRef(null)
const isLoading = status === 'idle' || status === 'pending'
// 👇🏾 new loading indicator
if (isLoading) {
return (
<section className="callout warning text-center">
<p className="h3">Loading new results...</p>
</section>
)
}
return (
items.length > 0 && (
...
)
)
}
Results.propTypes = {
items: ...,
// new prop type for `status` 👇🏾
status: PropTypes.oneOf(['idle', 'pending', 'resolved']).isRequired,
}
NOTE: You can change the value to
wait()
inapi.js
to be higher to simulate a slow API response.
Display the loading indicator as well as the previous results using a Fragment:
const Results = ({ items, status }) => {
const isLoading = status === 'idle' || status === 'pending'
return (
<>
{isLoading && (
<section className="callout warning text-center">
<p className="h3">Loading new results...</p>
</section>
)}
{items.length > 0 && (
...
)}
</>
)
}
In useGiphy.js
, refactor the two useState()
calls for status
& results
to a single call to useReducer()
:
const INITIAL_STATE = {
status: 'idle',
results: [],
}
// 👇🏾 brand new reducer
const reducer = (state, action) => {
switch (action.type) {
case 'started': {
return {
...state,
status: 'pending',
}
}
case 'success': {
return {
...state,
status: 'resolved',
results: action.results,
}
}
default: {
// In case we mis-type an action!
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
const useGiphy = () => {
const [searchParams, setSearchParams] = useState({})
// 👇🏾 new reducer state
const [state, dispatch] = useReducer(reducer, INITIAL_STATE)
useEffect(() => {
const fetchResults = async () => {
try {
// 👇🏾 dispatch action instead of directly setting state
dispatch({ type: 'started' })
const apiResponse = await getResults(searchParams)
// 👇🏾 dispatched action will set two state properties
dispatch({ type: 'success', results: apiResults })
} catch (err) {
console.error(err)
}
}
fetchResults()
}, [searchParams])
return [state, setSearchParams]
}
- Display an error state if the API fails to successfully return
- Can display with the previous results, but should hide when new results are requested
- Use the
'rejected'
status - 🔑 HINT:
throw new Error('Fake error!')
inapi.js
to easily simulate this case
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 Final Quiz!.
useReducer
API reference- Fragments
- Stop using isLoading booleans
- Don't Sync State. Derive It
- Should I useState or useReducer
Got questions? Need further clarification? Feel free to post a question in Ben Ilegbodu's AMA!