diff --git a/src/App.tsx b/src/App.tsx
index ac4466e..52be3b1 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,24 +1,20 @@
import { Container, CssBaseline } from "@mui/material";
-import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import NavigationBar from "./components/NavigationBar";
import LazyRoutes from "./LazyRoutes";
-import { reduxStore } from "./store/configureStore";
function App() {
return (
-
-
-
- <>
-
-
-
-
- >
-
-
-
+
+
+ <>
+
+
+
+
+ >
+
+
);
}
diff --git a/src/components/FormSubmission.tsx b/src/components/FormSubmission.tsx
index 9a105e2..08c0e2a 100644
--- a/src/components/FormSubmission.tsx
+++ b/src/components/FormSubmission.tsx
@@ -1,7 +1,6 @@
import { Formik } from "formik";
import * as yup from "yup";
import SharedForm from "../components/SharedForm";
-import { useAppDispatch } from "../store/configureStore";
type Props = {
handleCreateAction: (values: any) => any;
@@ -9,7 +8,6 @@ type Props = {
};
const FormSubmission = ({ handleCreateAction, hasDispatch = false }: Props) => {
- const dispatch = useAppDispatch();
return (
{
})}
onSubmit={(values, actions) => {
if (hasDispatch) {
- dispatch(handleCreateAction(values));
} else {
handleCreateAction(values);
}
diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx
index eaae678..099473a 100644
--- a/src/components/NavigationBar.tsx
+++ b/src/components/NavigationBar.tsx
@@ -3,14 +3,16 @@ import { createStyles, makeStyles } from "@mui/styles";
import { useNavigate } from "react-router-dom";
import { pathNames } from "../LazyRoutes";
import TotalOfCharacters from "./TotalOfCharacters";
-import { useAppSelector } from "../store/configureStore";
-import { useFetchVillainsQuery } from "../features/villains/query";
+import { useState } from "react";
const NavigationBar = () => {
const navigate = useNavigate();
const classes = useStyles();
- const { heroes } = useAppSelector((state) => state.hero);
- const { data: villains = [] } = useFetchVillainsQuery();
+
+ // TODO: use Redux to replace the heroes and villains
+ const [heroes, setHeroes] = useState([]);
+ const [villains, setVillains] = useState([]);
+
return (
diff --git a/src/components/tests/NavigationBar.test.tsx b/src/components/tests/NavigationBar.test.tsx
deleted file mode 100644
index 4cae121..0000000
--- a/src/components/tests/NavigationBar.test.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import HomePage from "../../pages/HomePage";
-import { render, screen } from "../../test-utils/testing-library-utils";
-
-it("Navigation menu is present", () => {
- render();
-
- const title = screen.getByTestId("home-title");
- expect(title).toBeInTheDocument();
- expect(title).toHaveTextContent(/welcome/i);
-});
diff --git a/src/features/heroes/heroAsyncActions.ts b/src/features/heroes/heroAsyncActions.ts
deleted file mode 100644
index 7e20865..0000000
--- a/src/features/heroes/heroAsyncActions.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { createAsyncThunk } from "@reduxjs/toolkit";
-import { EndPoints } from "../../axios/api-config";
-import {
- deleteAxios,
- getAxios,
- postAxios,
-} from "../../axios/generic-api-calls";
-import { HeroActionTypes, HeroModel } from "./heroTypes";
-
-export const getHeroesAction = createAsyncThunk(
- HeroActionTypes.FETCH_HEROES,
- async () => {
- // HTTP CALLS
- const response = await getAxios(EndPoints.heroes);
- // Return the response
- return response.data; // payload
- }
-);
-
-export const deleteHeroAction = createAsyncThunk(
- HeroActionTypes.REMOVE_HERO_BY_ID,
- async (id: string) => {
- return await deleteAxios(EndPoints.heroes, id);
- }
-);
-
-export const postHeroAction = createAsyncThunk(
- HeroActionTypes.ADD_HERO,
- async (hero: HeroModel) => {
- const { data } = await postAxios(EndPoints.heroes, hero);
-
- return data;
- }
-);
diff --git a/src/features/heroes/heroSlice.ts b/src/features/heroes/heroSlice.ts
deleted file mode 100644
index 3b170b8..0000000
--- a/src/features/heroes/heroSlice.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-import {
- getHeroesAction,
- deleteHeroAction,
- postHeroAction,
-} from "./heroAsyncActions";
-import { HeroModel, heroNamespace, HeroStateType } from "./heroTypes";
-
-// hero state
-export const initialState: HeroStateType = {
- hero: {} as HeroModel,
- heroes: [] as HeroModel[],
- loading: false,
-};
-
-// hero store
-export const heroSlice = createSlice({
- name: heroNamespace,
- initialState: initialState,
-
- // mutation using synchronous actions or without side effects
- reducers: {
- triggerLoading: (state, action: PayloadAction) => {
- state.loading = action.payload;
- },
- removeHeroFromStore: (state, action: PayloadAction) => {
- state.heroes = state.heroes.filter((h) => h.id !== action.payload); // payload is the id of the object
- },
- saveHeroList: (state, action: PayloadAction) => {
- state.heroes = action.payload;
- },
- },
-
- /*
- * mutation using asynchronous actions or with side effects.
- * INFO: NOT a requirements for redux-toolkit
- * ALTERNATIVE: fetching data from API then dispatch a synchronous action
- * The alternative is not an anti-pattern. This can easily be understood by developers new to React Reduxq
- * */
- extraReducers: (builder) => {
- builder.addCase(getHeroesAction.pending, (state, action) => {
- state.loading = true;
- });
- builder.addCase(getHeroesAction.fulfilled, (state, action) => {
- state.heroes = action.payload;
- state.loading = false;
- });
- builder.addCase(getHeroesAction.rejected, (state, action: any) => {
- console.log(action.error);
- state.loading = false;
- });
-
- // DELETE - optimistic update
- builder.addCase(deleteHeroAction.pending, (state, action) => {
- state.tempData = [...state.heroes]; // for rolling back
-
- const index = state.heroes.findIndex((h) => h.id === action.meta.arg);
- state.heroes.splice(index, 1);
- });
-
- builder.addCase(deleteHeroAction.rejected, (state, action: any) => {
- state.heroes = state.tempData as HeroModel[];
- });
-
- builder.addCase(postHeroAction.pending, (state, action) => {
- state.loading = true;
- });
-
- builder.addCase(postHeroAction.fulfilled, (state, action) => {
- state.heroes.push(action.payload);
- state.loading = false;
- });
-
- builder.addCase(postHeroAction.rejected, (state, action) => {
- console.log(action.error);
- state.loading = false;
- });
- },
-});
-
-// non-async actions
-export const { removeHeroFromStore, triggerLoading, saveHeroList } =
- heroSlice.actions;
diff --git a/src/features/heroes/heroTypes.ts b/src/features/heroes/heroTypes.ts
deleted file mode 100644
index dde370b..0000000
--- a/src/features/heroes/heroTypes.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-export type HeroStateType = {
- readonly heroes: HeroModel[];
- readonly hero: HeroModel;
- readonly loading: boolean;
-
- readonly tempData?: any;
-};
-
-export type ApiResponse = Record;
-
-export type HeroModel = {
- id: string;
- firstName: string;
- lastName: string;
- house: string;
- knownAs: string;
-} & ApiResponse;
-
-// action types
-export const heroNamespace = "hero";
-
-export const HeroActionTypes = {
- FETCH_HEROES: `${heroNamespace}/FETCH_HEROES`,
- REMOVE_HERO_BY_ID: `${heroNamespace}/REMOVE_HERO_BY_ID`,
- ADD_HERO: `${heroNamespace}/ADD_HERO`,
-};
diff --git a/src/features/heroes/tests/heroDispatch.test.tsx b/src/features/heroes/tests/heroDispatch.test.tsx
deleted file mode 100644
index f937d93..0000000
--- a/src/features/heroes/tests/heroDispatch.test.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { deleteHeroAction, getHeroesAction } from "../heroAsyncActions";
-import { HeroStateType } from "../heroTypes";
-import { reduxStore } from "../../../store/configureStore";
-
-describe("HeroesPage dispatch", () => {
- let state: HeroStateType;
-
- beforeEach(() => {
- //
- });
-
- /* Select the store.getState().hero again
- * before running another expect. It's just how it is */
-
- it("should dispatch getHeroesAction", async () => {
- await reduxStore.dispatch(getHeroesAction());
- state = reduxStore.getState().hero;
-
- expect(state.heroes).toHaveLength(2);
- });
-
- test("should dispatch deleteHeroById with HTTP request", async () => {
- await reduxStore.dispatch(deleteHeroAction(state.heroes[0].id));
- state = reduxStore.getState().hero;
- expect(state.heroes).toHaveLength(1);
- });
-});
diff --git a/src/features/villains/query.ts b/src/features/villains/query.ts
deleted file mode 100644
index 25074b1..0000000
--- a/src/features/villains/query.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
-
-export type VillainModel = {
- id: string;
- firstName: string;
- lastName: string;
- house: string;
- knownAs: string;
-};
-
-export const villainSlice = createApi({
- // where to keep the data in the reducers
- reducerPath: "villain",
- // fetchBaseQuery is a built-in fetch wrapper
- baseQuery: fetchBaseQuery({
- baseUrl: "/api/",
- prepareHeaders(headers) {
- headers.set("x-auth", "Bearer ...");
- return headers;
- },
- }),
- tagTypes: ["villains"],
- endpoints(builder) {
- return {
- fetchVillains: builder.query({
- query: () => `/villains`,
- }),
- addVillain: builder.mutation>({
- query: (req) => ({
- url: `/villains`,
- method: "POST",
- body: req,
- providesTags: ["villain"],
- }),
- }),
- removeVillain: builder.mutation({
- query: (id) => ({
- url: `/villains/${id}`,
- method: "DELETE",
- }),
- invalidatesTags: ["villains"],
- }),
- };
- },
-});
-
-// auto generated react hooks
-export const {
- useFetchVillainsQuery,
- useAddVillainMutation,
- useRemoveVillainMutation,
-} = villainSlice;
diff --git a/src/pages/HeroesPage.tsx b/src/pages/HeroesPage.tsx
index dfcb303..602fba5 100644
--- a/src/pages/HeroesPage.tsx
+++ b/src/pages/HeroesPage.tsx
@@ -5,24 +5,13 @@ import FormSubmission from "../components/FormSubmission";
import TitleBar from "../components/TitleBar";
import UpdateUiLabel from "../components/UpdateUiLabel";
-import {
- deleteHeroAction,
- getHeroesAction,
- postHeroAction,
-} from "../features/heroes/heroAsyncActions";
-import {
- saveHeroList,
- removeHeroFromStore,
- triggerLoading,
-} from "../features/heroes/heroSlice";
-import { useAppDispatch, useAppSelector } from "../store/configureStore";
import { deleteAxios, getAxios } from "../axios/generic-api-calls";
-import { HeroModel } from "../features/heroes/heroTypes";
import { EndPoints } from "../axios/api-config";
const HeroesPage = () => {
- const dispatch = useAppDispatch();
- const { heroes, loading } = useAppSelector((state) => state.hero);
+ // TODO: use Redux to replace the heroes and loading
+ const [heroes, setHeroes] = useState([]);
+ const [loading, setLoading] = useState(false);
const smallScreen = useMediaQuery("(max-width:600px)");
const classes = useStyles();
@@ -30,85 +19,14 @@ const HeroesPage = () => {
// local state
const [counter, setCounter] = useState("0");
- useEffect(() => {
- dispatch(getHeroesAction());
- // handleGetHeroes();
- // handleFetchHeroes();
- }, [dispatch]);
-
- /*
- * IF NO heroAsyncActions.ts and extraReducers
- * Can avoid race condition issue
- * Can be used with multiple HTTP request
- * Can be used with states that don't belong in the store
- * Can easily be understood by developers who are new to React Redux
- * Easy to reason about
- * Easy to do optimistic updates
- * */
- const handleGetHeroes = async () => {
- dispatch(triggerLoading(true));
- try {
- const { data } = await getAxios(EndPoints.heroes);
- dispatch(saveHeroList(data));
- // another HTTP request that requires the data above
- } catch (e: any) {
- alert(e.message);
- } finally {
- dispatch(triggerLoading(false));
- }
- };
-
- /*
- * IF NO heroAsyncActions.ts and extraReducers
- * Can avoid race condition issue
- * Can be used with multiple HTTP request
- * Can be used with states that don't belong in the store
- * Can easily be understood by developers who are new to React Redux
- * Easy to reason about
- * Easy to do optimistic updates
- * */
- const handleFetchHeroes = () => {
- dispatch(triggerLoading(true));
- fetch(EndPoints.heroes)
- .then((response) => {
- response.json().then((data: HeroModel[]) => {
- dispatch(saveHeroList(data));
- });
- })
- .catch((e: any) => {
- alert(e.message);
- })
- .finally(() => {
- dispatch(triggerLoading(false));
- });
- };
-
- /*
- * IF NO heroAsyncActions.ts and extraReducers
- * Can avoid race condition issue
- * Can be used with multiple HTTP request
- * Can be used with states that don't belong in the store
- * Can easily be understood by developers who are new to React Redux
- * Easy to reason about
- * Easy to do optimistic updates
- * */
- const handleDeleteHero = async (id: string) => {
- const previousHeroes = heroes;
- dispatch(removeHeroFromStore(id)); // optimistic update
- try {
- await deleteAxios(EndPoints.heroes, id);
- } catch (e: any) {
- alert(e.message);
- dispatch(saveHeroList(previousHeroes));
- }
- };
+ useEffect(() => {}, []);
return (
-
+
{}} />
<>
{heroes.map((h) => (
@@ -138,7 +56,7 @@ const HeroesPage = () => {
variant={"contained"}
color={"secondary"}
data-testid={"remove-button"}
- onClick={() => dispatch(removeHeroFromStore(h.id))}
+ onClick={() => {}}
>
Remove
{" "}
@@ -147,10 +65,7 @@ const HeroesPage = () => {
variant={"outlined"}
color={"secondary"}
data-testid={"delete-button"}
- onClick={async () => {
- dispatch(deleteHeroAction(h.id));
- // await handleDeleteHero(h.id);
- }}
+ onClick={async () => {}}
>
DELETE in DB
@@ -164,7 +79,7 @@ const HeroesPage = () => {
className={classes.button}
variant={"contained"}
color={"primary"}
- onClick={() => dispatch(getHeroesAction())}
+ onClick={() => {}}
>
Re-fetch
diff --git a/src/pages/VillainsPage.tsx b/src/pages/VillainsPage.tsx
index a189304..ecf5034 100644
--- a/src/pages/VillainsPage.tsx
+++ b/src/pages/VillainsPage.tsx
@@ -1,41 +1,18 @@
import React, { useState } from "react";
import { Box, Button, Typography, useMediaQuery } from "@mui/material";
-import {
- useAddVillainMutation,
- useFetchVillainsQuery,
- useRemoveVillainMutation,
- VillainModel,
- villainSlice,
-} from "../features/villains/query";
import { createStyles, makeStyles } from "@mui/styles";
import TitleBar from "../components/TitleBar";
import UpdateUiLabel from "../components/UpdateUiLabel";
import FormSubmission from "../components/FormSubmission";
-import { useAppDispatch } from "../store/configureStore";
const VillainsPage = () => {
- const dispatch = useAppDispatch();
-
// local state
const [counter, setCounter] = useState("0");
- const { data = [], isFetching, refetch } = useFetchVillainsQuery();
- const [addVillain] = useAddVillainMutation();
- const [removeVillain] = useRemoveVillainMutation();
+ // TODO: use Redux to replace the data and isFetching
+ const [data, setData] = useState([]);
+ const [isFetching, setIsFetching] = useState(false);
- const softDeleteVillain = (id: string) => {
- dispatch(
- villainSlice.util.updateQueryData(
- "fetchVillains",
- undefined,
- (draft: VillainModel[]) => {
- const index = draft.findIndex((v) => v.id == id);
- draft.splice(index, 1);
- }
- )
- );
- dispatch(villainSlice.util.invalidateTags(["villains"]));
- };
const smallScreen = useMediaQuery("(max-width:600px)");
const classes = useStyles();
return (
@@ -43,7 +20,7 @@ const VillainsPage = () => {
-
+ {}} />
<>
{data.map((v) => (
@@ -73,7 +50,7 @@ const VillainsPage = () => {
variant={"contained"}
color={"secondary"}
data-testid={"remove-button"}
- onClick={() => softDeleteVillain(v.id)}
+ onClick={() => {}}
>
Remove
{" "}
@@ -82,9 +59,7 @@ const VillainsPage = () => {
variant={"outlined"}
color={"secondary"}
data-testid={"delete-button"}
- onClick={async () => {
- await removeVillain(v.id);
- }}
+ onClick={async () => {}}
>
DELETE in DB
@@ -98,7 +73,7 @@ const VillainsPage = () => {
className={classes.button}
variant={"contained"}
color={"primary"}
- onClick={refetch}
+ onClick={() => {}}
>
Re-fetch
diff --git a/src/pages/tests/HeroesPage.test.tsx b/src/pages/tests/HeroesPage.test.tsx
deleted file mode 100644
index 4428723..0000000
--- a/src/pages/tests/HeroesPage.test.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import userEvent from "@testing-library/user-event";
-import { render, screen } from "../../test-utils/testing-library-utils";
-import HeroesPage from "../HeroesPage";
-
-describe("Heroes Page", () => {
- it("should render title", () => {
- render();
-
- const title = screen.getByTestId("title-page");
- expect(title).toBeInTheDocument();
- });
-
- it("should render loading message", () => {
- render();
-
- const loading = screen.getByTestId("title-page");
- expect(loading).toHaveTextContent(/loading.. please wait../i);
- });
-
- it("should mark a hero", async () => {
- render();
-
- const buttons = await screen.findAllByTestId("mark-button");
- expect(buttons).toHaveLength(2);
-
- await userEvent.click(buttons[0]);
- const cards = await screen.findAllByTestId("card");
- expect(cards[0]).toHaveTextContent("marked");
- });
-
- it("should remove a hero from the store", async () => {
- render();
-
- const buttons = await screen.findAllByTestId("remove-button");
- await userEvent.click(buttons[0]);
- expect(screen.getByTestId("total-heroes")).toHaveTextContent("1");
- });
-
- it("should remove a hero from the store and database", async () => {
- render();
-
- const buttons = await screen.findAllByTestId("delete-button");
- await userEvent.click(buttons[0]);
- expect(screen.getByTestId("total-heroes")).toHaveTextContent("1");
- });
-});
diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts
deleted file mode 100644
index d5eaf8b..0000000
--- a/src/store/configureStore.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { configureStore } from "@reduxjs/toolkit";
-import { save, load } from "redux-localstorage-simple";
-import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
-import { heroSlice } from "../features/heroes/heroSlice";
-import { villainSlice } from "../features/villains/query";
-
-const reduxStore = configureStore({
- preloadedState: load(),
-
- reducer: {
- // rtk
- hero: heroSlice.reducer,
- // rtk query
- [villainSlice.reducerPath]: villainSlice.reducer,
- },
-
- middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware({
- serializableCheck: false,
- })
- .concat(save({ ignoreStates: ["villain"] }))
- .concat(villainSlice.middleware), // for query caching
-
- devTools:
- process.env.NODE_ENV !== "production" || process.env.PUBLIC_URL.length > 0,
-});
-
-export type RootState = ReturnType;
-export type AppDispatch = typeof reduxStore.dispatch;
-
-// to know the right types for dispatch
-const useAppDispatch = () => useDispatch();
-// to know the right types for state
-const useAppSelector: TypedUseSelectorHook = useSelector;
-
-export { reduxStore, useAppDispatch, useAppSelector };
diff --git a/src/test-utils/testing-library-utils.tsx b/src/test-utils/testing-library-utils.tsx
deleted file mode 100644
index fc9d583..0000000
--- a/src/test-utils/testing-library-utils.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { ReactElement, ReactNode } from "react";
-import {
- render as rtlRender,
- RenderOptions,
- RenderResult,
-} from "@testing-library/react";
-import { Provider } from "react-redux";
-import { BrowserRouter } from "react-router-dom";
-import { EnhancedStore } from "@reduxjs/toolkit";
-import { Container, CssBaseline } from "@mui/material";
-
-import NavigationBar from "../components/NavigationBar";
-import { reduxStore } from "../store/configureStore";
-
-type ReduxRenderOptions = {
- store?: EnhancedStore;
- renderOptions?: Omit;
-};
-
-const render = (
- ui: ReactElement,
- { store = reduxStore, ...renderOptions }: ReduxRenderOptions = {}
-): RenderResult => {
- function Wrapper({ children }: { children?: ReactNode }): ReactElement {
- return (
-
-
-
-
- {children}
-
-
-
- );
- }
- return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
-};
-
-// re-export everything
-export * from "@testing-library/react";
-
-// override render method
-export { render };