diff --git a/docs/02-app/01-building-your-application/01-routing/10-router-handlers.mdx b/docs/02-app/01-building-your-application/01-routing/10-router-handlers.mdx index 7b4b91560a1bf..1fa8ff6153903 100644 --- a/docs/02-app/01-building-your-application/01-routing/10-router-handlers.mdx +++ b/docs/02-app/01-building-your-application/01-routing/10-router-handlers.mdx @@ -388,6 +388,60 @@ export async function GET(request, { params }) { ### Streaming +Streaming is commonly used in combination with Large Language Models (LLMs), such an OpenAI, for AI-generated content. Learn more about the [AI SDK](https://sdk.vercel.ai/docs). + +```ts filename="app/api/completion/route.ts" switcher +import { Configuration, OpenAIApi } from 'openai-edge' +import { OpenAIStream, StreamingTextResponse } from 'ai' + +const config = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}) +const openai = new OpenAIApi(config) + +export const runtime = 'edge' + +export async function POST(req: Request) { + const { prompt } = await req.json() + const response = await openai.createCompletion({ + model: 'text-davinci-003', + stream: true, + temperature: 0.6, + prompt: 'What is Next.js?', + }) + + const stream = OpenAIStream(response) + return new StreamingTextResponse(stream) +} +``` + +```js filename="app/api/completion/route.js" switcher +import { Configuration, OpenAIApi } from 'openai-edge' +import { OpenAIStream, StreamingTextResponse } from 'ai' + +const config = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}) +const openai = new OpenAIApi(config) + +export const runtime = 'edge' + +export async function POST(req) { + const { prompt } = await req.json() + const response = await openai.createCompletion({ + model: 'text-davinci-003', + stream: true, + temperature: 0.6, + prompt: 'What is Next.js?', + }) + + const stream = OpenAIStream(response) + return new StreamingTextResponse(stream) +} +``` + +These abstractions use the Web APIs to create a stream. You can also use the underlying Web APIs directly. + ```ts filename="app/api/route.ts" switcher // https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#convert_async_iterator_to_stream function iteratorToStream(iterator: any) { diff --git a/examples/with-redux/app/api/identity-count/route.ts b/examples/with-redux/app/api/identity-count/route.ts new file mode 100644 index 0000000000000..02cfe2f948371 --- /dev/null +++ b/examples/with-redux/app/api/identity-count/route.ts @@ -0,0 +1,12 @@ +/* Core */ +import { NextResponse } from 'next/server' + +export async function POST(req: Request, res: Response) { + const body = await req.json() + const { amount = 1 } = body + + // simulate IO latency + await new Promise((r) => setTimeout(r, 500)) + + return NextResponse.json({ data: amount }) +} diff --git a/examples/with-redux/src/features/counter/Counter.tsx b/examples/with-redux/app/components/Counter/Counter.tsx similarity index 54% rename from examples/with-redux/src/features/counter/Counter.tsx rename to examples/with-redux/app/components/Counter/Counter.tsx index 1bb1fa86a3fff..f9aabbde86731 100644 --- a/examples/with-redux/src/features/counter/Counter.tsx +++ b/examples/with-redux/app/components/Counter/Counter.tsx @@ -1,22 +1,23 @@ +'use client' + +/* Core */ import { useState } from 'react' -import { useAppSelector, useAppDispatch } from '../../hooks' +/* Instruments */ import { - decrement, - increment, - incrementByAmount, - incrementAsync, - incrementIfOdd, + counterSlice, + useSelector, + useDispatch, selectCount, -} from './counterSlice' -import styles from './Counter.module.css' - -function Counter() { - const dispatch = useAppDispatch() - const count = useAppSelector(selectCount) - const [incrementAmount, setIncrementAmount] = useState('2') + incrementAsync, + incrementIfOddAsync, +} from '@/lib/redux' +import styles from './counter.module.css' - const incrementValue = Number(incrementAmount) || 0 +export const Counter = () => { + const dispatch = useDispatch() + const count = useSelector(selectCount) + const [incrementAmount, setIncrementAmount] = useState(2) return (
@@ -24,7 +25,7 @@ function Counter() { @@ -32,7 +33,7 @@ function Counter() { @@ -42,23 +43,25 @@ function Counter() { className={styles.textbox} aria-label="Set increment amount" value={incrementAmount} - onChange={(e) => setIncrementAmount(e.target.value)} + onChange={(e) => setIncrementAmount(Number(e.target.value ?? 0))} /> @@ -66,5 +69,3 @@ function Counter() {
) } - -export default Counter diff --git a/examples/with-redux/src/features/counter/Counter.module.css b/examples/with-redux/app/components/Counter/counter.module.css similarity index 100% rename from examples/with-redux/src/features/counter/Counter.module.css rename to examples/with-redux/app/components/Counter/counter.module.css diff --git a/examples/with-redux/app/components/Nav.tsx b/examples/with-redux/app/components/Nav.tsx new file mode 100644 index 0000000000000..3cf3ed002ce0b --- /dev/null +++ b/examples/with-redux/app/components/Nav.tsx @@ -0,0 +1,31 @@ +'use client' + +/* Core */ +import Link from 'next/link' +import { usePathname } from 'next/navigation' + +/* Instruments */ +import styles from '../styles/layout.module.css' + +export const Nav = () => { + const pathname = usePathname() + + return ( + + ) +} diff --git a/examples/with-redux/public/favicon.ico b/examples/with-redux/app/icon.ico similarity index 100% rename from examples/with-redux/public/favicon.ico rename to examples/with-redux/app/icon.ico diff --git a/examples/with-redux/app/layout.tsx b/examples/with-redux/app/layout.tsx new file mode 100644 index 0000000000000..c071150ee449f --- /dev/null +++ b/examples/with-redux/app/layout.tsx @@ -0,0 +1,66 @@ +/* Components */ +import { Providers } from '@/lib/providers' +import { Nav } from './components/Nav' + +/* Instruments */ +import styles from './styles/layout.module.css' +import './styles/globals.css' + +export default function RootLayout(props: React.PropsWithChildren) { + return ( + + + +
+
+ + +
+ ) +} diff --git a/examples/with-redux/app/page.tsx b/examples/with-redux/app/page.tsx new file mode 100644 index 0000000000000..559704d119ecc --- /dev/null +++ b/examples/with-redux/app/page.tsx @@ -0,0 +1,10 @@ +/* Components */ +import { Counter } from './components/Counter/Counter' + +export default function IndexPage() { + return +} + +export const metadata = { + title: 'Redux Toolkit', +} diff --git a/examples/with-redux/app/styles/globals.css b/examples/with-redux/app/styles/globals.css new file mode 100644 index 0000000000000..0665acd3f5fe8 --- /dev/null +++ b/examples/with-redux/app/styles/globals.css @@ -0,0 +1,16 @@ +html, +body { + min-height: 100vh; + padding: 0; + margin: 0; + font-family: system-ui, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} diff --git a/examples/with-redux/app/styles/layout.module.css b/examples/with-redux/app/styles/layout.module.css new file mode 100644 index 0000000000000..a9ff48222af67 --- /dev/null +++ b/examples/with-redux/app/styles/layout.module.css @@ -0,0 +1,77 @@ +.container { + display: grid; + grid-template-areas: + 'nav' + 'header' + 'main' + 'footer'; + grid-template-rows: auto auto 1fr 36px; + align-items: center; + min-height: 100vh; +} + +.logo { + height: 40vmin; + pointer-events: none; +} + +.header { + grid-area: header; +} + +.main { + grid-area: main; +} + +.header, +.main { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.footer { + grid-area: footer; + justify-self: center; +} + +.nav { + grid-area: nav; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px; + font-size: calc(10px + 2vmin); +} + +.link:hover { + text-decoration: underline; +} + +.link { + color: #704cb6; +} + +.link.active { + text-decoration: underline; +} + +@media (prefers-reduced-motion: no-preference) { + .logo { + animation: logo-float infinite 3s ease-in-out; + } +} + +@keyframes logo-float { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(10px); + } + 100% { + transform: translateY(0px); + } +} diff --git a/examples/with-redux/app/verify/page.tsx b/examples/with-redux/app/verify/page.tsx new file mode 100644 index 0000000000000..2af0400962e5f --- /dev/null +++ b/examples/with-redux/app/verify/page.tsx @@ -0,0 +1,11 @@ +export default function VerifyPage() { + return ( + <> +

Verify page

+

+ This page is intended to verify that Redux state is persisted across + page navigations. +

+ + ) +} diff --git a/examples/with-redux/jest.config.ts b/examples/with-redux/jest.config.ts deleted file mode 100644 index 174ee5dcf423f..0000000000000 --- a/examples/with-redux/jest.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { InitialOptionsTsJest } from 'ts-jest/dist/types' - -const config: InitialOptionsTsJest = { - preset: 'ts-jest', - setupFilesAfterEnv: ['/setupTests.ts'], - transform: { - '.+\\.(css|styl|less|sass|scss)$': 'jest-css-modules-transform', - }, - testEnvironment: 'jsdom', - globals: { - 'ts-jest': { - tsconfig: 'tsconfig.test.json', - }, - }, -} - -export default config diff --git a/examples/with-redux/lib/providers.tsx b/examples/with-redux/lib/providers.tsx new file mode 100644 index 0000000000000..4150f7d22dff6 --- /dev/null +++ b/examples/with-redux/lib/providers.tsx @@ -0,0 +1,11 @@ +'use client' + +/* Core */ +import { Provider } from 'react-redux' + +/* Instruments */ +import { reduxStore } from '@/lib/redux' + +export const Providers = (props: React.PropsWithChildren) => { + return {props.children} +} diff --git a/examples/with-redux/lib/redux/createAppAsyncThunk.ts b/examples/with-redux/lib/redux/createAppAsyncThunk.ts new file mode 100644 index 0000000000000..df0b3ebc67410 --- /dev/null +++ b/examples/with-redux/lib/redux/createAppAsyncThunk.ts @@ -0,0 +1,14 @@ +/* Core */ +import { createAsyncThunk } from '@reduxjs/toolkit' + +/* Instruments */ +import type { ReduxState, ReduxDispatch } from './store' + +/** + * ? A utility function to create a typed Async Thnuk Actions. + */ +export const createAppAsyncThunk = createAsyncThunk.withTypes<{ + state: ReduxState + dispatch: ReduxDispatch + rejectValue: string +}>() diff --git a/examples/with-redux/lib/redux/index.ts b/examples/with-redux/lib/redux/index.ts new file mode 100644 index 0000000000000..e6f7630ff02b7 --- /dev/null +++ b/examples/with-redux/lib/redux/index.ts @@ -0,0 +1,2 @@ +export * from './store' +export * from './slices' diff --git a/examples/with-redux/lib/redux/middleware.ts b/examples/with-redux/lib/redux/middleware.ts new file mode 100644 index 0000000000000..ba0940c76e0d9 --- /dev/null +++ b/examples/with-redux/lib/redux/middleware.ts @@ -0,0 +1,20 @@ +/* Core */ +import { createLogger } from 'redux-logger' + +const middleware = [ + createLogger({ + duration: true, + timestamp: false, + collapsed: true, + colors: { + title: () => '#139BFE', + prevState: () => '#1C5FAF', + action: () => '#149945', + nextState: () => '#A47104', + error: () => '#ff0005', + }, + predicate: () => typeof window !== 'undefined', + }), +] + +export { middleware } diff --git a/examples/with-redux/lib/redux/rootReducer.ts b/examples/with-redux/lib/redux/rootReducer.ts new file mode 100644 index 0000000000000..4023a34eb8774 --- /dev/null +++ b/examples/with-redux/lib/redux/rootReducer.ts @@ -0,0 +1,6 @@ +/* Instruments */ +import { counterSlice } from './slices' + +export const reducer = { + counter: counterSlice.reducer, +} diff --git a/examples/with-redux/lib/redux/slices/counterSlice/counterSlice.ts b/examples/with-redux/lib/redux/slices/counterSlice/counterSlice.ts new file mode 100644 index 0000000000000..fa1fe58a9afcc --- /dev/null +++ b/examples/with-redux/lib/redux/slices/counterSlice/counterSlice.ts @@ -0,0 +1,50 @@ +/* Core */ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' + +/* Instruments */ +import { incrementAsync } from './thunks' + +const initialState: CounterSliceState = { + value: 0, + status: 'idle', +} + +export const counterSlice = createSlice({ + name: 'counter', + initialState, + // The `reducers` field lets us define reducers and generate associated actions + reducers: { + increment: (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: (state) => { + state.value -= 1 + }, + // Use the PayloadAction type to declare the contents of `action.payload` + incrementByAmount: (state, action: PayloadAction) => { + state.value += action.payload + }, + }, + // The `extraReducers` field lets the slice handle actions defined elsewhere, + // including actions generated by createAsyncThunk or in other slices. + extraReducers: (builder) => { + builder + .addCase(incrementAsync.pending, (state) => { + state.status = 'loading' + }) + .addCase(incrementAsync.fulfilled, (state, action) => { + state.status = 'idle' + state.value += action.payload + }) + }, +}) + +/* Types */ +export interface CounterSliceState { + value: number + status: 'idle' | 'loading' | 'failed' +} diff --git a/examples/with-redux/lib/redux/slices/counterSlice/fetchIdentityCount.ts b/examples/with-redux/lib/redux/slices/counterSlice/fetchIdentityCount.ts new file mode 100644 index 0000000000000..4e444820fbee2 --- /dev/null +++ b/examples/with-redux/lib/redux/slices/counterSlice/fetchIdentityCount.ts @@ -0,0 +1,12 @@ +export const fetchIdentityCount = async ( + amount = 1 +): Promise<{ data: number }> => { + const response = await fetch('http://localhost:3000/api/identity-count', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ amount }), + }) + const result = await response.json() + + return result +} diff --git a/examples/with-redux/lib/redux/slices/counterSlice/index.ts b/examples/with-redux/lib/redux/slices/counterSlice/index.ts new file mode 100644 index 0000000000000..9609b3a0687ca --- /dev/null +++ b/examples/with-redux/lib/redux/slices/counterSlice/index.ts @@ -0,0 +1,3 @@ +export * from './counterSlice' +export * from './thunks' +export * from './selectors' diff --git a/examples/with-redux/lib/redux/slices/counterSlice/selectors.ts b/examples/with-redux/lib/redux/slices/counterSlice/selectors.ts new file mode 100644 index 0000000000000..5e6261408c030 --- /dev/null +++ b/examples/with-redux/lib/redux/slices/counterSlice/selectors.ts @@ -0,0 +1,7 @@ +/* Instruments */ +import type { ReduxState } from '@/lib/redux' + +// The function below is called a selector and allows us to select a value from +// the state. Selectors can also be defined inline where they're used instead of +// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)` +export const selectCount = (state: ReduxState) => state.counter.value diff --git a/examples/with-redux/lib/redux/slices/counterSlice/thunks.ts b/examples/with-redux/lib/redux/slices/counterSlice/thunks.ts new file mode 100644 index 0000000000000..5668498cd0a36 --- /dev/null +++ b/examples/with-redux/lib/redux/slices/counterSlice/thunks.ts @@ -0,0 +1,33 @@ +/* Instruments */ +import { createAppAsyncThunk } from '@/lib/redux/createAppAsyncThunk' +import { fetchIdentityCount } from './fetchIdentityCount' +import { selectCount } from './selectors' +import { counterSlice } from './counterSlice' +import type { ReduxThunkAction } from '@/lib/redux' + +// 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. +export const incrementAsync = createAppAsyncThunk( + 'counter/fetchIdentityCount', + async (amount: number) => { + const response = await fetchIdentityCount(amount) + + // The value we return becomes the `fulfilled` action payload + return response.data + } +) + +// 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 incrementIfOddAsync = + (amount: number): ReduxThunkAction => + (dispatch, getState) => { + const currentValue = selectCount(getState()) + + if (currentValue % 2 === 1) { + dispatch(counterSlice.actions.incrementByAmount(amount)) + } + } diff --git a/examples/with-redux/lib/redux/slices/index.ts b/examples/with-redux/lib/redux/slices/index.ts new file mode 100644 index 0000000000000..6540ea7467869 --- /dev/null +++ b/examples/with-redux/lib/redux/slices/index.ts @@ -0,0 +1 @@ +export * from './counterSlice' diff --git a/examples/with-redux/lib/redux/store.ts b/examples/with-redux/lib/redux/store.ts new file mode 100644 index 0000000000000..0d23fc8f509ba --- /dev/null +++ b/examples/with-redux/lib/redux/store.ts @@ -0,0 +1,46 @@ +/* Core */ +import { + configureStore, + type ConfigureStoreOptions, + type ThunkAction, + type Action, +} from '@reduxjs/toolkit' +import { + useSelector as useReduxSelector, + useDispatch as useReduxDispatch, + type TypedUseSelectorHook, +} from 'react-redux' + +/* Instruments */ +import { reducer } from './rootReducer' +import { middleware } from './middleware' + +const configreStoreDefaultOptions: ConfigureStoreOptions = { reducer } + +export const makeReduxStore = ( + options: ConfigureStoreOptions = configreStoreDefaultOptions +) => { + const store = configureStore(options) + + return store +} + +export const reduxStore = configureStore({ + reducer, + middleware: (getDefaultMiddleware) => { + return getDefaultMiddleware().concat(middleware) + }, +}) +export const useDispatch = () => useReduxDispatch() +export const useSelector: TypedUseSelectorHook = useReduxSelector + +/* Types */ +export type ReduxStore = typeof reduxStore +export type ReduxState = ReturnType +export type ReduxDispatch = typeof reduxStore.dispatch +export type ReduxThunkAction = ThunkAction< + ReturnType, + ReduxState, + unknown, + Action +> diff --git a/examples/with-redux/next.config.mjs b/examples/with-redux/next.config.mjs new file mode 100644 index 0000000000000..94be31c3d55d8 --- /dev/null +++ b/examples/with-redux/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +export default nextConfig diff --git a/examples/with-redux/package.json b/examples/with-redux/package.json index 23b138a05f013..02db447cef8a9 100644 --- a/examples/with-redux/package.json +++ b/examples/with-redux/package.json @@ -3,30 +3,21 @@ "scripts": { "dev": "next", "build": "next build", - "start": "next start", - "type-check": "tsc", - "test": "jest" + "start": "next start" }, "dependencies": { - "@reduxjs/toolkit": "^1.3.6", + "@reduxjs/toolkit": "1.9.5", "next": "latest", - "react": "^18.1.0", - "react-dom": "^18.1.0", - "react-redux": "^7.2.0" + "react": "18.2.0", + "react-dom": "18.2.0", + "react-redux": "8.1.0" }, "devDependencies": { - "@testing-library/jest-dom": "^5.0.0", - "@testing-library/react": "^14.0.0", - "@testing-library/user-event": "^13.0.0", - "@types/jest": "^27.0.1", - "@types/node": "^16.9.1", - "@types/react": "^18.0.10", - "@types/react-dom": "^18.0.5", - "@types/react-redux": "^7.1.18", - "jest": "^27.2.0", - "jest-css-modules-transform": "^4.2.0", - "ts-jest": "^27.0.5", - "ts-node": "^10.2.1", - "typescript": "^4.3.4" + "@types/node": "20.3.1", + "@types/react": "18.2.12", + "@types/react-dom": "18.2.5", + "@types/redux-logger": "3.0.9", + "redux-logger": "3.0.6", + "typescript": "5.1.3" } } diff --git a/examples/with-redux/setupTests.ts b/examples/with-redux/setupTests.ts deleted file mode 100644 index 7aa390db552bd..0000000000000 --- a/examples/with-redux/setupTests.ts +++ /dev/null @@ -1,4 +0,0 @@ -import '@testing-library/jest-dom' -import { loadEnvConfig } from '@next/env' - -loadEnvConfig(__dirname, true, { info: () => null, error: console.error }) diff --git a/examples/with-redux/src/features/counter/Counter.spec.tsx b/examples/with-redux/src/features/counter/Counter.spec.tsx deleted file mode 100644 index 558c4104ce0e6..0000000000000 --- a/examples/with-redux/src/features/counter/Counter.spec.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { render, screen } from '@testing-library/react' -import user from '@testing-library/user-event' -import { Provider } from 'react-redux' - -jest.mock('./counterAPI', () => ({ - fetchCount: (amount: number) => - new Promise<{ data: number }>((resolve) => - setTimeout(() => resolve({ data: amount }), 500) - ), -})) - -import { makeStore } from '../../store' -import Counter from './Counter' - -describe('', () => { - it('renders the component', () => { - const store = makeStore() - - render( - - - - ) - - expect(screen.getByText('0')).toBeInTheDocument() - }) - - it('decrements the value', () => { - const store = makeStore() - - render( - - - - ) - - user.click(screen.getByRole('button', { name: /decrement value/i })) - - expect(screen.getByText('-1')).toBeInTheDocument() - }) - - it('increments the value', () => { - const store = makeStore() - - render( - - - - ) - - user.click(screen.getByRole('button', { name: /increment value/i })) - - expect(screen.getByText('1')).toBeInTheDocument() - }) - - it('increments by amount', () => { - const store = makeStore() - - render( - - - - ) - - user.type(screen.getByLabelText(/set increment amount/i), '{backspace}5') - user.click(screen.getByRole('button', { name: /add amount/i })) - - expect(screen.getByText('5')).toBeInTheDocument() - }) - - it('increments async', async () => { - const store = makeStore() - - render( - - - - ) - - user.type(screen.getByLabelText(/set increment amount/i), '{backspace}3') - user.click(screen.getByRole('button', { name: /add async/i })) - - await expect(screen.findByText('3')).resolves.toBeInTheDocument() - }) - - it('increments if amount is odd', async () => { - const store = makeStore() - - render( - - - - ) - - user.click(screen.getByRole('button', { name: /add if odd/i })) - - expect(screen.getByText('0')).toBeInTheDocument() - - user.click(screen.getByRole('button', { name: /increment value/i })) - user.type(screen.getByLabelText(/set increment amount/i), '{backspace}8') - user.click(screen.getByRole('button', { name: /add if odd/i })) - - await expect(screen.findByText('9')).resolves.toBeInTheDocument() - }) -}) diff --git a/examples/with-redux/src/features/counter/counterAPI.ts b/examples/with-redux/src/features/counter/counterAPI.ts deleted file mode 100644 index 9f6c4bd877ee3..0000000000000 --- a/examples/with-redux/src/features/counter/counterAPI.ts +++ /dev/null @@ -1,12 +0,0 @@ -export async function fetchCount(amount = 1): Promise<{ data: number }> { - const response = await fetch('/api/counter', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ amount }), - }) - const result = await response.json() - - return result -} diff --git a/examples/with-redux/src/features/counter/counterSlice.ts b/examples/with-redux/src/features/counter/counterSlice.ts deleted file mode 100644 index e9e1718019212..0000000000000 --- a/examples/with-redux/src/features/counter/counterSlice.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' - -import type { AppState, AppThunk } from '../../store' -import { fetchCount } from './counterAPI' - -export interface CounterState { - value: number - status: 'idle' | 'loading' | 'failed' -} - -const initialState: CounterState = { - value: 0, - status: 'idle', -} - -// 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. -export const incrementAsync = createAsyncThunk( - 'counter/fetchCount', - async (amount: number) => { - const response = await fetchCount(amount) - // The value we return becomes the `fulfilled` action payload - return response.data - } -) - -export const counterSlice = createSlice({ - name: 'counter', - initialState, - // The `reducers` field lets us define reducers and generate associated actions - reducers: { - increment: (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: (state) => { - state.value -= 1 - }, - // Use the PayloadAction type to declare the contents of `action.payload` - incrementByAmount: (state, action: PayloadAction) => { - state.value += action.payload - }, - }, - // The `extraReducers` field lets the slice handle actions defined elsewhere, - // including actions generated by createAsyncThunk or in other slices. - extraReducers: (builder) => { - builder - .addCase(incrementAsync.pending, (state) => { - state.status = 'loading' - }) - .addCase(incrementAsync.fulfilled, (state, action) => { - state.status = 'idle' - state.value += action.payload - }) - }, -}) - -export const { increment, decrement, incrementByAmount } = counterSlice.actions - -// The function below is called a selector and allows us to select a value from -// the state. Selectors can also be defined inline where they're used instead of -// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)` -export const selectCount = (state: AppState) => state.counter.value - -// 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) { - dispatch(incrementByAmount(amount)) - } - } - -export default counterSlice.reducer diff --git a/examples/with-redux/src/hooks.ts b/examples/with-redux/src/hooks.ts deleted file mode 100644 index 96d0b0199c759..0000000000000 --- a/examples/with-redux/src/hooks.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { ChangeEvent } from 'react' -import { useEffect, useRef } from 'react' -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' - -import type { AppDispatch, AppState } from './store' - -export const useForm = - (defaultValues: TContent) => - (handler: (content: TContent) => void) => - async (event: ChangeEvent) => { - event.preventDefault() - event.persist() - - const form = event.target as HTMLFormElement - const elements = Array.from(form.elements) as HTMLInputElement[] - const data = elements - .filter((element) => element.hasAttribute('name')) - .reduce( - (object, element) => ({ - ...object, - [`${element.getAttribute('name')}`]: element.value, - }), - defaultValues - ) - await handler(data) - form.reset() - } - -// https://overreacted.io/making-setinterval-declarative-with-react-hooks/ -export const useInterval = (callback: Function, delay: number) => { - const savedCallback = useRef() - useEffect(() => { - savedCallback.current = callback - }, [callback]) - useEffect(() => { - const handler = (...args: any) => savedCallback.current?.(...args) - - if (delay !== null) { - const id = setInterval(handler, delay) - return () => clearInterval(id) - } - }, [delay]) -} - -// Use throughout your app instead of plain `useDispatch` and `useSelector` -export const useAppDispatch = () => useDispatch() - -export const useAppSelector: TypedUseSelectorHook = useSelector diff --git a/examples/with-redux/src/pages/_app.tsx b/examples/with-redux/src/pages/_app.tsx deleted file mode 100644 index d99399f5f07b9..0000000000000 --- a/examples/with-redux/src/pages/_app.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import '../styles/globals.css' - -import { Provider } from 'react-redux' -import type { AppProps } from 'next/app' - -import store from '../store' - -export default function MyApp({ Component, pageProps }: AppProps) { - return ( - - - - ) -} diff --git a/examples/with-redux/src/pages/api/counter.ts b/examples/with-redux/src/pages/api/counter.ts deleted file mode 100644 index 1aa9b5093d4a5..0000000000000 --- a/examples/with-redux/src/pages/api/counter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { NextApiHandler } from 'next' - -const countHandler: NextApiHandler = async (request, response) => { - const { amount = 1 } = request.body - - // simulate IO latency - await new Promise((resolve) => setTimeout(resolve, 500)) - - response.json({ data: amount }) -} - -export default countHandler diff --git a/examples/with-redux/src/pages/index.tsx b/examples/with-redux/src/pages/index.tsx deleted file mode 100644 index 2ed50fb6db169..0000000000000 --- a/examples/with-redux/src/pages/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { NextPage } from 'next' -import Head from 'next/head' - -import Counter from '../features/counter/Counter' -import styles from '../styles/Home.module.css' - -const IndexPage: NextPage = () => { - return ( -
- - Redux Toolkit - - -
- logo - -

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

- - Learn - - React - - , - - Redux - - , - - Redux Toolkit - - , and - - React Redux - - -
-
- ) -} - -export default IndexPage diff --git a/examples/with-redux/src/store.ts b/examples/with-redux/src/store.ts deleted file mode 100644 index c0e5753bea4ae..0000000000000 --- a/examples/with-redux/src/store.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit' - -import counterReducer from './features/counter/counterSlice' - -export function makeStore() { - return configureStore({ - reducer: { counter: counterReducer }, - }) -} - -const store = makeStore() - -export type AppState = ReturnType - -export type AppDispatch = typeof store.dispatch - -export type AppThunk = ThunkAction< - ReturnType, - AppState, - unknown, - Action -> - -export default store diff --git a/examples/with-redux/src/styles/Home.module.css b/examples/with-redux/src/styles/Home.module.css deleted file mode 100644 index aeef5641ac5d4..0000000000000 --- a/examples/with-redux/src/styles/Home.module.css +++ /dev/null @@ -1,39 +0,0 @@ -.container { - text-align: center; -} - -.logo { - height: 40vmin; - pointer-events: none; -} - -.header { - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); -} - -.link { - color: rgb(112, 76, 182); -} - -@media (prefers-reduced-motion: no-preference) { - .logo { - animation: logo-float infinite 3s ease-in-out; - } -} - -@keyframes logo-float { - 0% { - transform: translateY(0); - } - 50% { - transform: translateY(10px); - } - 100% { - transform: translateY(0px); - } -} diff --git a/examples/with-redux/src/styles/globals.css b/examples/with-redux/src/styles/globals.css deleted file mode 100644 index e5e2dcc23baf1..0000000000000 --- a/examples/with-redux/src/styles/globals.css +++ /dev/null @@ -1,16 +0,0 @@ -html, -body { - padding: 0; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -} - -a { - color: inherit; - text-decoration: none; -} - -* { - box-sizing: border-box; -} diff --git a/examples/with-redux/tsconfig.json b/examples/with-redux/tsconfig.json index 4fa631c261428..31f3bc8b20a7e 100644 --- a/examples/with-redux/tsconfig.json +++ b/examples/with-redux/tsconfig.json @@ -1,19 +1,24 @@ { + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": ".", + "paths": { "@/*": ["./*"] }, + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, - "module": "esnext", + "module": "ESNEXT", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve" + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/examples/with-redux/tsconfig.test.json b/examples/with-redux/tsconfig.test.json deleted file mode 100644 index 4fd5045d7dcc0..0000000000000 --- a/examples/with-redux/tsconfig.test.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx" - } -}