-
Notifications
You must be signed in to change notification settings - Fork 16.5k
refactor: Introduce api resource hooks, fetch owners for chart errors #13218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
|
||
| function useChartOwnerNames(chartId: string) { | ||
| const { result } = useResourceTransform( | ||
| useApiV1Resource<Chart>(`/api/v1/chart/${chartId}`), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: If you want you can just select the columns you want by:
/api/v1/chart/501?q=(columns:!(owners.first_name,owners.last_name,owners.id),keys:!(none))
these parameters in JSON format are:
{
"columns": [
"owners.first_name",
"owners.last_name",
"owners.id"
],
"keys": [
"none"
]
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect if more of these requests are being made in the future, and we've changed the useApiResource hooks to use shared storage, it would actually be better for performance to request the entire object. That way if one hook wants owners and another hook wants creators, or name, or whatever, they can all get what they need with one request.
For now though, this is definitely a good add. Thanks for the tip!
Codecov Report
@@ Coverage Diff @@
## master #13218 +/- ##
===========================================
+ Coverage 53.06% 72.95% +19.89%
===========================================
Files 489 576 +87
Lines 17314 20772 +3458
Branches 4482 5375 +893
===========================================
+ Hits 9187 15155 +5968
+ Misses 8127 5494 -2633
- Partials 0 123 +123
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
|
Hmm, interesting concept. We definitely lack some handy React hooks to fetch API resources. But I'd also like to point out the It even infers the return type for you: So we may be able to simplify the hooks a little bit. For example, function useAsyncData<T>(promise: Promise<T>) {
const [result, setResult] = useState<{ data?: T; error": any }>();
useEffect(() => {
promise.then(data => setResult({ data }))
.catch(error => setResult({ error }));
}, [promise]);
return { data, error };
}
function useChartOwnerNames(chartId: number) {
const fetchChartOwnerNames = makeApi({
method: 'GET',
requestType: 'rison',
endpoint: `/api/v1/chart/${chartId}`,
processResponse: ({ result } : { result?: Chart }) =>
results?.owners.map(owner =>
`${owner.first_name || ''} ${owner.last_name || ''}`.strip() || owner.id) || [],
);
return useAsyncData(fetchChartOwnerNames({
columns: ['owners.first_name', 'owners.last_name', 'owners.id']
}));
}If we add support for url match params for |
|
@ktmud At first I did consider an approach like your That's also why I opted not to use the I do wonder if those cool future add-ons might be better implemented inside the guts of |
|
@suddjian I'm not against having hooks that do extra cool stuff. I think a powerful API resource manager would be very useful. I do, however, believe there's value in hiding API endpoints and pre-defined transformations from end users and have a centralized, strongly typed API catalog like this.
Re: dedupping requests and caching results, I do have some simple solution that I used in our internal Superset: const promiseCache: Record<
string,
{ timestamp: number; promise: Promise<any> }
> = {};
/**
* Cache a promise maker, make sure it doesn't fire multiple times
* @param api - the promise generator.
* @param expires - cache TTL in seconds.
* @param keyPrefix - prefix for the cache key.
*/
export function cached<T extends (...args: any[]) => Promise<any>>(
api: T,
{
expires = 300,
keyPrefix = '',
}: {
expires?: number;
keyPrefix?: string;
},
) {
return function wrapped(...args: Parameters<T>): ReturnType<T> {
const cacheKey = `${keyPrefix}-${JSON.stringify(args)}`;
if (
cacheKey in promiseCache &&
promiseCache[cacheKey].timestamp + expires * 1000 > Date.now()
) {
return promiseCache[cacheKey].promise as ReturnType<T>;
}
const promise = api(...args);
promiseCache[cacheKey] = {
promise,
timestamp: Date.now(),
};
promise.catch(() => {
// don't cache failed responses
delete promiseCache[cacheKey];
});
return promise as ReturnType<T>;
};
}
export const getValidMetrics = cached(
makeApi<MinervaValidationApiPaylod, ControlValues['metrics']>({
method: 'GET',
endpoint: '/minerva/valid_metrics',
}),
{ keyPrefix: 'metrics' },
);It's a very simple wrapper on the generated API caller and handles both caching and deduplication (making the same API call will not fire a request twice). For now, this is probably good enough for Superset, but if we are thinking of getting fancy with deduplication and caching by entity, I would also like to see us evaluating some existing solutions out there such as REST Hooks before committing to create our own solution for things like shared storage. |
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [endpoint]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to follow the logic of this function, which is nested in a few other functions, but basically trying to see if we need this useEffect block. Do you expect this method to be called multiple times with the same endpoint string as an argument? And you're saying that if that happens, you don't want to fetch the resource again, but just return the original one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's exactly right! We do need to expect this function to be called multiple times with the same endpoint string, because hooks are called every time the component renders. But we don't want to fire off a new http request every time a render occurs. This useEffect block makes sure that a new request only fires if the caller requests a different resource (when the endpoint changes).
For reference see the React docs on building your own hooks https://reactjs.org/docs/hooks-custom.html
I like the catalog idea! That is very much compatible with this work. I'll add something like that for these hooks as well. Almost all new frontend code is being written with function components, with some efforts underway to convert class components to functions, and it is also quite easy to write a HOC to patch in hook functionality to class components if needed, so relying on hooks for this sort of logic doesn't bother me at all. I'm actually pretty excited about the possibilities there.
This is my first time reading about the REST hooks library, looks like a great option! I agree that we should wait to review it until we want to get fancy. Looks like it adds a few new abstractions, which may or may not be right for us when we reach that point. The hooks I've written here have a very simple interface so they should be easy to replace with a library later if we want to. |
|
@suddjian are we still thinking of moving api responses into redux and how would that work with this? |
| export function useApiResourceFullBody<RESULT>( | ||
| endpoint: string, | ||
| ): Resource<RESULT> { | ||
| const [resource, setResource] = useState<Resource<RESULT>>(initialState); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eschutho Moving state into Redux (or any other sort of provider) could be done by only modifying the internal logic of this hook. Specifically, by calling Redux's useSelector here instead of useState, and using actions instead of calling setResource.
You know what maybe I'll just do that now instead of talking about it 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awesome!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I implemented redux state management, but I'm not actually convinced that is the best way to share state between these hooks. So I've reverted it. But if you want to see what that looks like, the commit is in the PR history.
| export function useChartOwnerNames(chartId: string) { | ||
| return useTransformedResource( | ||
| useApiV1Resource<Chart>(`/api/v1/chart/${chartId}?q=${ownerNamesQuery}`), | ||
| extractOwnerNames, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so on a high level, these two args are basically set up the request and then parse the response? I'm trying to see how digestible this code will be to beginners that are trying to use it, too. :)
Small nit, but should we move the url to the top of this file as a constant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nm, I see that in your notes above..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to use properly formatted doc comments on everything. If you browse the code in an IDE and mouse-over the use of one of these functions, it should tell you what's up
This reverts commit d64d50a.
|
Another library for consideration: https://react-query.tanstack.com/ |
…apache#13218) * refactor: introduce api resource hooks, fix error owners * comment * vestigial comment * not a function * clarification * vestigial comment * filter the api for a leaner response * reorganize code, add resource hook catalog * better names * implement state management in redux * Revert "implement state management in redux" This reverts commit d64d50a. * add tests for hooks

SUMMARY
This introduces a new hook
useApiV1Resource, which I want to put forward as an example of a new state management pattern. This lets a functional component fetch arbitrary api data with just a couple lines of code, with the entire async request lifecycle and component lifecycle taken care of. For now it uses local state, but we can change it to use any other form of storage without changing how the hook is called.As a micro-POC, I've used the hook to refactor some code to lazy load chart owners instead of depending on bootstrap data. This will save quite a few joins in future stages of the Single Page App project.
TEST PLAN
Unit tests to be written before merge - seeking feedback on hook design first.
ADDITIONAL INFORMATION
ownersfield from slices on the frontend #13227