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 };