-
Notifications
You must be signed in to change notification settings - Fork 0
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
Use request cr changes #25
Changes from 4 commits
9b14210
3391fcf
661a87d
cf40591
34eb883
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,10 +17,14 @@ | |
* under the License. | ||
*/ | ||
|
||
import { useEffect, useState, useRef } from 'react'; | ||
import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; | ||
|
||
import { HttpSetup } from '../../../../../src/core/public'; | ||
import { sendRequest, SendRequestConfig, SendRequestResponse } from './send_request'; | ||
import { | ||
sendRequest as _sendRequest, | ||
SendRequestConfig, | ||
SendRequestResponse, | ||
} from './send_request'; | ||
|
||
export interface UseRequestConfig extends SendRequestConfig { | ||
pollIntervalMs?: number; | ||
|
@@ -33,114 +37,129 @@ export interface UseRequestResponse<D = any, E = Error> { | |
isLoading: boolean; | ||
error: E | null; | ||
data?: D | null; | ||
sendRequest: () => Promise<SendRequestResponse<D, E>>; | ||
sendRequest: () => Promise<SendRequestResponse<D | null, E | null>>; | ||
} | ||
|
||
export const useRequest = <D = any, E = Error>( | ||
httpClient: HttpSetup, | ||
{ | ||
path, | ||
method, | ||
query, | ||
body, | ||
query: _query, | ||
body: _body, | ||
pollIntervalMs, | ||
initialData, | ||
deserializer = (data: any): any => data, | ||
deserializer, | ||
}: UseRequestConfig | ||
): UseRequestResponse<D, E> => { | ||
const sendRequestRef = useRef<() => Promise<SendRequestResponse<D, E>>>(); | ||
const scheduleRequestRef = useRef<() => void>(); | ||
const isMounted = useRef(false); | ||
const pollIntervalIdRef = useRef<any>(null); | ||
|
||
// Main states for tracking request status and data | ||
const [error, setError] = useState<null | any>(null); | ||
const [isLoading, setIsLoading] = useState<boolean>(true); | ||
const [data, setData] = useState<any>(initialData); | ||
|
||
// Consumers can use isInitialRequest to implement a polling UX. | ||
const [isInitialRequest, setIsInitialRequest] = useState<boolean>(true); | ||
const pollIntervalMsRef = useRef<number | undefined>(); | ||
const pollIntervalIdRef = useRef<any>(null); | ||
const [totalRequests, setTotalRequests] = useState(0); | ||
const isInitialRequest = totalRequests === 0; | ||
|
||
// We always want to use the most recently-set interval when scheduling the next request. | ||
pollIntervalMsRef.current = pollIntervalMs; | ||
// Convert our object to string to be able to compare them in our useMemo | ||
// This allows the consumer to freely passed new objects to the hook on each | ||
// render without asking him to memoize them. | ||
const bodyToString = _body ? JSON.stringify(_body) : undefined; | ||
const queryToString = _query ? JSON.stringify(_query) : undefined; | ||
|
||
// Tied to every render and bound to each request. | ||
let isOutdatedRequest = false; | ||
const requestBody = useMemo(() => { | ||
return { | ||
path, | ||
method, | ||
query: queryToString ? JSON.parse(queryToString) : undefined, | ||
body: bodyToString ? JSON.parse(bodyToString) : undefined, | ||
}; | ||
}, [path, method, queryToString, bodyToString]); | ||
|
||
scheduleRequestRef.current = () => { | ||
// Clear current interval | ||
const cleanUpPollInterval = useCallback(() => { | ||
if (pollIntervalIdRef.current) { | ||
clearTimeout(pollIntervalIdRef.current); | ||
} | ||
}, []); | ||
|
||
// Set new interval | ||
if (pollIntervalMsRef.current) { | ||
pollIntervalIdRef.current = setTimeout(sendRequestRef.current!, pollIntervalMsRef.current); | ||
} | ||
}; | ||
const sendRequest = useCallback(async () => { | ||
cleanUpPollInterval(); | ||
|
||
sendRequestRef.current = 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 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) { | ||
if (isMounted.current === false) { | ||
return { data: null, error: null }; | ||
} | ||
|
||
setIsLoading(false); | ||
setTotalRequests((prev) => prev + 1); | ||
|
||
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); | ||
const responseData = deserializer | ||
? deserializer(serializedResponseData) | ||
: 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 }; | ||
}; | ||
}, [requestBody, httpClient, deserializer, cleanUpPollInterval]); | ||
|
||
const scheduleRequest = useCallback(() => { | ||
cleanUpPollInterval(); | ||
|
||
if (pollIntervalMs) { | ||
pollIntervalIdRef.current = setTimeout(sendRequest, pollIntervalMs); | ||
} | ||
}, [pollIntervalMs, sendRequest, cleanUpPollInterval]); | ||
|
||
useEffect(() => { | ||
sendRequest(); | ||
}, [sendRequest]); | ||
|
||
// Whenever the scheduleRequest() changes (which changes when the pollIntervalMs changes) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From what we discussed yesterday, I've been thinking and it's the first So the end result is
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And this revealed a bug in the test for pollInterval. We need to wait for twice the amount of time to check for the So the test becomes describe('pollIntervalMs', () => {
it('sends another request after the specified time has elapsed', async () => {
const { setupSuccessRequest, advanceTime, getSendRequestSpy } = helpers;
setupSuccessRequest({ pollIntervalMs: REQUEST_TIME });
await advanceTime(REQUEST_TIME);
expect(getSendRequestSpy().callCount).toBe(1);
// We need to advance (1) the pollIntervalMs and (2) the request time
await advanceTime(REQUEST_TIME * 2);
expect(getSendRequestSpy().callCount).toBe(2);
await advanceTime(REQUEST_TIME * 2);
expect(getSendRequestSpy().callCount).toBe(3);
});
}); |
||
// we schedule a new request | ||
useEffect(() => { | ||
sendRequestRef.current!(); | ||
scheduleRequest(); | ||
return cleanUpPollInterval; | ||
}, [scheduleRequest, cleanUpPollInterval]); | ||
|
||
// Whenever the totalRequests state changes | ||
// we schedule a new request | ||
useEffect(() => { | ||
if (isMounted.current === false) { | ||
// Don't schedule on component mount | ||
return; | ||
} | ||
|
||
// 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, query]); | ||
scheduleRequest(); | ||
return cleanUpPollInterval; | ||
}, [totalRequests, scheduleRequest, cleanUpPollInterval]); | ||
|
||
useEffect(() => { | ||
scheduleRequestRef.current!(); | ||
isMounted.current = true; | ||
|
||
// Clean up timeout and mark inflight requests as stale if the poll interval changes. | ||
return () => { | ||
/* eslint-disable-next-line react-hooks/exhaustive-deps */ | ||
isOutdatedRequest = true; | ||
if (pollIntervalIdRef.current) { | ||
clearTimeout(pollIntervalIdRef.current); | ||
} | ||
isMounted.current = false; | ||
// When the component unmounts, clear any existing interval. | ||
cleanUpPollInterval(); | ||
}; | ||
}, [pollIntervalMs]); | ||
}, [cleanUpPollInterval]); | ||
|
||
return { | ||
isInitialRequest, | ||
isLoading, | ||
error, | ||
data, | ||
sendRequest: sendRequestRef.current, // Gives the user the ability to manually request data | ||
sendRequest, // Gives the user the ability to manually request data | ||
}; | ||
}; |
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 copied over your logic here in a "default"
onChange
handler to not make a bigger refactor.