-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Fix es_ui_shared eslint violations for useRequest hook #72947
Fix es_ui_shared eslint violations for useRequest hook #72947
Conversation
2897789
to
2554f46
Compare
Pinging @elastic/es-ui (Team:Elasticsearch UI) |
- Add clearer cleanup logic for unmounted components. - Align logic and comments in np_ready_request.ts and original request.ts.
…o false during cleanups unrelated to unmounting.
- Split request.ts into send_request.ts and use_request.ts. - Convert test files into TS. - Relax SendRequestResponse type definition to type error as any instead of expecting an Error, since we weren't actually fulfilling this expectation.
2554f46
to
961b0b2
Compare
961b0b2
to
2fe8e19
Compare
// To be functionally correct we'd send a new request if the method, path, query or body changes. | ||
// But it doesn't seem likely that the method will change and body is likely to be a new | ||
// object even if its shape hasn't changed, so for now we're just watching the path and the query. | ||
}, [path, stringifiedQuery]); | ||
}, [path, query]); |
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.
These are the only dependencies that I think should have any effect on the hook behavior, at least at this point in this hook's development. If we were to change the refs above to useCallback
, I'm concerned about introducing new and possibly unwanted behavior. For example, converting sendRequestRef
to useCallback
means defining dependencies that duplicate those defined here (see below). I think this makes the code's behavior more difficult to predict from reading it and I'm not clear on what the benefit would be.
I'm open to exploring this in separate PRs, but I think for the context of this PR I'd like to avoid introducing any complexity that doesn't have a clear, deliberate purpose (e.g. fixing a bug or defining behavior more clearly). I think the current code makes it clear what will happen if a) dependencies change and b) sendRequestRef
is called. If other people feel the same way then I think that's enough for us to consider this merge-worthy in its current form.
const sendRequest = useCallback(async () => {
// We don't clear error or data, so it's up to the consumer to decide whether to display the
// "old" error/data or loading state when a new request is in-flight.
setIsLoading(true);
const requestBody = {
path,
method,
query,
body,
};
const response = await sendRequest<D, E>(httpClient, requestBody);
const { data: serializedResponseData, error: responseError } = response;
// If an outdated request has resolved, ignore its outdated data.
if (isOutdatedRequest) {
return { data: null, error: null };
}
setError(responseError);
// If there's an error, keep the data from the last request in case it's still useful to the user.
if (!responseError) {
const responseData = deserializer(serializedResponseData);
setData(responseData);
}
setIsLoading(false);
setIsInitialRequest(false);
// If we're on an interval, we need to schedule the next request. This also allows us to reset
// the interval if the user has manually requested the data, to avoid doubled-up requests.
scheduleRequestRef.current!();
return { data: serializedResponseData, error: responseError };
}, [body, deserializer, httpClient, isOutdatedRequest, method, path, query]);
CC @walterra @jen-huang. The code here is the same as what you approved in #59626. I'm tagging you for visibility, and I intend to merge this once @sebelga approves. We have most of the release cycle to detect any bugs introduced into the consuming apps by these changes. One subtle change that could cause a change in behavior is in the way return {
data: null,
error: e.response ? e.response : e,
}; To this: return {
data: null,
error: e.response && e.response.data ? e.response.data : e.body,
}; To find and fix any bugs means we'll need to look through Snapshot Restore and Ingest Manager, which depended upon the old behavior. |
…ed on its dependencies. - Update sendRequest to clear the poll interval immediately when it's called. - Update eslintrc to lint es_ui_shared. - Type arguments correctly as UseRequestConfig in helpers.
… add test coverage for request behavior.
if (!isLoading) { | ||
scheduleRequest(); | ||
} | ||
}, [isLoading, scheduleRequest]); |
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 that here we're using isLoading
to signal the need to reschedule a request, and not totalRequests
as in https://github.com/cjcenizal/kibana/pull/25/files#diff-a60f7c6c34d3d037970d34856d1b3da2R142.
I think it makes sense to keep each hook as single-purpose as possible. This hook's sole purpose is to manage the polling rhythm.
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 think it is a little bit less explicit than with my totalRequest
count (I personally understand better "when the request count increments, reschedule a request", as "isLoading" seems like it could change independently.) but I am OK to go with your suggestion.
But your comment is not right, the reschedule will happen whenever scheduleRequest
changes, thus whenever its dependencies change.
Also, you are missing a
if (!isMounted.current) {
return;
}
on top, as we don't want to schedule a request when the component mounts, but when the initial request finished.
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.
But your comment is not right, the reschedule will happen whenever scheduleRequest changes, thus whenever its dependencies change.
Could you explain this a bit? From the way I read this code, it looks like the effect is called whenever scheduleRequest
(and therefore sendRequest
) changes, but the isLoading
condition prevents scheduleRequest
from being called each time.
we don't want to schedule a request when the component mounts, but when the initial request finished.
isLoading
is true when the component mounts, so the request isn't being initially scheduled. I'll add a comment to clarify this.
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.
For context, I had to switch away from using totalRequests
because here we're incrementing it before the request completes to get the request ID, instead of after like you originally suggested. This meant that if we used it to schedule the next request, then the next request would be scheduled before the first one completes.
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.
isLoading is true when the component mounts
This is the problem I think. It should be false
(as I was expecting it) and it is set to true when sendRequest()
is called.
Could you explain this a bit?
Nevermind I will understand it. For me adding so much comment makes it harder to follow and it seems that there is something "special" the code, but that's ok.
I was saying that the ´useEffect´ will be triggered whenever any of the deps changes (which is simply how useEffect works). And of course, the if
condition prevents the reschedule. But I don't need a comment to explain that.
My mental model was
const [isLoading, setIsLoading] = useState(false); // nothing happened just yet
...
useEffect(() => {
if (!isMounted.current) {
return; // clearly indicate to not schedule anything on mount
}
if (!isLoading) {
scheduleRequest();
}
}, [isLoading, scheduleRequest]);
With the above code, I don't need any comment as it is self explanatory.
For context, I had to switch away from using totalRequests because here we're incrementing it before the request completes to get the request ID, instead of after like you originally suggested. This meant that if we used it to schedule the next request, then the next request would be scheduled before the first one completes.
I am not following but that's OK I guess 😄
@sebelga I've converted all of |
@elasticmachine merge upstream |
// Setting isLoading to false also acts as a signal for scheduling the next poll request. | ||
setIsLoading(false); | ||
|
||
return { data: serializedResponseData, error: responseError }; |
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 think it's probably an antipattern that we're returning values from sendRequest
. Seems like the consumer should be depending on the values returned by the hook instead. Once we merge this I'll create an issue to check for any consumers that depend on this return value and refactor them so we can try to remove this.
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.
👍 This is something I noticed since the first iteration. But for some reason, we added the "// Gives the user the ability to manually request data"
comment and exposed the handler.
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 think we do want to retain sendRequest
as part of the useRequest
hook's return value, because we need it for "Reload" button behavior. But I don't think calling sendRequest
should return a promise that resolves to data
and error
, since that information is already available off the useRequest
hook's return value.
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.
Ah ok I see, then maybe the API should be clearer and expose a refresh()
handler that indeed does not return data
nor error
.
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 work @cjcenizal ! We almost got it! 😊 I left some not about unnecessary comments that "restate trivial things". For the hook I left a comment about a missing mount check that should be added. I will approve but it'd be good to add it before merging. Thanks!
// Setting isLoading to false also acts as a signal for scheduling the next poll request. | ||
setIsLoading(false); | ||
|
||
return { data: serializedResponseData, error: responseError }; |
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.
👍 This is something I noticed since the first iteration. But for some reason, we added the "// Gives the user the ability to manually request data"
comment and exposed the handler.
clearPollInterval(); | ||
|
||
// Pull off request ID and increment request count. | ||
const requestId = requestCountRef.current++; |
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.
nit: here it would be better to have
const requestId = ++requestCountRef.current;
and not have the requestCountRef.current - 1;
below (the -1
part)
// If there's a scheduled poll request, this new one will supersede it. | ||
clearPollInterval(); | ||
|
||
// Schedule next poll request. |
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.
nit: same here about unnecessary comment
} | ||
}, [pollIntervalMs, sendRequest, clearPollInterval]); | ||
|
||
// Send the request on component mount and whenever the dependencies of sendRequest() change. |
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.
nit: same here about the unnecessary comment (here you are basically describing the useEffect
lifecycle.
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've removed the other comments you pointed out. I think this is the one comment I'm going to keep, just to really emphasize "the dependencies of sendRequest()" bit. It seems super obvious after staring at this code for hours, but I think the comment is more for someone who hasn't already spent a ton of time looking at the code. :)
if (!isLoading) { | ||
scheduleRequest(); | ||
} | ||
}, [isLoading, scheduleRequest]); |
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 think it is a little bit less explicit than with my totalRequest
count (I personally understand better "when the request count increments, reschedule a request", as "isLoading" seems like it could change independently.) but I am OK to go with your suggestion.
But your comment is not right, the reschedule will happen whenever scheduleRequest
changes, thus whenever its dependencies change.
Also, you are missing a
if (!isMounted.current) {
return;
}
on top, as we don't want to schedule a request when the component mounts, but when the initial request finished.
// data, to avoid doubled-up requests. | ||
clearPollInterval(); | ||
|
||
// Pull off request ID and increment request count. |
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.
nit: I am not sure it is necessary to comment on obvious code behavior 😊 As per our guidelines "Don't use comments to restate trivial things." (https://github.com/elastic/kibana/blob/master/STYLEGUIDE.md#use-slashes-for-comments)
@@ -94,12 +94,6 @@ module.exports = { | |||
'jsx-a11y/no-onchange': 'off', | |||
}, | |||
}, | |||
{ |
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.
Great! Thanks for removing it here and adding the comments in the few places we still have the issue 👍
* Reconcile request helpers with eslint rules for React hooks. - Add clearer cleanup logic for unmounted components. - Align logic and comments in np_ready_request.ts and original request.ts. * Reorganize modules and convert tests to TS. - Split request.ts into send_request.ts and use_request.ts. - Convert test files into TS. - Relax SendRequestResponse type definition to type error as any instead of expecting an Error, since we weren't actually fulfilling this expectation. * Convert everything to hooks and add test coverage for request behavior. * Fix Watcher memoization bugs. # Conflicts: # .eslintrc.js
@elastic/ingest-management @elastic/ml-ui Sorry for not pinging you before merging this! Could you please test out your apps which consume |
💚 Build SucceededBuild metrics@kbn/optimizer bundle module count
async chunks size
page load bundle size
History
To update your PR or re-run it, just comment with: |
Replaces #59626
Partially addresses #49554
Closes #49572 (originally fixed as part of the NP migration)
Closes #49562 (originally fixed as part of the NP migration)
Closes #42225 (flaky test)
Closes #42563 (flaky test)
Closes #42562 (flaky test)
Closes #42561 (flaky test)
This PR:
request.ts
helper and replaces it with the NP-ready versionAdds clean-up logic for when the component using this helper is unmounted(This was reverted)SendRequestResponse
type definition to typeerror
asany
instead ofError
, since we weren't actually fulfilling that type.To test:
Errors
I tested error-handling using data streams.
ES errors
405
400
caused_by
chainI was concerned that the change in the way
sendRequest
surfaced errors would change the waycaused_by
chains were being surfaced in the UI, but it looks like the two apps that use this module and surfacecaused_by
chains actually return them as part of the success response, not the error response. These apps are Snapshot Restore (deletRepositories endpoint) and Index Management (deleteDataStreams and deleteIndexTemplates endpoints).Non-ES errors
500
500