Skip to content

Instantly share code, notes, and snippets.

@bengotow
Created June 17, 2021 19:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bengotow/e08f4a2155ecdac2a629e7ca95ba9de3 to your computer and use it in GitHub Desktop.
Save bengotow/e08f4a2155ecdac2a629e7ca95ba9de3 to your computer and use it in GitHub Desktop.
useResource with Examples
// Basic usage to fetch data for display
export const NotesTabContent: React.FunctionComponent<{
scope: CoreAPI.TabScope;
boundsForExports: TimeBounds<number>;
}> = ({ scope, boundsForExports }) => {
const [kinds, setKinds] = useQueryPersistedState<string[]>({
encode: kinds => ({ kinds: kinds.join(',') }),
decode: qs => (qs.kinds ? qs.kinds.split(',') : ['New', 'Known']),
});
const [notes] = useResource<PeopleAPI.ProfileNoteResponse[]>(`/talent/api/notes`, {
...scope,
kinds,
count: 100,
});
return (
<div style={{ display: 'flex' }}>
<Column style={{ width: '65%' }}>
{notes && notes.length > 0 ? (
<List
pagination={{ size: 'small' }}
dataSource={notes}
renderItem={note => <NoteRow key={note.id} note={note} />}
/>
) : (
<SpinnerOrEmptyState data={notes} title="No recent notes" />
)}
</Column>
</div>
)
}
// Usage with the second parameter, which provides pre-built refresh / PUT / POST APIs following a restful schema, and updates the cached result value.
export const PageConfig = ({match}) => {
const [configs = [], { putItem }] = useResource<CoreAPI.Config[], CoreAPI.Config>(
'/talent/api/config'
);
const config = configs.find(c => c.key === match.params.key);
return (
<Editor
config={config}
onSave={async config => {
await putItem(config);
return config;
}}
key={config.key}
/>
);
};
import { message } from 'antd';
import qs from 'query-string';
import React from 'react';
// This assumes the presence of a function called `makeRequest` that runs the actual fetch calls to the backend (probably with your auth headers, etc)
interface ResourceConfig {
silent?: boolean;
GETRequestMethod?: 'GET' | 'POST';
}
export interface ResourceOps<T, U = Partial<T>> {
post: (v: U) => Promise<void>;
put: (v: U) => Promise<void>;
putItem: (item: { id: string | number } & U) => Promise<void>;
delete: () => Promise<void>;
deleteItem: (item: string | number | { id: string | number }) => Promise<void>;
applyLocalUpdates: (v: T) => void;
refresh: () => Promise<T>;
}
export function useResource<T, U = Partial<T>>(
path: string,
query?: { [key: string]: any },
config?: ResourceConfig
) {
const GETRequestMethod = config?.GETRequestMethod || 'GET';
const [state, setState] = React.useState<{ value: T; url: string } | undefined>(undefined);
const url = `${path}${path.includes('?') ? '&' : '?'}${
GETRequestMethod === 'GET' && query ? qs.stringify(query) : ''
}`;
React.useEffect(() => {
const fetch = async () => {
setState(undefined);
setState({
url,
value: await makeRequest<T>(
url,
GETRequestMethod,
GETRequestMethod === 'POST' ? query : undefined
),
});
};
void fetch();
}, [url]);
const ops: ResourceOps<T, U> = {
post: async (v: U) => {
try {
const resp = await makeRequest<U>(path, 'POST', v);
if ('id' in resp && state && state.value instanceof Array) {
setState({ ...state, value: [...state.value, resp] as any });
} else {
console.warn(
'useResource: POST state update skipped, response contains no id or state.value is not an array'
);
}
!config?.silent && void message.success('Item created');
} catch (err) {
!config?.silent && void message.error('Failed to save, try again.');
throw err;
}
},
put: async (v: U) => {
try {
const resp = await makeRequest<T>(path, 'PUT', v);
if (state) {
if ('id' in resp) {
setState({ ...state, value: Object.assign({}, state.value, resp) });
} else if (state.value instanceof Array && resp instanceof Array) {
setState({ ...state, value: resp });
} else {
console.warn(
'useResource: PUT state update skipped, response does not look like object or array item'
);
}
}
!config?.silent && void message.success('Changes saved');
} catch (err) {
!config?.silent && void message.error('Failed to save, try again.');
throw err;
}
},
putItem: async (item: { id: string | number } & U) => {
const resp = await makeRequest<any>(`${path}/${item.id}`, 'PUT', item);
if (resp && 'id' in resp && state && state.value instanceof Array) {
const nextValue: any = [];
for (const item of state.value) {
nextValue.push(item.id === resp.id ? resp : item);
}
setState({ ...state, value: nextValue });
} else {
console.warn(
'useResource: PUT state update skipped, response does not look like array item'
);
}
!config?.silent && void message.success('Updated successfully');
},
delete: async () => {
await makeRequest<T>(path, 'DELETE');
!config?.silent && void message.success('Deleted successfully');
},
deleteItem: async (item: string | number | { id: string | number }) => {
const itemId = typeof item === 'object' && 'id' in item ? item.id : item;
await makeRequest<T>(`${path}/${itemId}`, 'DELETE');
!config?.silent && void message.success('Deleted successfully');
if (state && state.value instanceof Array) {
setState({ ...state, value: state.value.filter(i => i.id !== itemId) as any });
} else {
console.warn(
'useResource: DELETE state update skipped, local state is not an array of items'
);
}
},
applyLocalUpdates: (v: T) => {
setState({ url, value: v });
},
refresh: async () => {
const v = await makeRequest<T>(
url,
GETRequestMethod,
GETRequestMethod === 'POST' ? query : undefined
);
setState({ url, value: v });
return v;
},
};
// Explicitly tell TS this is a tuple of two distinct types, not an array of (T | typeof Ops)
if (state?.url === url) {
return [state.value, ops] as [T | undefined, ResourceOps<T, U>];
} else {
return [undefined, ops] as [T | undefined, ResourceOps<T, U>];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment