Last active
October 29, 2019 17:33
-
-
Save Sparragus/e10b71533f6d6f96020f777536520e8a to your computer and use it in GitHub Desktop.
Recipe for handling requests in a React App
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as qs from './querystring'; | |
/* | |
Helpers | |
*/ | |
const BASE_PATH = '/api/v1'; | |
class APIError extends Error { | |
constructor(res, data) { | |
super( | |
data.errors | |
? data.errors[0] | |
: data.error || data.message || res.statusText || res.status | |
); | |
this.name = 'APIError'; | |
this.type = data.type; | |
this.status = res.status; | |
this.statusText = res.statusText; | |
} | |
} | |
function buildPath(path) { | |
const normalizedPath = path[0] === '/' ? path : `/${path}`; | |
return BASE_PATH + normalizedPath; | |
} | |
function getAuthToken() { | |
// This varies per project | |
return 'somelongasstokenyouneedtogetforyouruser'; | |
} | |
function makeRequest(path, options = {}) { | |
const { method = 'GET', body, headers = {} } = options; | |
const normalizedBody = typeof body === 'object' ? JSON.stringify(body) : body; | |
const token = getAuthToken(); | |
if (token) { | |
headers['Authorization'] = `Bearer: ${token}`; | |
} | |
headers['Content-Type'] = 'application/json'; | |
headers['Accept'] = 'application/json'; | |
return { | |
resource: buildPath(path), | |
init: { | |
method, | |
body: normalizedBody, | |
headers, | |
}, | |
}; | |
} | |
async function callApi(path, options) { | |
const req = makeRequest(path, options); | |
const res = await fetch(req.resource, req.init); | |
if (!res.ok) { | |
let data = {}; | |
try { | |
data = await res.json(); | |
} catch (error) {} | |
const error = new APIError(res, data); | |
throw error; | |
} | |
// No Content | |
if (res.status == 204) { | |
return; | |
} | |
try { | |
return await res.json(); | |
} catch (error) { | |
// The server's response was not application/JSON | |
return {}; | |
} | |
} | |
/* | |
API Calls | |
*/ | |
// Posts | |
export const Posts = { | |
async list(filters = {}) { | |
const path = `/posts?${qs.stringify(filters)}`; | |
const data = await callApi(path, { | |
method: 'GET', | |
}); | |
// This here varies depending on the API response | |
return data.posts; | |
}, | |
async get(id) { | |
const path = `/posts/${id}`; | |
const data = await callApi(path, { | |
method: 'GET', | |
}); | |
// This here varies depending on the API response | |
return data.post; | |
}, | |
async create(post) { | |
const path = `/posts`; | |
const data = await callApi(path, { | |
method: 'POST', | |
body: { | |
post, | |
}, | |
}); | |
// This here varies depending on the API response | |
return data.post; | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import useQuery from './useQuery.js'; | |
import * as api from './api.js'; | |
function LoadingPage () { | |
return <div>Loading...</div> | |
} | |
function ErrorPage ({ error }) { | |
return <div>Error: {error.message}</div> | |
} | |
function PostsListItem ({ post }) { | |
return ( | |
<div> | |
<div> | |
<Link to={`/posts/${post.id}`}> | |
{post.title} | |
</Link> | |
</div> | |
<p>{post.summary}</p> | |
</div> | |
) | |
} | |
export default function PostsPage() { | |
const posts = useQuery(() => api.Posts.list()) | |
if (posts.loading) { | |
return <LoadingPage /> | |
} | |
if (posts.error) { | |
return <ErrorPage error={posts.error} /> | |
} | |
return ( | |
<ul> | |
{posts.data.map(post => | |
<li key={post.id}> | |
<PostsListItem post={post} /> | |
</li> | |
} | |
</ul> | |
) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Install this from npm | |
import qs from 'qs'; | |
export function parse(value, options = {}) { | |
return qs.parse(value, { | |
ignoreQueryPrefix: value.charAt(0) === '?', | |
...options, | |
}); | |
} | |
export function stringify(value, options = { arrayFormat: 'brackets' }) { | |
return qs.stringify(value, options); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
const FETCH = 'FETCH'; | |
const SUCCESS = 'SUCCESS'; | |
const ERROR = 'ERROR'; | |
function reducer(state, action = {}) { | |
switch (action.type) { | |
case FETCH: | |
return { ...state, loading: true, error: null }; | |
case SUCCESS: | |
return { ...state, data: action.payload, loading: false, loaded: true }; | |
case ERROR: | |
return { ...state, error: action.payload, loading: false }; | |
default: | |
throw new Error('Unexpected action type.'); | |
} | |
} | |
export default function useQuery(query, options = {}) { | |
const { autoFire = true } = options; | |
const [state, dispatch] = React.useReducer(reducer, { | |
data: undefined, | |
loading: true, | |
loaded: false, | |
error: null, | |
}); | |
async function fetchData() { | |
dispatch({ type: FETCH }); | |
try { | |
const data = await query(); | |
dispatch({ type: SUCCESS, payload: data }); | |
} catch (error) { | |
dispatch({ type: ERROR, payload: error }); | |
} | |
} | |
React.useEffect(() => { | |
if (autoFire) { | |
fetchData(); | |
} | |
}, []); | |
return { | |
data: state.data, | |
loading: state.loading, | |
loaded: state.loaded, | |
error: state.error, | |
refetch: fetchData, | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment