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.
*/
import { createContext, useEffect, useMemo, useRef, useState } from 'react';
import { Action, Dispatch } from 'redux';
import { makeApi } from '@superset-ui/core';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
export enum ResourceStatus {
LOADING = 'loading',
@ -32,6 +34,10 @@ export enum ResourceStatus {
*/
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.
// 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.
@ -61,12 +67,87 @@ type ErrorState = {
error: Error;
};
const initialState: LoadingState = {
const loadingState: LoadingState = {
status: ResourceStatus.LOADING,
result: 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.
* Returns the full response body from the API, including metadata.
@ -87,21 +168,19 @@ const initialState: LoadingState = {
export function useApiResourceFullBody<RESULT>(
endpoint: string,
): Resource<RESULT> {
const [resource, setResource] = useState<Resource<RESULT>>(initialState);
const cancelRef = useRef<() => void>(() => {});
const dispatch = useDispatch<Dispatch<ResourceAction>>();
const resource = useSelector(selectResourceForEndpoint<RESULT>(endpoint));
useEffect(() => {
// If refresh is implemented, this will need to change.
// The previous values should stay during refresh.
setResource(initialState);
if (resource != null) {
// fetching already underway/complete, don't duplicate work.
return;
}
// when this effect runs, the endpoint has changed.
// cancel any current calls so that state doesn't get messed up.
cancelRef.current();
let cancelled = false;
cancelRef.current = () => {
cancelled = true;
};
dispatch({
type: RESOURCE_FETCH_START,
endpoint,
});
const fetchResource = makeApi<{}, RESULT>({
method: 'GET',
@ -110,31 +189,22 @@ export function useApiResourceFullBody<RESULT>(
fetchResource({})
.then(result => {
if (!cancelled) {
setResource({
status: ResourceStatus.COMPLETE,
result,
error: null,
});
}
dispatch({
type: RESOURCE_FETCH_COMPLETE,
endpoint,
result,
});
})
.catch(error => {
if (!cancelled) {
setResource({
status: ResourceStatus.ERROR,
result: null,
error,
});
}
dispatch({
type: RESOURCE_FETCH_ERROR,
endpoint,
error,
});
});
}, [endpoint, resource, dispatch]);
// Cancel the request when the component un-mounts
return () => {
cancelled = true;
};
}, [endpoint]);
return resource;
return resource ?? loadingState;
}
/**

View File

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

View File

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

View File

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