implement state management in redux

This commit is contained in:
David Aaron Suddjian 2021-02-23 15:26:43 -08:00
parent 2c6fd0abb8
commit d64d50a99f
4 changed files with 110 additions and 40 deletions

View File

@ -17,8 +17,10 @@
* under the License. * under the License.
*/ */
import { createContext, useEffect, useMemo, useRef, useState } from 'react';
import { Action, Dispatch } from 'redux';
import { makeApi } from '@superset-ui/core'; import { makeApi } from '@superset-ui/core';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux';
export enum ResourceStatus { export enum ResourceStatus {
LOADING = 'loading', LOADING = 'loading',
@ -32,6 +34,10 @@ export enum ResourceStatus {
*/ */
export type Resource<T> = LoadingState | CompleteState<T> | ErrorState; export type Resource<T> = LoadingState | CompleteState<T> | ErrorState;
export type ReduxState = {
[url: string]: Resource<any>;
};
// Trying out something a little different: a separate type per status. // Trying out something a little different: a separate type per status.
// This should let Typescript know whether a Resource has a result or error. // This should let Typescript know whether a Resource has a result or error.
// It's possible that I'm expecting too much from Typescript here. // It's possible that I'm expecting too much from Typescript here.
@ -61,12 +67,87 @@ type ErrorState = {
error: Error; error: Error;
}; };
const initialState: LoadingState = { const loadingState: LoadingState = {
status: ResourceStatus.LOADING, status: ResourceStatus.LOADING,
result: null, result: null,
error: null, error: null,
}; };
const RESOURCE_FETCH_START = 'RESOURCE_FETCH_START';
const RESOURCE_FETCH_COMPLETE = 'RESOURCE_FETCH_COMPLETE';
const RESOURCE_FETCH_ERROR = 'RESROUCE_FETCH_ERROR';
type FetchStart = {
type: typeof RESOURCE_FETCH_START;
endpoint: string;
};
type FetchComplete<T = any> = {
type: typeof RESOURCE_FETCH_COMPLETE;
endpoint: string;
result: T;
};
type FetchError = {
type: typeof RESOURCE_FETCH_ERROR;
endpoint: string;
error: Error;
};
type ResourceAction = FetchStart | FetchComplete | FetchError;
export type ResourcesState = Record<string, Resource<any>>;
export const initialResourcesState: ResourcesState = {};
export function resourcesReducer(
state: ResourcesState = initialResourcesState,
action: ResourceAction,
): ResourcesState {
switch (action.type) {
case RESOURCE_FETCH_START: {
return {
...state,
[action.endpoint]: {
status: ResourceStatus.LOADING,
result: null,
error: null,
},
};
}
case RESOURCE_FETCH_COMPLETE: {
const { endpoint, result } = action;
return {
...state,
[endpoint]: {
status: ResourceStatus.COMPLETE,
result,
error: null,
},
};
}
case RESOURCE_FETCH_ERROR: {
const { endpoint, error } = action as FetchError;
return {
...state,
[endpoint]: {
status: ResourceStatus.ERROR,
result: null,
error,
},
};
}
default:
return state;
}
}
type ReduxRootState = { apiResources: ResourcesState };
const selectResourceForEndpoint = <RESULT>(endpoint: string) => (
state: ReduxRootState,
): Resource<RESULT> | null => state.apiResources[endpoint] ?? null;
/** /**
* A general-purpose hook to fetch the response from an endpoint. * A general-purpose hook to fetch the response from an endpoint.
* Returns the full response body from the API, including metadata. * Returns the full response body from the API, including metadata.
@ -87,21 +168,19 @@ const initialState: LoadingState = {
export function useApiResourceFullBody<RESULT>( export function useApiResourceFullBody<RESULT>(
endpoint: string, endpoint: string,
): Resource<RESULT> { ): Resource<RESULT> {
const [resource, setResource] = useState<Resource<RESULT>>(initialState); const dispatch = useDispatch<Dispatch<ResourceAction>>();
const cancelRef = useRef<() => void>(() => {}); const resource = useSelector(selectResourceForEndpoint<RESULT>(endpoint));
useEffect(() => { useEffect(() => {
// If refresh is implemented, this will need to change. if (resource != null) {
// The previous values should stay during refresh. // fetching already underway/complete, don't duplicate work.
setResource(initialState); return;
}
// when this effect runs, the endpoint has changed. dispatch({
// cancel any current calls so that state doesn't get messed up. type: RESOURCE_FETCH_START,
cancelRef.current(); endpoint,
let cancelled = false; });
cancelRef.current = () => {
cancelled = true;
};
const fetchResource = makeApi<{}, RESULT>({ const fetchResource = makeApi<{}, RESULT>({
method: 'GET', method: 'GET',
@ -110,31 +189,22 @@ export function useApiResourceFullBody<RESULT>(
fetchResource({}) fetchResource({})
.then(result => { .then(result => {
if (!cancelled) { dispatch({
setResource({ type: RESOURCE_FETCH_COMPLETE,
status: ResourceStatus.COMPLETE, endpoint,
result, result,
error: null, });
});
}
}) })
.catch(error => { .catch(error => {
if (!cancelled) { dispatch({
setResource({ type: RESOURCE_FETCH_ERROR,
status: ResourceStatus.ERROR, endpoint,
result: null, error,
error, });
});
}
}); });
}, [endpoint, resource, dispatch]);
// Cancel the request when the component un-mounts return resource ?? loadingState;
return () => {
cancelled = true;
};
}, [endpoint]);
return resource;
} }
/** /**

View File

@ -17,11 +17,7 @@
* under the License. * under the License.
*/ */
export { export * from './apiResources';
useApiResourceFullBody,
useApiV1Resource,
useTransformedResource,
} from './apiResources';
// A central catalog of API Resource hooks. // A central catalog of API Resource hooks.
// Add new API hooks here, organized under // Add new API hooks here, organized under

View File

@ -47,6 +47,7 @@ import getFilterConfigsFromFormdata from '../util/getFilterConfigsFromFormdata';
import getLocationHash from '../util/getLocationHash'; import getLocationHash from '../util/getLocationHash';
import newComponentFactory from '../util/newComponentFactory'; import newComponentFactory from '../util/newComponentFactory';
import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox'; import { TIME_RANGE } from '../../visualizations/FilterBox/FilterBox';
import { initialResourcesState } from 'src/common/hooks/apiResources/apiResources';
export default function getInitialState(bootstrapData) { export default function getInitialState(bootstrapData) {
const { user_id, datasources, common, editMode, urlParams } = bootstrapData; const { user_id, datasources, common, editMode, urlParams } = bootstrapData;
@ -308,5 +309,6 @@ export default function getInitialState(bootstrapData) {
dashboardLayout, dashboardLayout,
messageToasts: [], messageToasts: [],
impressionId: shortid.generate(), impressionId: shortid.generate(),
apiResources: initialResourcesState,
}; };
} }

View File

@ -18,6 +18,7 @@
*/ */
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { resourcesReducer } from 'src/common/hooks/apiResources';
import charts from '../../chart/chartReducer'; import charts from '../../chart/chartReducer';
import dashboardInfo from './dashboardInfo'; import dashboardInfo from './dashboardInfo';
import dashboardState from './dashboardState'; import dashboardState from './dashboardState';
@ -41,4 +42,5 @@ export default combineReducers({
impressionId, impressionId,
messageToasts, messageToasts,
sliceEntities, sliceEntities,
apiResources: resourcesReducer,
}); });