diff --git a/.gitignore b/.gitignore index a547bf3..628b921 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Custom +reduxDemo \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 18b9c97..f3569fa 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,4 +1,4 @@ { - "semi": false, + "semi": true, "arrowParens": "avoid" } diff --git a/src/App.tsx b/src/App.tsx index 6e729fb..aa21485 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,7 @@ -import "./App.css" -import { Counter } from "./features/counter/Counter" -import { Quotes } from "./features/quotes/Quotes" -import logo from "./logo.svg" +import "src/App.css"; const App = () => { - return ( -
-
- logo - -

- Edit src/App.tsx and save to reload. -

- -
-
- ) -} + return
App
; +}; -export default App +export default App; diff --git a/src/app/createAppSlice.ts b/src/app/createAppSlice.ts deleted file mode 100644 index 64afebb..0000000 --- a/src/app/createAppSlice.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit" - -// `buildCreateSlice` allows us to create a slice with async thunks. -export const createAppSlice = buildCreateSlice({ - creators: { asyncThunk: asyncThunkCreator }, -}) diff --git a/src/app/store.ts b/src/app/store.ts deleted file mode 100644 index 9de8802..0000000 --- a/src/app/store.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Action, ThunkAction } from "@reduxjs/toolkit" -import { combineSlices, configureStore } from "@reduxjs/toolkit" -import { setupListeners } from "@reduxjs/toolkit/query" -import { counterSlice } from "../features/counter/counterSlice" -import { quotesApiSlice } from "../features/quotes/quotesApiSlice" - -// `combineSlices` automatically combines the reducers using -// their `reducerPath`s, therefore we no longer need to call `combineReducers`. -const rootReducer = combineSlices(counterSlice, quotesApiSlice) -// Infer the `RootState` type from the root reducer -export type RootState = ReturnType - -// The store setup is wrapped in `makeStore` to allow reuse -// when setting up tests that need the same store config -export const makeStore = (preloadedState?: Partial) => { - const store = configureStore({ - reducer: rootReducer, - // Adding the api middleware enables caching, invalidation, polling, - // and other useful features of `rtk-query`. - middleware: getDefaultMiddleware => { - return getDefaultMiddleware().concat(quotesApiSlice.middleware) - }, - preloadedState, - }) - // configure listeners using the provided defaults - // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors - setupListeners(store.dispatch) - return store -} - -export const store = makeStore() - -// Infer the type of `store` -export type AppStore = typeof store -// Infer the `AppDispatch` type from the store itself -export type AppDispatch = AppStore["dispatch"] -export type AppThunk = ThunkAction< - ThunkReturnType, - RootState, - unknown, - Action -> diff --git a/src/features/counter/Counter.module.css b/src/features/counter/Counter.module.css deleted file mode 100644 index a0e619d..0000000 --- a/src/features/counter/Counter.module.css +++ /dev/null @@ -1,81 +0,0 @@ -.row { - display: flex; - align-items: center; - justify-content: center; -} - -.row > button { - margin-left: 4px; - margin-right: 8px; -} - -.row:not(:last-child) { - margin-bottom: 16px; -} - -.value { - font-size: 78px; - padding-left: 16px; - padding-right: 16px; - margin-top: 2px; - font-family: "Courier New", Courier, monospace; -} - -.button { - appearance: none; - background: none; - font-size: 32px; - padding-left: 12px; - padding-right: 12px; - outline: none; - border: 2px solid transparent; - color: rgb(112, 76, 182); - padding-bottom: 4px; - cursor: pointer; - background-color: rgba(112, 76, 182, 0.1); - border-radius: 2px; - transition: all 0.15s; -} - -.textbox { - font-size: 32px; - padding: 2px; - width: 64px; - text-align: center; - margin-right: 4px; -} - -.button:hover, -.button:focus { - border: 2px solid rgba(112, 76, 182, 0.4); -} - -.button:active { - background-color: rgba(112, 76, 182, 0.2); -} - -.asyncButton { - composes: button; - position: relative; -} - -.asyncButton:after { - content: ""; - background-color: rgba(112, 76, 182, 0.15); - display: block; - position: absolute; - width: 100%; - height: 100%; - left: 0; - top: 0; - opacity: 0; - transition: - width 1s linear, - opacity 0.5s ease 1s; -} - -.asyncButton:active:after { - width: 0%; - opacity: 1; - transition: 0s; -} diff --git a/src/features/counter/Counter.tsx b/src/features/counter/Counter.tsx deleted file mode 100644 index a286d80..0000000 --- a/src/features/counter/Counter.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useState } from "react" - -import { useAppDispatch, useAppSelector } from "../../app/hooks" -import styles from "./Counter.module.css" -import { - decrement, - increment, - incrementAsync, - incrementByAmount, - incrementIfOdd, - selectCount, - selectStatus, -} from "./counterSlice" - -export const Counter = () => { - const dispatch = useAppDispatch() - const count = useAppSelector(selectCount) - const status = useAppSelector(selectStatus) - const [incrementAmount, setIncrementAmount] = useState("2") - - const incrementValue = Number(incrementAmount) || 0 - - return ( -
-
- - - {count} - - -
-
- { - setIncrementAmount(e.target.value) - }} - /> - - - -
-
- ) -} diff --git a/src/features/counter/counterAPI.ts b/src/features/counter/counterAPI.ts deleted file mode 100644 index aca3ef6..0000000 --- a/src/features/counter/counterAPI.ts +++ /dev/null @@ -1,6 +0,0 @@ -// A mock function to mimic making an async request for data -export const fetchCount = (amount = 1) => { - return new Promise<{ data: number }>(resolve => - setTimeout(() => resolve({ data: amount }), 500), - ) -} diff --git a/src/features/counter/counterSlice.test.ts b/src/features/counter/counterSlice.test.ts deleted file mode 100644 index 12eafe1..0000000 --- a/src/features/counter/counterSlice.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { AppStore } from "../../app/store" -import { makeStore } from "../../app/store" -import type { CounterSliceState } from "./counterSlice" -import { - counterSlice, - decrement, - increment, - incrementByAmount, - selectCount, -} from "./counterSlice" - -interface LocalTestContext { - store: AppStore -} - -describe("counter reducer", it => { - beforeEach(context => { - const initialState: CounterSliceState = { - value: 3, - status: "idle", - } - - const store = makeStore({ counter: initialState }) - - context.store = store - }) - - it("should handle initial state", () => { - expect(counterSlice.reducer(undefined, { type: "unknown" })).toStrictEqual({ - value: 0, - status: "idle", - }) - }) - - it("should handle increment", ({ store }) => { - expect(selectCount(store.getState())).toBe(3) - - store.dispatch(increment()) - - expect(selectCount(store.getState())).toBe(4) - }) - - it("should handle decrement", ({ store }) => { - expect(selectCount(store.getState())).toBe(3) - - store.dispatch(decrement()) - - expect(selectCount(store.getState())).toBe(2) - }) - - it("should handle incrementByAmount", ({ store }) => { - expect(selectCount(store.getState())).toBe(3) - - store.dispatch(incrementByAmount(2)) - - expect(selectCount(store.getState())).toBe(5) - }) -}) diff --git a/src/features/counter/counterSlice.ts b/src/features/counter/counterSlice.ts deleted file mode 100644 index 07bc1f5..0000000 --- a/src/features/counter/counterSlice.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { PayloadAction } from "@reduxjs/toolkit" -import { createAppSlice } from "../../app/createAppSlice" -import type { AppThunk } from "../../app/store" -import { fetchCount } from "./counterAPI" - -export interface CounterSliceState { - value: number - status: "idle" | "loading" | "failed" -} - -const initialState: CounterSliceState = { - value: 0, - status: "idle", -} - -// If you are not using async thunks you can use the standalone `createSlice`. -export const counterSlice = createAppSlice({ - name: "counter", - // `createSlice` will infer the state type from the `initialState` argument - initialState, - // The `reducers` field lets us define reducers and generate associated actions - reducers: create => ({ - increment: create.reducer(state => { - // Redux Toolkit allows us to write "mutating" logic in reducers. It - // doesn't actually mutate the state because it uses the Immer library, - // which detects changes to a "draft state" and produces a brand new - // immutable state based off those changes - state.value += 1 - }), - decrement: create.reducer(state => { - state.value -= 1 - }), - // Use the `PayloadAction` type to declare the contents of `action.payload` - incrementByAmount: create.reducer( - (state, action: PayloadAction) => { - state.value += action.payload - }, - ), - // The function below is called a thunk and allows us to perform async logic. It - // can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This - // will call the thunk with the `dispatch` function as the first argument. Async - // code can then be executed and other actions can be dispatched. Thunks are - // typically used to make async requests. - incrementAsync: create.asyncThunk( - async (amount: number) => { - const response = await fetchCount(amount) - // The value we return becomes the `fulfilled` action payload - return response.data - }, - { - pending: state => { - state.status = "loading" - }, - fulfilled: (state, action) => { - state.status = "idle" - state.value += action.payload - }, - rejected: state => { - state.status = "failed" - }, - }, - ), - }), - // You can define your selectors here. These selectors receive the slice - // state as their first argument. - selectors: { - selectCount: counter => counter.value, - selectStatus: counter => counter.status, - }, -}) - -// Action creators are generated for each case reducer function. -export const { decrement, increment, incrementByAmount, incrementAsync } = - counterSlice.actions - -// Selectors returned by `slice.selectors` take the root state as their first argument. -export const { selectCount, selectStatus } = counterSlice.selectors - -// We can also write thunks by hand, which may contain both sync and async logic. -// Here's an example of conditionally dispatching actions based on current state. -export const incrementIfOdd = - (amount: number): AppThunk => - (dispatch, getState) => { - const currentValue = selectCount(getState()) - - if (currentValue % 2 === 1 || currentValue % 2 === -1) { - dispatch(incrementByAmount(amount)) - } - } diff --git a/src/features/quotes/Quotes.module.css b/src/features/quotes/Quotes.module.css deleted file mode 100644 index 1f85690..0000000 --- a/src/features/quotes/Quotes.module.css +++ /dev/null @@ -1,20 +0,0 @@ -.select { - font-size: 25px; - padding: 5px; - padding-top: 2px; - padding-bottom: 2px; - size: 50; - outline: none; - border: 2px solid transparent; - color: rgb(112, 76, 182); - cursor: pointer; - background-color: rgba(112, 76, 182, 0.1); - border-radius: 5px; - transition: all 0.15s; -} - -.container { - display: flex; - flex-direction: column; - align-items: center; -} diff --git a/src/features/quotes/Quotes.tsx b/src/features/quotes/Quotes.tsx deleted file mode 100644 index c490c4a..0000000 --- a/src/features/quotes/Quotes.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useState } from "react" -import styles from "./Quotes.module.css" -import { useGetQuotesQuery } from "./quotesApiSlice" - -const options = [5, 10, 20, 30] - -export const Quotes = () => { - const [numberOfQuotes, setNumberOfQuotes] = useState(10) - // Using a query hook automatically fetches data and returns query values - const { data, isError, isLoading, isSuccess } = - useGetQuotesQuery(numberOfQuotes) - - if (isError) { - return ( -
-

There was an error!!!

-
- ) - } - - if (isLoading) { - return ( -
-

Loading...

-
- ) - } - - if (isSuccess) { - return ( -
-

Select the Quantity of Quotes to Fetch:

- - {data.quotes.map(({ author, quote, id }) => ( -
- “{quote}” -
- {author} -
-
- ))} -
- ) - } - - return null -} diff --git a/src/features/quotes/quotesApiSlice.ts b/src/features/quotes/quotesApiSlice.ts deleted file mode 100644 index a1c7b5a..0000000 --- a/src/features/quotes/quotesApiSlice.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Need to use the React-specific entry point to import `createApi` -import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react" - -interface Quote { - id: number - quote: string - author: string -} - -interface QuotesApiResponse { - quotes: Quote[] - total: number - skip: number - limit: number -} - -// Define a service using a base URL and expected endpoints -export const quotesApiSlice = createApi({ - baseQuery: fetchBaseQuery({ baseUrl: "https://dummyjson.com/quotes" }), - reducerPath: "quotesApi", - // Tag types are used for caching and invalidation. - tagTypes: ["Quotes"], - endpoints: build => ({ - // Supply generics for the return type (in this case `QuotesApiResponse`) - // and the expected query argument. If there is no argument, use `void` - // for the argument type instead. - getQuotes: build.query({ - query: (limit = 10) => `?limit=${limit}`, - // `providesTags` determines which 'tag' is attached to the - // cached data returned by the query. - providesTags: (result, error, id) => [{ type: "Quotes", id }], - }), - }), -}) - -// Hooks are auto-generated by RTK-Query -// Same as `quotesApiSlice.endpoints.getQuotes.useQuery` -export const { useGetQuotesQuery } = quotesApiSlice diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 8466738..0000000 --- a/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main.tsx b/src/main.tsx index 45c0705..710cae1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,14 +1,14 @@ -import React from "react" -import { createRoot } from "react-dom/client" -import { Provider } from "react-redux" -import App from "./App" -import { store } from "./app/store" -import "./index.css" +import React from "react"; +import { createRoot } from "react-dom/client"; +import { Provider } from "react-redux"; +import { store } from "src/redux/store"; +import App from "src/App"; +import "src/index.css"; -const container = document.getElementById("root") +const container = document.getElementById("root"); if (container) { - const root = createRoot(container) + const root = createRoot(container); root.render( @@ -16,9 +16,9 @@ if (container) { , - ) + ); } else { throw new Error( "Root element with ID 'root' was not found in the document. Ensure there is a corresponding HTML element with the ID 'root' in your HTML file.", - ) + ); } diff --git a/src/app/hooks.ts b/src/redux/hooks.ts similarity index 82% rename from src/app/hooks.ts rename to src/redux/hooks.ts index bc8990d..0ac0258 100644 --- a/src/app/hooks.ts +++ b/src/redux/hooks.ts @@ -4,9 +4,9 @@ // We disable the ESLint rule here because this is the designated place // for importing and re-exporting the typed versions of hooks. /* eslint-disable @typescript-eslint/no-restricted-imports */ -import { useDispatch, useSelector } from "react-redux" -import type { AppDispatch, RootState } from "./store" +import { useDispatch, useSelector } from "react-redux"; +import type { AppDispatch, RootState } from "src/redux/store"; // Use throughout your app instead of plain `useDispatch` and `useSelector` -export const useAppDispatch = useDispatch.withTypes() -export const useAppSelector = useSelector.withTypes() +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); diff --git a/src/redux/store.ts b/src/redux/store.ts new file mode 100644 index 0000000..cee8cdd --- /dev/null +++ b/src/redux/store.ts @@ -0,0 +1,24 @@ +import { combineSlices, configureStore } from "@reduxjs/toolkit"; + +// pass in slices to combine into combineSlices() +// `combineSlices` automatically combines the reducers using +// their `reducerPath`s, therefore we no longer need to call `combineReducers`. +const rootReducer = combineSlices(); + +// Infer the `RootState` type from the root reducer +type RootState = ReturnType; + +const store = configureStore({ + reducer: rootReducer, + // this is unnecessary, but included for demonstration purposes + // since default middlewares are automatically included + middleware: getDefaultMiddleware => getDefaultMiddleware(), +}); + +// Infer the type of `store` +type AppStore = typeof store; +// Infer the `AppDispatch` type from the store itself +type AppDispatch = AppStore["dispatch"]; + +export { store }; +export type { RootState, AppStore, AppDispatch }; diff --git a/src/setupTests.ts b/src/setupTests.ts deleted file mode 100644 index b9e7622..0000000 --- a/src/setupTests.ts +++ /dev/null @@ -1 +0,0 @@ -import "@testing-library/jest-dom/vitest" diff --git a/src/utils/test-utils.tsx b/src/utils/test-utils.tsx deleted file mode 100644 index 90e184a..0000000 --- a/src/utils/test-utils.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { RenderOptions } from "@testing-library/react" -import { render } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import type { PropsWithChildren, ReactElement } from "react" -import { Provider } from "react-redux" -import type { AppStore, RootState } from "../app/store" -import { makeStore } from "../app/store" - -/** - * This type extends the default options for - * React Testing Library's render function. It allows for - * additional configuration such as specifying an initial Redux state and - * a custom store instance. - */ -interface ExtendedRenderOptions extends Omit { - /** - * Defines a specific portion or the entire initial state for the Redux store. - * This is particularly useful for initializing the state in a - * controlled manner during testing, allowing components to be rendered - * with predetermined state conditions. - */ - preloadedState?: Partial - - /** - * Allows the use of a specific Redux store instance instead of a - * default or global store. This flexibility is beneficial when - * testing components with unique store requirements or when isolating - * tests from a global store state. The custom store should be configured - * to match the structure and middleware of the store used by the application. - * - * @default makeStore(preloadedState) - */ - store?: AppStore -} - -/** - * Renders the given React element with Redux Provider and custom store. - * This function is useful for testing components that are connected to the Redux store. - * - * @param ui - The React component or element to render. - * @param extendedRenderOptions - Optional configuration options for rendering. This includes `preloadedState` for initial Redux state and `store` for a specific Redux store instance. Any additional properties are passed to React Testing Library's render function. - * @returns An object containing the Redux store used in the render, User event API for simulating user interactions in tests, and all of React Testing Library's query functions for testing the component. - */ -export const renderWithProviders = ( - ui: ReactElement, - extendedRenderOptions: ExtendedRenderOptions = {}, -) => { - const { - preloadedState = {}, - // Automatically create a store instance if no store was passed in - store = makeStore(preloadedState), - ...renderOptions - } = extendedRenderOptions - - const Wrapper = ({ children }: PropsWithChildren) => ( - {children} - ) - - // Return an object with the store and all of RTL's query functions - return { - store, - user: userEvent.setup(), - ...render(ui, { wrapper: Wrapper, ...renderOptions }), - } -} diff --git a/tsconfig.json b/tsconfig.json index dedb7b8..d15f928 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,9 @@ { "compilerOptions": { + "baseUrl": ".", + "paths": { + "src/*": ["./src/*"] + }, "target": "ESNext", "useDefineForClassFields": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], diff --git a/vite.config.ts b/vite.config.ts index 39a5632..f8237b4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,5 @@ -import { defineConfig } from "vitest/config" -import react from "@vitejs/plugin-react" +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ @@ -13,4 +13,9 @@ export default defineConfig({ setupFiles: "src/setupTests", mockReset: true, }, -}) + resolve: { + alias: { + src: "/src", + }, + }, +});