add useFetch demo

This commit is contained in:
Keith Grant
2020-01-29 09:14:32 -08:00
parent d15f7b76fa
commit aaf371ee23
4 changed files with 142 additions and 63 deletions

View File

@@ -37,7 +37,8 @@ function Inventory({ i18n, setBreadcrumb }) {
useEffect(() => { useEffect(() => {
async function fetchData() { async function fetchData() {
try { try {
setHasContentLoading(true); // TODO: delete next line
// setHasContentLoading(true);
const { data } = await InventoriesAPI.readDetail(match.params.id); const { data } = await InventoriesAPI.readDetail(match.params.id);
setBreadcrumb(data); setBreadcrumb(data);
setInventory(data); setInventory(data);

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React, { useCallback, useEffect } from 'react';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -11,20 +11,38 @@ import DeleteButton from '@components/DeleteButton';
import ContentError from '@components/ContentError'; import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import { InventoriesAPI } from '@api'; import { InventoriesAPI } from '@api';
import useEndpoint from './useEndpoint'; import useEndpoint, { useFetch } from './useEndpoint';
import { Inventory } from '../../../types'; import { Inventory } from '../../../types';
function InventoryDetail({ inventory, i18n }) { function InventoryDetail({ inventory, i18n }) {
const history = useHistory(); const history = useHistory();
const { results: instanceGroups, isLoading, error } = useEndpoint( // initial approach with useEndpoint()
// const { results: instanceGroups, isLoading, error } = useEndpoint(
// useCallback(async () => {
// const { data } = await InventoriesAPI.readInstanceGroups(inventory.id);
// return data.results;
// }, [inventory.id])
// );
// more versatile approach with useFetch()
const {
result: instanceGroups,
isLoading,
error,
fetch: fetchInstanceGroups,
} = useFetch(
useCallback(async () => { useCallback(async () => {
const { data } = await InventoriesAPI.readInstanceGroups(inventory.id); const { data } = await InventoriesAPI.readInstanceGroups(inventory.id);
return data.results; return data.results;
}, [inventory.id]), }, [inventory.id]),
inventory.id []
); );
useEffect(() => {
fetchInstanceGroups(inventory.id);
}, [fetchInstanceGroups, inventory.id]);
const deleteInventory = async () => { const deleteInventory = async () => {
await InventoriesAPI.destroy(inventory.id); await InventoriesAPI.destroy(inventory.id);
history.push(`/inventories`); history.push(`/inventories`);

View File

@@ -1,5 +1,6 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef, useCallback } from 'react';
// Initial approach (useEffect baked in)
export default function useEndpoint(fetch) { export default function useEndpoint(fetch) {
const [results, setResults] = useState([]); const [results, setResults] = useState([]);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@@ -38,3 +39,41 @@ export default function useEndpoint(fetch) {
error, error,
}; };
} }
// more versatile approach (returns function to make API request)
export function useFetch(fetch, initialValue) {
const [result, setResult] = useState(initialValue);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return {
result,
error,
isLoading,
fetch: useCallback(async () => {
setIsLoading(true);
try {
const response = await fetch();
if (isMounted.current) {
setResult(response);
}
} catch (err) {
if (isMounted.current) {
setError(err);
}
} finally {
if (isMounted.current) {
setIsLoading(false);
}
}
}, [fetch]),
};
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom'; import { useLocation, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -13,6 +13,10 @@ import PaginatedDataList, {
ToolbarDeleteButton, ToolbarDeleteButton,
} from '@components/PaginatedDataList'; } from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs'; import { getQSConfig, parseQueryString } from '@util/qs';
// TODO: move useEndpoint to better location
import useEndpoint, {
useFetch,
} from '../../Inventory/InventoryDetail/useEndpoint';
import OrganizationListItem from './OrganizationListItem'; import OrganizationListItem from './OrganizationListItem';
@@ -25,64 +29,89 @@ const QS_CONFIG = getQSConfig('organization', {
function OrganizationsList({ i18n }) { function OrganizationsList({ i18n }) {
const location = useLocation(); const location = useLocation();
const match = useRouteMatch(); const match = useRouteMatch();
const [contentError, setContentError] = useState(null);
const [deletionError, setDeletionError] = useState(null);
const [hasContentLoading, setHasContentLoading] = useState(true);
const [itemCount, setItemCount] = useState(0);
const [organizations, setOrganizations] = useState([]);
const [orgActions, setOrgActions] = useState(null);
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const [deletionError, setDeletionError] = useState(null);
const addUrl = `${match.url}/add`; const addUrl = `${match.url}/add`;
const canAdd = orgActions && orgActions.POST;
const isAllSelected =
selected.length === organizations.length && selected.length > 0;
const loadOrganizations = async ({ search }) => { // const {
const params = parseQueryString(QS_CONFIG, search); // results: organizations,
setContentError(null); // error: contentError,
setHasContentLoading(true); // isLoading: isOrgsLoading,
try { // } = useEndpoint(
const [ // useCallback(async () => {
{ // const params = parseQueryString(QS_CONFIG, location.search);
data: { count, results }, // const [
}, // {
{ // data: { count, results },
data: { actions }, // },
}, // {
] = await Promise.all([ // data: { actions },
// },
// ] = await Promise.all([
// OrganizationsAPI.read(params),
// loadOrganizationActions(),
// ]);
// return {
// organizations: results,
// count,
// actions,
// };
// }, [location])
// );
const {
result: { organizations = {}, organizationCount, actions },
error: contentError,
isLoading: isOrgsLoading,
fetch: fetchOrganizations,
} = useFetch(
useCallback(async () => {
const params = parseQueryString(QS_CONFIG, location.search);
const [orgs, orgActions] = await Promise.all([
OrganizationsAPI.read(params), OrganizationsAPI.read(params),
loadOrganizationActions(), OrganizationsAPI.readOptions(),
]); ]);
setItemCount(count); return {
setOrganizations(results); organizations: orgs.data.results,
setOrgActions(actions); organizationCount: orgs.data.count,
setSelected([]); actions: orgActions.data.actions,
} catch (error) { };
setContentError(error); }, [location]),
} finally { {
setHasContentLoading(false); organizations: [],
organizationCount: 0,
actions: {},
} }
}; );
const loadOrganizationActions = () => { const {
if (orgActions) { isLoading: isDeleteLoading,
return Promise.resolve({ data: { actions: orgActions } }); // error: deletionError,
} fetch: deleteOrganizations,
return OrganizationsAPI.readOptions(); } = useFetch(
}; useCallback(async () => {
return Promise.all(
selected.map(({ id }) => OrganizationsAPI.destroy(id))
);
}, [selected])
);
useEffect(() => {
fetchOrganizations();
}, [fetchOrganizations]);
const handleOrgDelete = async () => { const handleOrgDelete = async () => {
setHasContentLoading(true); await deleteOrganizations();
try { await fetchOrganizations();
await Promise.all(selected.map(({ id }) => OrganizationsAPI.destroy(id)));
} catch (error) {
setDeletionError(error);
} finally {
await loadOrganizations(location);
}
}; };
const hasContentLoading = isDeleteLoading || isOrgsLoading;
const canAdd = actions && actions.POST;
const isAllSelected =
selected.length === organizations.length && selected.length > 0;
const handleSelectAll = isSelected => { const handleSelectAll = isSelected => {
if (isSelected) { if (isSelected) {
setSelected(organizations); setSelected(organizations);
@@ -99,14 +128,6 @@ function OrganizationsList({ i18n }) {
} }
}; };
const handleDeleteErrorClose = () => {
setDeletionError(null);
};
useEffect(() => {
loadOrganizations(location);
}, [location]); // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<> <>
<PageSection> <PageSection>
@@ -115,7 +136,7 @@ function OrganizationsList({ i18n }) {
contentError={contentError} contentError={contentError}
hasContentLoading={hasContentLoading} hasContentLoading={hasContentLoading}
items={organizations} items={organizations}
itemCount={itemCount} itemCount={organizationCount}
pluralizedItemName="Organizations" pluralizedItemName="Organizations"
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} onRowClick={handleSelect}
@@ -179,7 +200,7 @@ function OrganizationsList({ i18n }) {
isOpen={deletionError} isOpen={deletionError}
variant="danger" variant="danger"
title={i18n._(t`Error!`)} title={i18n._(t`Error!`)}
onClose={handleDeleteErrorClose} onClose={() => setDeletionError(null)}
> >
{i18n._(t`Failed to delete one or more organizations.`)} {i18n._(t`Failed to delete one or more organizations.`)}
<ErrorDetail error={deletionError} /> <ErrorDetail error={deletionError} />