Our app wouldn’t be very interesting without the ability to request data from a
backend for the user to view and interact with. The way to do this in the web is
using HTTP with the window.fetch
API. Here’s a quick simple example of that
API in action:
window.fetch('http://example.com/movies.json').then(response => {return response.json()}).then(data => {console.log(data)})
All the HTTP methods are supported as well, for example, here’s how you would POST data:
window.fetch('http://example.com/movies', {method: 'POST',headers: {'Content-Type': 'application/json',// if auth is required. Each API may be different, but// the Authorization header with a token is common.Authorization: `Bearer ${token}`,},body: JSON.stringify(data), // body data type must match "content-type" header}).then(response => {return response.json()}).then(data => {console.log(data)})
If the request fails with an unsuccessful status code (>= 400
), then the
response
object’s ok
property will be false. It’s common to reject the
promise in this case:
window.fetch(url).then(async response => {const data = await response.json()if (response.ok) {return data} else {return Promise.reject(data)}})
It’s good practice to wrap window.fetch
in your own function so you can set
defaults (especially handy for authentication). Additionally, it’s common to
have “clients” which build upon this wrapper for operations on different
resources.
Integrating this kind of thing with React involves utilizing React’s useEffect
hook for making the request and useState
for managing the status of the
request as well as the response data and error information.
You might consider making the network request in the event handler. In general I
recommend to do all your side effects inside the useEffect
. This is because in
the event handler you don’t have any possibility to prevent race conditions, or
to implement any cancellation mechanism.
📜 https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
import * as React from 'react'function useSafeDispatch(dispatch) {const mounted = React.useRef(false)React.useLayoutEffect(() => {mounted.current = truereturn () => (mounted.current = false)}, [])return React.useCallback((...args) => (mounted.current ? dispatch(...args) : void 0),[dispatch],)}// Example usage:// const {data, error, status, run} = useAsync()// React.useEffect(() => {// run(fetchPokemon(pokemonName))// }, [pokemonName, run])const defaultInitialState = {status: 'idle', data: null, error: null}function useAsync(initialState) {const initialStateRef = React.useRef({...defaultInitialState,...initialState,})const [{status, data, error}, setState] = React.useReducer((s, a) => ({...s, ...a}),initialStateRef.current,)const safeSetState = useSafeDispatch(setState)const setData = React.useCallback(data => safeSetState({data, status: 'resolved'}),[safeSetState],)const setError = React.useCallback(error => safeSetState({error, status: 'rejected'}),[safeSetState],)const reset = React.useCallback(() => safeSetState(initialStateRef.current),[safeSetState],)const run = React.useCallback(promise => {if (!promise || !promise.then) {throw new Error(`The argument passed to useAsync().run must be a promise. Maybe a function that's passed isn't returning anything?`,)}safeSetState({status: 'pending'})return promise.then(data => {setData(data)return data},error => {setError(error)return Promise.reject(error)},)},[safeSetState, setData, setError],)return {// using the same names that react-query uses for convenienceisIdle: status === 'idle',isLoading: status === 'pending',isError: status === 'rejected',isSuccess: status === 'resolved',setData,setError,error,status,data,run,reset,}}export {useAsync}
/** @jsx jsx */import {jsx} from '@emotion/core'import * as React from 'react'import Tooltip from '@reach/tooltip'import {FaSearch} from 'react-icons/fa'import {Input, BookListUL, Spinner} from './components/lib'import {BookRow} from './components/book-row'import {client} from './utils/api-client'function DiscoverBooksScreen() {const [status, setStatus] = React.useState('idle')const [data, setData] = React.useState(null)const [query, setQuery] = React.useState('')const [queried, setQueried] = React.useState(false)const isLoading = status === 'loading'const isSuccess = status === 'success'React.useEffect(() => {if (!queried) {return}setStatus('loading')client(`books?query=${encodeURIComponent(query)}`).then(responseData => {setData(responseData)setStatus('success')})}, [query, queried])function handleSearchSubmit(event) {event.preventDefault()setQueried(true)setQuery(event.target.elements.search.value)}return (<divcss={{maxWidth: 800, margin: 'auto', width: '90vw', padding: '40px 0'}}><form onSubmit={handleSearchSubmit}><Inputplaceholder="Search books..."id="search"css={{width: '100%'}}/><Tooltip label="Search Books"><label htmlFor="search"><buttontype="submit"css={{border: '0',position: 'relative',marginLeft: '-35px',background: 'transparent',}}>{isLoading ? <Spinner /> : <FaSearch aria-label="search" />}</button></label></Tooltip></form>{isSuccess ? (data?.books?.length ? (<BookListUL css={{marginTop: 20}}>{data.books.map(book => (<li key={book.id} aria-label={book.title}><BookRow key={book.id} book={book} /></li>))}</BookListUL>) : (<p>No books found. Try another search.</p>)) : null}</div>)}export {DiscoverBooksScreen}
Quick Links
Legal Stuff
Social Media