Skip to content
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

feat: migrate useFetch to react-query #48

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"react": "^17.0.2",
"react-datetime": "^3.0.4",
"react-dom": "^17.0.2",
"react-query": "^3.34.12",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-virtualized-auto-sizer": "^1.0.5",
Expand Down
31 changes: 19 additions & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,28 @@ import {
Route,
Redirect,
} from "react-router-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";

import Home from "./pages/Home";
import theme from "./theme";

const queryClient = new QueryClient();

export const App: React.FC = () => (
<ChakraProvider theme={theme}>
<Router>
<BasicLayout>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Redirect to="/" />
</Switch>
</BasicLayout>
</Router>
</ChakraProvider>
<QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}>
<Router>
<BasicLayout>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Redirect to="/" />
</Switch>
</BasicLayout>
</Router>
</ChakraProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
7 changes: 5 additions & 2 deletions src/components/GraphWrapper/graphWrapper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import { queryEntities } from "../../services/queryEntity";
import constants from "../../utils/constants";
import { useGlobalStore } from "../../store/global";

const { defaultStartTimestamp, defaultStepValue, defaultEndTimestamp } =
constants;
const {
defaultStartTimestamp,
defaultStepValue,
defaultEndTimestamp,
} = constants;

const TestComponent = () => {
const { changeRoute } = useGlobalStore();
Expand Down
20 changes: 13 additions & 7 deletions src/components/GraphWrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ interface PageProps {

const GraphWrapper: React.FC<PageProps> = (props: PageProps) => {
const { selectedRoutePath } = props;
const { selectedStartTimestamp, selectedEndTimestamp, selectedStepValue } =
useTimeQuerierStore();
const {
selectedStartTimestamp,
selectedEndTimestamp,
selectedStepValue,
} = useTimeQuerierStore();

const { data, error, status } = useFetch<apiResponse<queryResponse>>(
const { data, error, isLoading, isFetching, isError } = useFetch<
apiResponse<queryResponse>
>(
"graph",
queryEntities(
selectedRoutePath,
selectedStartTimestamp,
Expand All @@ -32,7 +38,7 @@ const GraphWrapper: React.FC<PageProps> = (props: PageProps) => {
)
);

if (error) {
if (isError) {
return (
<VStack w="95%" h="100%" margin="auto" justifyContent="center">
<Alert
Expand All @@ -47,7 +53,7 @@ const GraphWrapper: React.FC<PageProps> = (props: PageProps) => {
>
<AlertIcon boxSize="40px" mr={0} />
<AlertTitle mt={4} mb={1} fontSize="lg">
{error}
{error?.response?.data}
</AlertTitle>
<AlertDescription maxWidth="sm">
An unexpected error occurred while processing the graph.
Expand All @@ -57,7 +63,7 @@ const GraphWrapper: React.FC<PageProps> = (props: PageProps) => {
);
}

if (status === "fetching" || status === "init")
if (isLoading || isFetching)
return (
<VStack w="95%" h="100%" margin="auto" justifyContent="center">
<CircularProgress size="10vh" isIndeterminate />
Expand Down Expand Up @@ -90,7 +96,7 @@ const GraphWrapper: React.FC<PageProps> = (props: PageProps) => {
return (
<VStack w="95%" h="100%" margin="auto" justifyContent="center">
<Box data-testid="graph" width="100%" height="90vh">
<ReusableGraph graphData={data.data} />
<ReusableGraph graphData={data.data.data} />
</Box>
</VStack>
);
Expand Down
18 changes: 10 additions & 8 deletions src/components/MachineSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,38 @@ import { apiResponse, machineResponse } from "../../utils/types";

const MachineSelector: React.FC = () => {
const { selectedMachine, changeSelectedMachine } = useGlobalStore();
const { data, error, status } = useFetch<apiResponse<machineResponse>>(
getActiveMachines()
);
const machines = data ? data.data.machines : [];
const { data, error, isFetching, isLoading, isError, isFetched } = useFetch<
apiResponse<machineResponse>
>("machines", getActiveMachines());

const machines = data ? data.data.data.machines : [];

useEffect(() => {
if (machines.length) changeSelectedMachine(machines[0]);
}, [data]);

if (error) {
if (isError) {
return (
<Alert data-testid="error-message" fontSize="xs" status="error">
<AlertIcon />
{error}
{error?.response?.data}
</Alert>
);
}

if (status === "fetching" || status === "init") {
if (isFetching || isLoading) {
return (
<VStack w="95%" h="100%" margin="auto" justifyContent="center">
<CircularProgress size="5vh" isIndeterminate />
</VStack>
);
}

return (
<VStack w="100%">
<Select
mt={3}
isDisabled={status != "fetched" || error !== undefined}
isDisabled={!isFetched || isError}
value={selectedMachine ? selectedMachine : "Fetching Machines"}
data-testid="machine-selector"
onChange={(e) => {
Expand Down
18 changes: 10 additions & 8 deletions src/components/RouteSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import { apiResponse, routeResponse } from "../../utils/types";

const RouteSelector: React.FC = () => {
const { selectedMachine } = useGlobalStore();
const { data, error, status } = useFetch<apiResponse<routeResponse[]>>(
getRoutes(selectedMachine ? selectedMachine : "")
);
const routes = data ? data.data : [];
const { data, error, isLoading, isFetching, isError, isFetched } = useFetch<
apiResponse<routeResponse[]>
>("routes", getRoutes(selectedMachine ? selectedMachine : ""));

const routes = data ? data.data.data : [];
const [filteredRoutes, changeFilteredRoutes] = useState<routeResponse[]>([]);

useEffect(() => {
Expand All @@ -21,23 +22,24 @@ const RouteSelector: React.FC = () => {
const handleFilteredRoutesChange = (routes: routeResponse[]) => {
changeFilteredRoutes([...routes]);
};
if (error) {

if (isError) {
return (
<VStack data-testid="route-selector" mt={4} w="100%" h="100%">
<Search
routes={routes}
isDisabled={error !== undefined || status != "fetched"}
isDisabled={isError || !isFetched}
changeFilteredRoutes={handleFilteredRoutesChange}
/>
<Alert data-testid="error-message" fontSize="xs" status="error">
<AlertIcon />
{error}
{error?.response?.data}
</Alert>
</VStack>
);
}

if (status === "fetching" || status === "init")
if (isFetching || isLoading)
return (
<VStack w="95%" h="100%" margin="auto" justifyContent="center">
<CircularProgress size="5vh" isIndeterminate />
Expand Down
7 changes: 2 additions & 5 deletions src/components/RouteSelector/routeSelector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@ import { getRoutes } from "../../services/getRoutes";
import { truncate } from "../../utils/stringManipulation";

jest.mock("axios");
jest.mock(
"react-virtualized-auto-sizer",
() =>
({ children }: any) =>
children({ height: 600, width: 600 })
jest.mock("react-virtualized-auto-sizer", () => ({ children }: any) =>
children({ height: 600, width: 600 })
);

const mockedAxios = axios as jest.Mocked<typeof axios>;
Expand Down
7 changes: 5 additions & 2 deletions src/components/TimeQuerier/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ import { useTimeQuerierStore } from "../../store/timeQuerier";

const TimeQuerier: React.FC = () => {
const styles = useStyleConfig("DateTime", {});
const { selectedStartTimestamp, selectedEndTimestamp, changeTimeQuerier } =
useTimeQuerierStore();
const {
selectedStartTimestamp,
selectedEndTimestamp,
changeTimeQuerier,
} = useTimeQuerierStore();
const { defaultStepValue, minStepValue, dateFormat, timeFormat } = constants;
const [startTime, setStartTime] = useState(moment(selectedStartTimestamp));
const [endTime, setEndTime] = useState(moment(selectedEndTimestamp));
Expand Down
7 changes: 3 additions & 4 deletions src/theme/components/DateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ export const DateTime = {
},
".rdtPicker td.rdtDay:hover,.rdtPicker thead tr:first-of-type th:hover\
,.rdtPicker .rdtTimeToggle:hover,.rdtCounter .rdtBtn:hover\
,td.rdtMonth:hover, td.rdtYear:hover,.rdtSwitch:hover":
{
bg: mode("#eee", "darkPrimary")(props),
},
,td.rdtMonth:hover, td.rdtYear:hover,.rdtSwitch:hover": {
bg: mode("#eee", "darkPrimary")(props),
},
".rdtPicker td.rdtDisabled:hover": {
bg: "none",
},
Expand Down
9 changes: 8 additions & 1 deletion src/utils/customRender.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from "react";
import { render, RenderOptions } from "@testing-library/react";
import { ChakraProvider } from "@chakra-ui/react";
import { QueryClient, QueryClientProvider } from "react-query";

import theme from "../theme";

/* Function used to render the component that we need to test
Expand All @@ -9,8 +11,13 @@ import theme from "../theme";
input : component that is tested
output : component after being wrapped by all the providers
*/

const queryClient = new QueryClient();

const AllProviders = ({ children }: { children?: React.ReactNode }) => (
<ChakraProvider theme={theme}>{children}</ChakraProvider>
<QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}>{children}</ChakraProvider>
</QueryClientProvider>
);

const customRender = (ui: React.ReactElement, options?: RenderOptions) =>
Expand Down
100 changes: 9 additions & 91 deletions src/utils/useFetch.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,19 @@
/* eslint-disable */
import { useEffect, useReducer, useRef } from "react";
import axios, { AxiosRequestConfig } from "axios";

interface Response<T> {
status: "init" | "fetching" | "error" | "fetched";
data?: T | undefined;
error?: string | undefined;
}

// used if you want to reference
// persistent storage
interface Cache<T> {
[url: string]: T;
}

type Action<T> =
| { type: "request" }
| { type: "success"; payload: T }
| { type: "failure"; payload: string };
import axios, { AxiosRequestConfig, AxiosError, AxiosResponse } from "axios";
import { useQuery } from "react-query";

/* Reusable fetch service for making requests to api endpoints
input - url,options
output - data,error,status
input - uniqueName, url, options
output - data, error, status
*/
const useFetch = <T = unknown>(
uniqueName: string,
url: string,
options?: AxiosRequestConfig
): Response<T> => {
const cache = useRef<Cache<T>>({});
const cancelRequest = useRef<boolean>(false);

const initialState: Response<T> = {
status: "init",
error: undefined,
data: undefined,
};

// main function
const fetchReducer = (state: Response<T>, action: Action<T>): Response<T> => {
switch (action.type) {
case "request":
return { ...initialState, status: "fetching" };
case "success":
return { ...initialState, status: "fetched", data: action.payload };
case "failure":
return { ...initialState, status: "error", error: action.payload };
default:
return state;
}
};

// useReducer instead of use state could make the function
// easier to debug across multiple usages.
const [state, dispatch] = useReducer(fetchReducer, initialState);
useEffect(() => {
if (!url) {
return;
}

const fetchData = async () => {
dispatch({ type: "request" });

if (cache.current[url]) {
dispatch({ type: "success", payload: cache.current[url] });
} else {
try {
const response = options
? await axios.get(url, options)
: await axios.get(url);

cache.current[url] = response.data;

if (cancelRequest.current) return;

dispatch({ type: "success", payload: response.data });
} catch (error) {
// Let's keep error handling to another day,
// hence the type for error is "any" here.
if (cancelRequest.current) return;

// server should return a message, else use a custom message such as "something went wrong"
dispatch({ type: "failure", payload: error.message });
}
}
};

fetchData();
}, [url]);

useEffect(() => {
// runs when component is destroyed.
return () => {
cancelRequest.current = true;
};
}, []);

return state;
) => {
return useQuery<AxiosResponse<T>, AxiosError>(uniqueName, () =>
options ? axios.get(url, options) : axios.get(url)
);
};

export default useFetch;
Loading