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 (
+
+
+
+
+
+
+
+
+
+
+ {props.children}
+
+
+
+
+
+
+ )
+}
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
-
-
-
-
- )
-}
-
-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"
- }
-}