Skip to content

Instantly share code, notes, and snippets.

@Sparragus
Last active October 29, 2019 17:33
Show Gist options
  • Save Sparragus/e10b71533f6d6f96020f777536520e8a to your computer and use it in GitHub Desktop.
Save Sparragus/e10b71533f6d6f96020f777536520e8a to your computer and use it in GitHub Desktop.
Recipe for handling requests in a React App
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;
}
};
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>
)
}
// 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);
}
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