Refactor use_observable to update state with useReducer (#132703)

Issue: https://github.com/elastic/seceng/issues/3807
Similar issue: https://github.com/elastic/kibana/pull/132069

Why? When calling setState inside an async function, react doesn't automatically batch updates, leading to an inconsistent application state.

Read more: https://github.com/reactwg/react-18/discussions/21
This commit is contained in:
Pablo Machado 2022-05-24 09:57:45 +02:00 committed by GitHub
parent 35a4274048
commit 510b8b2cac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -6,12 +6,36 @@
* Side Public License, v 1.
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useReducer } from 'react';
import { Observable, Subscription } from 'rxjs';
import { useIsMounted } from '../use_is_mounted';
import { Task } from '../types';
interface State<T> {
loading: boolean;
error?: unknown;
result?: T;
}
export type Action<T> =
| { type: 'setResult'; result: T }
| { type: 'setError'; error: unknown }
| { type: 'load' };
const createReducer =
<T>() =>
(state: State<T>, action: Action<T>) => {
switch (action.type) {
case 'setResult':
return { ...state, result: action.result, loading: false };
case 'setError':
return { ...state, error: action.error, loading: false };
case 'load':
return { loading: true, result: undefined, error: undefined };
}
};
/**
*
* @param fn function returning an observable
@ -22,31 +46,30 @@ export const useObservable = <Args extends unknown[], Result>(
fn: (...args: Args) => Observable<Result>
): Task<Args, Result> => {
const isMounted = useIsMounted();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<unknown | undefined>();
const [result, setResult] = useState<Result | undefined>();
const subRef = useRef<Subscription | undefined>();
const reducer = createReducer<Result>();
const [state, dispatch] = useReducer(reducer, {
loading: false,
error: undefined,
result: undefined,
});
const start = useCallback(
(...args: Args) => {
if (subRef.current) {
subRef.current.unsubscribe();
}
setLoading(true);
setResult(undefined);
setError(undefined);
dispatch({ type: 'load' });
subRef.current = fn(...args).subscribe(
(r) => {
if (isMounted()) {
setResult(r);
setLoading(false);
dispatch({ type: 'setResult', result: r });
}
},
(e) => {
if (isMounted()) {
setError(e);
setLoading(false);
dispatch({ type: 'setError', error: e });
}
}
);
@ -64,9 +87,9 @@ export const useObservable = <Args extends unknown[], Result>(
);
return {
error,
loading,
result,
result: state.result,
error: state.error,
loading: state.loading,
start,
};
};