redux store setup
This commit is contained in:
parent
3c1f32980d
commit
09409907e2
3
.gitignore
vendored
3
.gitignore
vendored
@ -22,3 +22,6 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Custom
|
||||||
|
reduxDemo
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"semi": false,
|
"semi": true,
|
||||||
"arrowParens": "avoid"
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
|
22
src/App.tsx
22
src/App.tsx
@ -1,21 +1,7 @@
|
|||||||
import "./App.css"
|
import "src/App.css";
|
||||||
import { Counter } from "./features/counter/Counter"
|
|
||||||
import { Quotes } from "./features/quotes/Quotes"
|
|
||||||
import logo from "./logo.svg"
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return <div>App</div>;
|
||||||
<div className="App">
|
};
|
||||||
<header className="App-header">
|
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
|
||||||
<Counter />
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to reload.
|
|
||||||
</p>
|
|
||||||
<Quotes />
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
@ -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 },
|
|
||||||
})
|
|
@ -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<typeof rootReducer>
|
|
||||||
|
|
||||||
// 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<RootState>) => {
|
|
||||||
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<ThunkReturnType = void> = ThunkAction<
|
|
||||||
ThunkReturnType,
|
|
||||||
RootState,
|
|
||||||
unknown,
|
|
||||||
Action
|
|
||||||
>
|
|
@ -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;
|
|
||||||
}
|
|
@ -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 (
|
|
||||||
<div>
|
|
||||||
<div className={styles.row}>
|
|
||||||
<button
|
|
||||||
className={styles.button}
|
|
||||||
aria-label="Decrement value"
|
|
||||||
onClick={() => dispatch(decrement())}
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
<span aria-label="Count" className={styles.value}>
|
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
className={styles.button}
|
|
||||||
aria-label="Increment value"
|
|
||||||
onClick={() => dispatch(increment())}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.row}>
|
|
||||||
<input
|
|
||||||
className={styles.textbox}
|
|
||||||
aria-label="Set increment amount"
|
|
||||||
value={incrementAmount}
|
|
||||||
type="number"
|
|
||||||
onChange={e => {
|
|
||||||
setIncrementAmount(e.target.value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => dispatch(incrementByAmount(incrementValue))}
|
|
||||||
>
|
|
||||||
Add Amount
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={styles.asyncButton}
|
|
||||||
disabled={status !== "idle"}
|
|
||||||
onClick={() => dispatch(incrementAsync(incrementValue))}
|
|
||||||
>
|
|
||||||
Add Async
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(incrementIfOdd(incrementValue))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add If Odd
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
@ -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<LocalTestContext>("counter reducer", it => {
|
|
||||||
beforeEach<LocalTestContext>(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)
|
|
||||||
})
|
|
||||||
})
|
|
@ -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<number>) => {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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 (
|
|
||||||
<div>
|
|
||||||
<h1>There was an error!!!</h1>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Loading...</h1>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSuccess) {
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<h3>Select the Quantity of Quotes to Fetch:</h3>
|
|
||||||
<select
|
|
||||||
className={styles.select}
|
|
||||||
value={numberOfQuotes}
|
|
||||||
onChange={e => {
|
|
||||||
setNumberOfQuotes(Number(e.target.value))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{options.map(option => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{data.quotes.map(({ author, quote, id }) => (
|
|
||||||
<blockquote key={id}>
|
|
||||||
“{quote}”
|
|
||||||
<footer>
|
|
||||||
<cite>{author}</cite>
|
|
||||||
</footer>
|
|
||||||
</blockquote>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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<QuotesApiResponse, number>({
|
|
||||||
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
|
|
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g fill="#764ABC"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
20
src/main.tsx
20
src/main.tsx
@ -1,14 +1,14 @@
|
|||||||
import React from "react"
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client";
|
||||||
import { Provider } from "react-redux"
|
import { Provider } from "react-redux";
|
||||||
import App from "./App"
|
import { store } from "src/redux/store";
|
||||||
import { store } from "./app/store"
|
import App from "src/App";
|
||||||
import "./index.css"
|
import "src/index.css";
|
||||||
|
|
||||||
const container = document.getElementById("root")
|
const container = document.getElementById("root");
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
const root = createRoot(container)
|
const root = createRoot(container);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
@ -16,9 +16,9 @@ if (container) {
|
|||||||
<App />
|
<App />
|
||||||
</Provider>
|
</Provider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
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.",
|
"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.",
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
// We disable the ESLint rule here because this is the designated place
|
// We disable the ESLint rule here because this is the designated place
|
||||||
// for importing and re-exporting the typed versions of hooks.
|
// for importing and re-exporting the typed versions of hooks.
|
||||||
/* eslint-disable @typescript-eslint/no-restricted-imports */
|
/* eslint-disable @typescript-eslint/no-restricted-imports */
|
||||||
import { useDispatch, useSelector } from "react-redux"
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import type { AppDispatch, RootState } from "./store"
|
import type { AppDispatch, RootState } from "src/redux/store";
|
||||||
|
|
||||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
export const useAppSelector = useSelector.withTypes<RootState>();
|
24
src/redux/store.ts
Normal file
24
src/redux/store.ts
Normal file
@ -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<typeof rootReducer>;
|
||||||
|
|
||||||
|
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 };
|
@ -1 +0,0 @@
|
|||||||
import "@testing-library/jest-dom/vitest"
|
|
@ -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<RenderOptions, "queries"> {
|
|
||||||
/**
|
|
||||||
* 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<RootState>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) => (
|
|
||||||
<Provider store={store}>{children}</Provider>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Return an object with the store and all of RTL's query functions
|
|
||||||
return {
|
|
||||||
store,
|
|
||||||
user: userEvent.setup(),
|
|
||||||
...render(ui, { wrapper: Wrapper, ...renderOptions }),
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"src/*": ["./src/*"]
|
||||||
|
},
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { defineConfig } from "vitest/config"
|
import { defineConfig } from "vitest/config";
|
||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -13,4 +13,9 @@ export default defineConfig({
|
|||||||
setupFiles: "src/setupTests",
|
setupFiles: "src/setupTests",
|
||||||
mockReset: true,
|
mockReset: true,
|
||||||
},
|
},
|
||||||
})
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
src: "/src",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user