├── src ├── vite-env.d.ts ├── utils.ts ├── index.css ├── App.tsx ├── main.tsx ├── tutorial │ ├── 02-actions │ │ ├── 02-action.tsx │ │ ├── 03-hook.tsx │ │ ├── 04-complex.tsx │ │ └── 01-traditional-approach.tsx │ ├── 01-use │ │ ├── 02-use-approach.tsx │ │ └── 01-traditional-approach.tsx │ ├── 05-render-issue │ │ └── index.tsx │ ├── 03-useFormStatus │ │ ├── 02-final.tsx │ │ └── 01-starter.tsx │ └── 04-useOptimistic │ │ ├── 02-final.tsx │ │ └── 01-starter.tsx ├── final │ ├── 05-render-issue │ │ └── index.tsx │ ├── 01-use │ │ ├── 01-traditional-approach.tsx │ │ └── 02-use-approach.tsx │ ├── 02-actions │ │ ├── 02-action.tsx │ │ ├── 03-hook.tsx │ │ ├── 04-complex.tsx │ │ └── 01-traditional-approach.tsx │ ├── 03-useFormStatus │ │ ├── 01-starter.tsx │ │ └── 02-final.tsx │ └── 04-useOptimistic │ │ ├── 01-starter.tsx │ │ └── 02-final.tsx └── assets │ └── react.svg ├── postcss.config.js ├── tsconfig.json ├── tailwind.config.js ├── .gitignore ├── vite.config.ts ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── db.json ├── eslint.config.js ├── package.json ├── public └── vite.svg └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const API_URL = 'http://localhost:3001'; 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | function App() { 2 | return ( 3 |

4 | React 19 Tutorial with TypeScript 5 |

6 | ); 7 | } 8 | 9 | export default App; 10 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | const ReactCompilerConfig = {}; 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [ 7 | react({ 8 | babel: { 9 | plugins: [['babel-plugin-react-compiler', ReactCompilerConfig]], 10 | }, 11 | }), 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React 19 Tutorial with TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": "1", 5 | "name": "John Doe", 6 | "email": "johndoe@example.com" 7 | }, 8 | { 9 | "id": "2", 10 | "name": "Jane Smith", 11 | "email": "janesmith@example.com" 12 | }, 13 | { 14 | "id": "d6ce", 15 | "name": "susan", 16 | "email": "susan@gmail.com" 17 | }, 18 | { 19 | "id": "b98d", 20 | "name": "john", 21 | "email": "john@gmail.com" 22 | }, 23 | { 24 | "id": "f661", 25 | "name": "john", 26 | "email": "john@gmail.com" 27 | } 28 | ], 29 | 30 | "todos": [ 31 | { 32 | "id": "1", 33 | "text": "Learn React", 34 | "completed": true 35 | }, 36 | { 37 | "id": "a185", 38 | "text": "new item", 39 | "completed": true 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import reactHooks from 'eslint-plugin-react-hooks'; 4 | import reactRefresh from 'eslint-plugin-react-refresh'; 5 | import tseslint from 'typescript-eslint'; 6 | import reactCompiler from 'eslint-plugin-react-compiler'; 7 | 8 | export default tseslint.config( 9 | { ignores: ['dist'] }, 10 | { 11 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 12 | files: ['**/*.{ts,tsx}'], 13 | languageOptions: { 14 | ecmaVersion: 2020, 15 | globals: globals.browser, 16 | }, 17 | plugins: { 18 | 'react-hooks': reactHooks, 19 | 'react-refresh': reactRefresh, 20 | 'react-compiler': reactCompiler, 21 | }, 22 | rules: { 23 | ...reactHooks.configs.recommended.rules, 24 | 'react-refresh/only-export-components': [ 25 | 'warn', 26 | { allowConstantExport: true }, 27 | ], 28 | 'react-compiler/react-compiler': 'error', 29 | }, 30 | } 31 | ); 32 | -------------------------------------------------------------------------------- /src/tutorial/02-actions/02-action.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_URL } from '../../utils'; 3 | 4 | const Component = () => { 5 | return ( 6 |
7 | 14 | 21 | 24 |
25 | ); 26 | }; 27 | export default Component; 28 | 29 | const formStyles = { 30 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 31 | input: 32 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 33 | button: 34 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200', 35 | }; 36 | -------------------------------------------------------------------------------- /src/tutorial/02-actions/03-hook.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useActionState } from 'react'; 3 | import { API_URL } from '../../utils'; 4 | 5 | const Component = () => { 6 | return ( 7 |
8 | 15 | 22 | 25 |
26 | ); 27 | }; 28 | export default Component; 29 | 30 | const formStyles = { 31 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 32 | input: 33 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 34 | button: 35 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200', 36 | }; 37 | -------------------------------------------------------------------------------- /src/tutorial/01-use/02-use-approach.tsx: -------------------------------------------------------------------------------- 1 | import { use, Suspense, useState } from 'react'; 2 | import axios from 'axios'; 3 | import { API_URL } from '../../utils'; 4 | 5 | type User = { 6 | id: string; 7 | name: string; 8 | email: string; 9 | }; 10 | 11 | const Component = () => { 12 | const users = [] as User[]; 13 | const [count, setCount] = useState(0); 14 | 15 | return ( 16 |
17 | 23 |

Users

24 | 35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/tutorial/02-actions/04-complex.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useActionState } from 'react'; 3 | import { API_URL } from '../../utils'; 4 | type Status = 'success' | 'error' | 'idle'; 5 | type FormState = { status: Status; name: string }; 6 | 7 | const Component = () => { 8 | return ( 9 |
10 | 17 | 24 | 27 |
28 | ); 29 | }; 30 | export default Component; 31 | 32 | const formStyles = { 33 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 34 | input: 35 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 36 | button: 37 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200', 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-19-tutorial", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "server": "json-server db.json --port 3001", 8 | "dev": "concurrently \"npm run server\" \"vite\"", 9 | "build": "tsc -b && vite build", 10 | "lint": "eslint .", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@types/react": "19.0.2", 15 | "@types/react-dom": "19.0.2", 16 | "axios": "^1.7.9", 17 | "concurrently": "^9.1.0", 18 | "json-server": "^1.0.0-beta.3", 19 | "react": "^19.0.0", 20 | "react-dom": "^19.0.0" 21 | }, 22 | "devDependencies": { 23 | "@eslint/js": "^9.17.0", 24 | "@vitejs/plugin-react": "^4.3.4", 25 | "autoprefixer": "^10.4.20", 26 | "babel-plugin-react-compiler": "^19.0.0-beta-201e55d-20241215", 27 | "eslint": "^9.17.0", 28 | "eslint-plugin-react-compiler": "^19.0.0-beta-201e55d-20241215", 29 | "eslint-plugin-react-hooks": "^5.0.0", 30 | "eslint-plugin-react-refresh": "^0.4.16", 31 | "globals": "^15.13.0", 32 | "postcss": "^8.4.49", 33 | "tailwindcss": "^3.4.17", 34 | "typescript": "~5.6.2", 35 | "typescript-eslint": "^8.18.1", 36 | "vite": "^6.0.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/final/05-render-issue/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useState } from 'react'; 2 | 3 | const Parent = () => { 4 | const [count, setCount] = useState(0); 5 | const [value, setValue] = useState(0); 6 | return ( 7 |
8 |

Parent Component

9 |

Count: {count}

10 | 16 | 17 | {/* */} 18 |
19 | ); 20 | }; 21 | 22 | type ChildProps = { 23 | value: number; 24 | setValue: (value: number) => void; 25 | }; 26 | 27 | const Child = ({ value, setValue }: ChildProps) => { 28 | console.log('Child rendered'); 29 | return ( 30 |
31 |

Child Component

32 |

Value: {value}

33 | 39 |
40 | ); 41 | }; 42 | const OptimizedChild = memo(Child); 43 | 44 | export default Parent; 45 | -------------------------------------------------------------------------------- /src/tutorial/05-render-issue/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useState } from 'react'; 2 | 3 | const Parent = () => { 4 | const [count, setCount] = useState(0); 5 | const [value, setValue] = useState(0); 6 | return ( 7 |
8 |

Parent Component

9 |

Count: {count}

10 | 16 | 17 | {/* */} 18 |
19 | ); 20 | }; 21 | 22 | type ChildProps = { 23 | value: number; 24 | setValue: (value: number) => void; 25 | }; 26 | 27 | const Child = ({ value, setValue }: ChildProps) => { 28 | console.log('Child rendered'); 29 | return ( 30 |
31 |

Child Component

32 |

Value: {value}

33 | 39 |
40 | ); 41 | }; 42 | const OptimizedChild = memo(Child); 43 | 44 | export default Parent; 45 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/final/01-use/01-traditional-approach.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import axios from 'axios'; 3 | import { API_URL } from '../../utils'; 4 | 5 | type User = { 6 | id: string; 7 | name: string; 8 | email: string; 9 | }; 10 | 11 | const Component = () => { 12 | const [users, setUsers] = useState([]); 13 | const [isLoading, setIsLoading] = useState(false); 14 | 15 | const fetchUsers = async () => { 16 | setIsLoading(true); 17 | try { 18 | const response = await axios.get(`${API_URL}/users`); 19 | setUsers(response.data); 20 | } catch (error) { 21 | console.log(error); 22 | } finally { 23 | setIsLoading(false); 24 | } 25 | }; 26 | 27 | useEffect(() => { 28 | fetchUsers(); 29 | }, []); 30 | 31 | return ( 32 |
33 |

Users

34 | {isLoading ? ( 35 |

Loading...

36 | ) : ( 37 |
    38 | {users.map((user) => ( 39 |
  • 43 | {user.name} 44 | - {user.email} 45 |
  • 46 | ))} 47 |
48 | )} 49 |
50 | ); 51 | }; 52 | export default Component; 53 | -------------------------------------------------------------------------------- /src/tutorial/01-use/01-traditional-approach.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import axios from 'axios'; 3 | import { API_URL } from '../../utils'; 4 | 5 | type User = { 6 | id: string; 7 | name: string; 8 | email: string; 9 | }; 10 | 11 | const Component = () => { 12 | const [users, setUsers] = useState([]); 13 | const [isLoading, setIsLoading] = useState(false); 14 | 15 | const fetchUsers = async () => { 16 | setIsLoading(true); 17 | try { 18 | const response = await axios.get(`${API_URL}/users`); 19 | setUsers(response.data); 20 | } catch (error) { 21 | console.log(error); 22 | } finally { 23 | setIsLoading(false); 24 | } 25 | }; 26 | 27 | useEffect(() => { 28 | fetchUsers(); 29 | }, []); 30 | 31 | return ( 32 |
33 |

Users

34 | {isLoading ? ( 35 |

Loading...

36 | ) : ( 37 |
    38 | {users.map((user) => ( 39 |
  • 43 | {user.name} 44 | - {user.email} 45 |
  • 46 | ))} 47 |
48 | )} 49 |
50 | ); 51 | }; 52 | export default Component; 53 | -------------------------------------------------------------------------------- /src/tutorial/03-useFormStatus/02-final.tsx: -------------------------------------------------------------------------------- 1 | import { useFormStatus } from 'react-dom'; 2 | import { useActionState } from 'react'; 3 | 4 | const formAction = async ( 5 | prevState: string, 6 | formData: FormData 7 | ): Promise => { 8 | await new Promise((resolve) => setTimeout(resolve, 2000)); 9 | console.log(formData); 10 | return 'success'; 11 | }; 12 | 13 | const FirstForm = () => { 14 | const [status, actionFunction] = useActionState( 15 | formAction, 16 | 'idle' 17 | ); 18 | return ( 19 |
20 | 21 | {status === 'success' &&

Form submitted successfully

} 22 |
23 | ); 24 | }; 25 | 26 | const SecondForm = () => { 27 | const [status, actionFunction] = useActionState( 28 | formAction, 29 | 'idle' 30 | ); 31 | return ( 32 |
33 | 34 | {status === 'success' &&

Form submitted successfully

} 35 |
36 | ); 37 | }; 38 | 39 | const ParentComponent = () => { 40 | return ( 41 | <> 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | const formStyles = { 49 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 50 | input: 51 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 52 | }; 53 | 54 | export default ParentComponent; 55 | -------------------------------------------------------------------------------- /src/final/02-actions/02-action.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_URL } from '../../utils'; 3 | 4 | const Component = () => { 5 | // const formAction = async (formData: FormData): Promise => { 6 | // const name = formData.get('name') as string; 7 | // const email = formData.get('email') as string; 8 | // await new Promise((resolve) => setTimeout(resolve, 2000)); 9 | // const response = await axios.post(`${API_URL}/users`, { name, email }); 10 | // console.log(response.data); 11 | // }; 12 | const formAction = async (formData: FormData): Promise => { 13 | const data = Object.fromEntries(formData); 14 | await new Promise((resolve) => setTimeout(resolve, 2000)); 15 | const response = await axios.post(`${API_URL}/users`, data); 16 | console.log(response.data); 17 | }; 18 | return ( 19 |
20 | 27 | 34 | 37 |
38 | ); 39 | }; 40 | export default Component; 41 | 42 | const formStyles = { 43 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 44 | input: 45 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 46 | button: 47 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200', 48 | }; 49 | -------------------------------------------------------------------------------- /src/final/01-use/02-use-approach.tsx: -------------------------------------------------------------------------------- 1 | import { use, Suspense, useState } from 'react'; 2 | import axios from 'axios'; 3 | import { API_URL } from '../../utils'; 4 | 5 | type User = { 6 | id: string; 7 | name: string; 8 | email: string; 9 | }; 10 | 11 | // Move the data fetching logic outside the component 12 | const fetchUsers = async () => { 13 | const response = await axios.get(`${API_URL}/users`); 14 | return response.data as User[]; 15 | }; 16 | 17 | const Component = () => { 18 | // Use the 'use' hook to directly consume the promise 19 | const users = use(fetchUsers()); 20 | const [count, setCount] = useState(0); 21 | 22 | return ( 23 |
24 | 30 |

Users

31 |
    32 | {users.map((user) => ( 33 |
  • 37 | {user.name} 38 | - {user.email} 39 |
  • 40 | ))} 41 |
42 |
43 | ); 44 | }; 45 | 46 | // Wrap the component with Suspense to handle the loading state 47 | const UsersPage = () => { 48 | return ( 49 | Loading...

52 | } 53 | > 54 | 55 |
56 | ); 57 | }; 58 | 59 | export default UsersPage; 60 | -------------------------------------------------------------------------------- /src/final/03-useFormStatus/01-starter.tsx: -------------------------------------------------------------------------------- 1 | import { useActionState } from 'react'; 2 | 3 | const formAction = async ( 4 | prevState: string, 5 | formData: FormData 6 | ): Promise => { 7 | await new Promise((resolve) => setTimeout(resolve, 2000)); 8 | console.log(formData); 9 | return 'success'; 10 | }; 11 | 12 | const FirstForm = () => { 13 | const [status, actionFunction, isPending] = useActionState( 14 | formAction, 15 | 'idle' 16 | ); 17 | return ( 18 |
19 | 20 | 23 | {status === 'success' &&

Form submitted successfully

} 24 |
25 | ); 26 | }; 27 | 28 | const SecondForm = () => { 29 | const [status, actionFunction, isPending] = useActionState( 30 | formAction, 31 | 'idle' 32 | ); 33 | return ( 34 |
35 | 36 | 39 | {status === 'success' &&

Form submitted successfully

} 40 |
41 | ); 42 | }; 43 | 44 | const ParentComponent = () => { 45 | return ( 46 | <> 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | const formStyles = { 54 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 55 | input: 56 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 57 | button: 58 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200', 59 | }; 60 | 61 | export default ParentComponent; 62 | -------------------------------------------------------------------------------- /src/tutorial/03-useFormStatus/01-starter.tsx: -------------------------------------------------------------------------------- 1 | import { useActionState } from 'react'; 2 | 3 | const formAction = async ( 4 | prevState: string, 5 | formData: FormData 6 | ): Promise => { 7 | await new Promise((resolve) => setTimeout(resolve, 2000)); 8 | console.log(formData); 9 | return 'success'; 10 | }; 11 | 12 | const FirstForm = () => { 13 | const [status, actionFunction, isPending] = useActionState( 14 | formAction, 15 | 'idle' 16 | ); 17 | return ( 18 |
19 | 20 | 23 | {status === 'success' &&

Form submitted successfully

} 24 |
25 | ); 26 | }; 27 | 28 | const SecondForm = () => { 29 | const [status, actionFunction, isPending] = useActionState( 30 | formAction, 31 | 'idle' 32 | ); 33 | return ( 34 |
35 | 36 | 39 | {status === 'success' &&

Form submitted successfully

} 40 |
41 | ); 42 | }; 43 | 44 | const ParentComponent = () => { 45 | return ( 46 | <> 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | const formStyles = { 54 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 55 | input: 56 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 57 | button: 58 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200', 59 | }; 60 | 61 | export default ParentComponent; 62 | -------------------------------------------------------------------------------- /src/final/02-actions/03-hook.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useActionState } from 'react'; 3 | import { API_URL } from '../../utils'; 4 | 5 | const formAction = async ( 6 | prevState: string, 7 | formData: FormData 8 | ): Promise => { 9 | try { 10 | const data = Object.fromEntries(formData); 11 | await new Promise((resolve) => setTimeout(resolve, 2000)); 12 | if (data.name === 'bobo') { 13 | throw new Error('Name is invalid'); 14 | } 15 | const response = await axios.post(`${API_URL}/users`, data); 16 | console.log(response.data); 17 | return 'success'; 18 | } catch (error) { 19 | return 'error'; 20 | } 21 | }; 22 | 23 | const Component = () => { 24 | const [status, actionFunction, isPending] = useActionState( 25 | formAction, 26 | 'idle' 27 | ); 28 | 29 | return ( 30 |
31 | 38 | 45 | 48 | {status === 'error' &&

Error !!!

} 49 | {status === 'success' && ( 50 |

Success !!!

51 | )} 52 |
53 | ); 54 | }; 55 | export default Component; 56 | 57 | const formStyles = { 58 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 59 | input: 60 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 61 | button: 62 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200', 63 | }; 64 | -------------------------------------------------------------------------------- /src/final/03-useFormStatus/02-final.tsx: -------------------------------------------------------------------------------- 1 | import { useFormStatus } from 'react-dom'; 2 | import { useActionState } from 'react'; 3 | 4 | const SubmitButton = () => { 5 | const { pending } = useFormStatus(); 6 | 7 | const btnStyles = 8 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200'; 9 | return ( 10 | 13 | ); 14 | }; 15 | 16 | const formAction = async ( 17 | prevState: string, 18 | formData: FormData 19 | ): Promise => { 20 | await new Promise((resolve) => setTimeout(resolve, 2000)); 21 | console.log(formData); 22 | return 'success'; 23 | }; 24 | 25 | const FirstForm = () => { 26 | const [status, actionFunction] = useActionState( 27 | formAction, 28 | 'idle' 29 | ); 30 | return ( 31 |
32 | 33 | 34 | {status === 'success' &&

Form submitted successfully

} 35 | 36 | ); 37 | }; 38 | 39 | const SecondForm = () => { 40 | const [status, actionFunction] = useActionState( 41 | formAction, 42 | 'idle' 43 | ); 44 | return ( 45 |
46 | 47 | 48 | {status === 'success' &&

Form submitted successfully

} 49 | 50 | ); 51 | }; 52 | 53 | const ParentComponent = () => { 54 | return ( 55 | <> 56 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | const formStyles = { 63 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 64 | input: 65 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 66 | }; 67 | 68 | export default ParentComponent; 69 | -------------------------------------------------------------------------------- /src/final/02-actions/04-complex.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useActionState } from 'react'; 3 | import { API_URL } from '../../utils'; 4 | type Status = 'success' | 'error' | 'idle'; 5 | type FormState = { status: Status; name: string }; 6 | 7 | const formAction = async ( 8 | prevState: FormState, 9 | formData: FormData 10 | ): Promise => { 11 | try { 12 | const data = Object.fromEntries(formData); 13 | await new Promise((resolve) => setTimeout(resolve, 2000)); 14 | if (data.name === 'bobo') { 15 | throw new Error('Name is invalid'); 16 | } 17 | const response = await axios.post(`${API_URL}/users`, data); 18 | console.log(response.data); 19 | return { status: 'success', name: data.name as string }; 20 | } catch (error) { 21 | return { status: 'error', name: prevState.name }; 22 | } 23 | }; 24 | 25 | const Component = () => { 26 | const [state, actionFunction, isPending] = useActionState< 27 | FormState, 28 | FormData 29 | >(formAction, { 30 | status: 'idle', 31 | name: '', 32 | }); 33 | 34 | return ( 35 |
36 | {!isPending && state.name && ( 37 |

38 | last user : {state.name} 39 |

40 | )} 41 | 48 | 55 | 58 | {!isPending && state?.status === 'error' && ( 59 |

Error !!!

60 | )} 61 | {!isPending && state?.status === 'success' && ( 62 |

Success !!!

63 | )} 64 |
65 | ); 66 | }; 67 | export default Component; 68 | 69 | const formStyles = { 70 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 71 | input: 72 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 73 | button: 74 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200', 75 | }; 76 | -------------------------------------------------------------------------------- /src/final/02-actions/01-traditional-approach.tsx: -------------------------------------------------------------------------------- 1 | import { useState, FormEvent } from 'react'; 2 | import axios from 'axios'; 3 | import { API_URL } from '../../utils'; 4 | type Status = 'success' | 'error' | 'idle'; 5 | 6 | const Component = () => { 7 | const [name, setName] = useState(''); 8 | const [email, setEmail] = useState(''); 9 | const [isPending, setIsPending] = useState(false); 10 | const [status, setStatus] = useState('idle'); 11 | 12 | const handleSubmit = async (e: FormEvent) => { 13 | e.preventDefault(); 14 | setIsPending(true); 15 | setStatus('idle'); 16 | 17 | try { 18 | await new Promise((resolve) => setTimeout(resolve, 1000)); 19 | if (name === 'bobo') { 20 | throw new Error('Name is invalid'); 21 | } 22 | const response = await axios.post(`${API_URL}/users`, { 23 | name, 24 | email, 25 | }); 26 | console.log(response.data); 27 | // Clear form after successful submission 28 | setName(''); 29 | setEmail(''); 30 | setStatus('success'); 31 | } catch (err) { 32 | setStatus('error'); 33 | } finally { 34 | setIsPending(false); 35 | } 36 | }; 37 | 38 | return ( 39 |
40 | setName(e.target.value)} 45 | required 46 | className={formStyles.input} 47 | placeholder='name' 48 | /> 49 | setEmail(e.target.value)} 54 | required 55 | className={formStyles.input} 56 | placeholder='email' 57 | /> 58 | 61 | {status === 'error' &&

Error !!!

} 62 | {status === 'success' && ( 63 |

Success !!!

64 | )} 65 |
66 | ); 67 | }; 68 | export default Component; 69 | 70 | const formStyles = { 71 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 72 | input: 73 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 74 | button: 75 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200', 76 | }; 77 | -------------------------------------------------------------------------------- /src/tutorial/02-actions/01-traditional-approach.tsx: -------------------------------------------------------------------------------- 1 | import { useState, FormEvent } from 'react'; 2 | import axios from 'axios'; 3 | import { API_URL } from '../../utils'; 4 | type Status = 'success' | 'error' | 'idle'; 5 | 6 | const Component = () => { 7 | const [name, setName] = useState(''); 8 | const [email, setEmail] = useState(''); 9 | const [isPending, setIsPending] = useState(false); 10 | const [status, setStatus] = useState('idle'); 11 | 12 | const handleSubmit = async (e: FormEvent) => { 13 | e.preventDefault(); 14 | setIsPending(true); 15 | setStatus('idle'); 16 | 17 | try { 18 | await new Promise((resolve) => setTimeout(resolve, 1000)); 19 | if (name === 'bobo') { 20 | throw new Error('Name is invalid'); 21 | } 22 | const response = await axios.post(`${API_URL}/users`, { 23 | name, 24 | email, 25 | }); 26 | console.log(response.data); 27 | // Clear form after successful submission 28 | setName(''); 29 | setEmail(''); 30 | setStatus('success'); 31 | } catch (err) { 32 | setStatus('error'); 33 | } finally { 34 | setIsPending(false); 35 | } 36 | }; 37 | 38 | return ( 39 |
40 | setName(e.target.value)} 45 | required 46 | className={formStyles.input} 47 | placeholder='name' 48 | /> 49 | setEmail(e.target.value)} 54 | required 55 | className={formStyles.input} 56 | placeholder='email' 57 | /> 58 | 61 | {status === 'error' &&

Error !!!

} 62 | {status === 'success' && ( 63 |

Success !!!

64 | )} 65 |
66 | ); 67 | }; 68 | export default Component; 69 | 70 | const formStyles = { 71 | container: 'max-w-md mx-auto mt-24 p-8 space-y-4 bg-white rounded shadow', 72 | input: 73 | 'w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500', 74 | button: 75 | 'w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-200', 76 | }; 77 | -------------------------------------------------------------------------------- /src/tutorial/04-useOptimistic/02-final.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useEffect, 3 | useState, 4 | useActionState, 5 | useOptimistic, 6 | useTransition, 7 | } from 'react'; 8 | import { useFormStatus } from 'react-dom'; 9 | import axios from 'axios'; 10 | import { API_URL } from '../../utils'; 11 | 12 | type FormState = { 13 | error: string | null; 14 | success: boolean; 15 | }; 16 | 17 | type Todo = { 18 | id: string; 19 | text: string; 20 | completed: boolean; 21 | }; 22 | 23 | function SubmitButton() { 24 | const { pending } = useFormStatus(); 25 | 26 | return ( 27 | 34 | ); 35 | } 36 | 37 | const TodoList = () => { 38 | const [todos, setTodos] = useState([]); 39 | 40 | const [formState, formAction] = useActionState( 41 | async (prevState: FormState, formData: FormData): Promise => { 42 | const text = formData.get('todo') as string; 43 | if (!text?.trim()) { 44 | return { error: 'Todo text is required !!!', success: false }; 45 | } 46 | try { 47 | // Actual API call 48 | await axios.post(`${API_URL}/todos`, { text, completed: false }); 49 | await fetchTodos(); // Refresh the list 50 | return { error: null, success: true }; 51 | } catch (error) { 52 | return { error: 'Failed to add todo', success: false }; 53 | } 54 | }, 55 | { error: null, success: false } 56 | ); 57 | 58 | const fetchTodos = async () => { 59 | const response = await axios.get(`${API_URL}/todos`); 60 | setTodos(response.data); 61 | }; 62 | 63 | const toggleTodo = (todo: Todo) => todo; 64 | 65 | useEffect(() => { 66 | fetchTodos(); 67 | }, []); 68 | 69 | return ( 70 |
71 |

Todo List

72 | 73 |
    74 | {todos.map((todo) => ( 75 |
  • 79 | toggleTodo(todo)} 83 | className='h-5 w-5 rounded border-gray-300' 84 | /> 85 | 90 | {todo.id} {todo.text} 91 | 92 |
  • 93 | ))} 94 |
95 | 96 |
97 | 103 | 104 | {formState.success &&

Success!!!

} 105 | {formState.error && ( 106 |

{formState.error}

107 | )} 108 | 109 |
110 | ); 111 | }; 112 | 113 | export default TodoList; 114 | -------------------------------------------------------------------------------- /src/final/04-useOptimistic/01-starter.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useActionState } from 'react'; 2 | import { useFormStatus } from 'react-dom'; 3 | import axios from 'axios'; 4 | import { API_URL } from '../../utils'; 5 | 6 | type FormState = { 7 | error: string | null; 8 | success: boolean; 9 | }; 10 | 11 | type Todo = { 12 | id: string; 13 | text: string; 14 | completed: boolean; 15 | }; 16 | 17 | function SubmitButton() { 18 | const { pending } = useFormStatus(); 19 | 20 | return ( 21 | 28 | ); 29 | } 30 | 31 | const TodoList = () => { 32 | const [todos, setTodos] = useState([]); 33 | 34 | const fetchTodos = async () => { 35 | await new Promise((resolve) => setTimeout(resolve, 2000)); 36 | const response = await axios.get(`${API_URL}/todos`); 37 | setTodos(response.data); 38 | }; 39 | // Action handler for form submission 40 | const [formState, formAction] = useActionState( 41 | async (prevState: FormState, formData: FormData): Promise => { 42 | const text = formData.get('todo') as string; 43 | if (!text?.trim()) { 44 | return { error: 'Todo text is required !!!', success: false }; 45 | } 46 | try { 47 | // Actual API call 48 | await axios.post(`${API_URL}/todos`, { text, completed: false }); 49 | await fetchTodos(); // Refresh the list 50 | return { error: null, success: true }; 51 | } catch (error) { 52 | return { error: 'Failed to add todo', success: false }; 53 | } 54 | }, 55 | { error: null, success: false } 56 | ); 57 | 58 | const toggleTodo = async (todo: Todo) => { 59 | try { 60 | await axios.put(`${API_URL}/todos/${todo.id}`, { 61 | ...todo, 62 | completed: !todo.completed, 63 | }); 64 | await fetchTodos(); 65 | } catch (error) { 66 | console.error('Failed to update todo:', error); 67 | } 68 | }; 69 | 70 | useEffect(() => { 71 | fetchTodos(); 72 | }, []); 73 | 74 | return ( 75 |
76 |

Todo List

77 | 78 |
    79 | {todos.map((todo) => ( 80 |
  • 84 | toggleTodo(todo)} 88 | className='h-5 w-5 rounded border-gray-300' 89 | /> 90 | 95 | {todo.id} {todo.text} 96 | 97 |
  • 98 | ))} 99 |
100 | 101 |
102 | 108 | 109 | 110 | {formState.success &&

Success!!!

} 111 | {formState.error && ( 112 |

{formState.error}

113 | )} 114 |
115 | ); 116 | }; 117 | 118 | export default TodoList; 119 | -------------------------------------------------------------------------------- /src/tutorial/04-useOptimistic/01-starter.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useActionState } from 'react'; 2 | import { useFormStatus } from 'react-dom'; 3 | import axios from 'axios'; 4 | import { API_URL } from '../../utils'; 5 | 6 | type FormState = { 7 | error: string | null; 8 | success: boolean; 9 | }; 10 | 11 | type Todo = { 12 | id: string; 13 | text: string; 14 | completed: boolean; 15 | }; 16 | 17 | function SubmitButton() { 18 | const { pending } = useFormStatus(); 19 | 20 | return ( 21 | 28 | ); 29 | } 30 | 31 | const TodoList = () => { 32 | const [todos, setTodos] = useState([]); 33 | 34 | const fetchTodos = async () => { 35 | await new Promise((resolve) => setTimeout(resolve, 2000)); 36 | const response = await axios.get(`${API_URL}/todos`); 37 | setTodos(response.data); 38 | }; 39 | // Action handler for form submission 40 | const [formState, formAction] = useActionState( 41 | async (prevState: FormState, formData: FormData): Promise => { 42 | const text = formData.get('todo') as string; 43 | if (!text?.trim()) { 44 | return { error: 'Todo text is r equired !!!', success: false }; 45 | } 46 | try { 47 | // Actual API call 48 | await axios.post(`${API_URL}/todos`, { text, completed: false }); 49 | await fetchTodos(); // Refresh the list 50 | return { error: null, success: true }; 51 | } catch (error) { 52 | return { error: 'Failed to add todo', success: false }; 53 | } 54 | }, 55 | { error: null, success: false } 56 | ); 57 | 58 | const toggleTodo = async (todo: Todo) => { 59 | try { 60 | await axios.put(`${API_URL}/todos/${todo.id}`, { 61 | ...todo, 62 | completed: !todo.completed, 63 | }); 64 | await fetchTodos(); 65 | } catch (error) { 66 | console.error('Failed to update todo:', error); 67 | } 68 | }; 69 | 70 | useEffect(() => { 71 | fetchTodos(); 72 | }, []); 73 | 74 | return ( 75 |
76 |

Todo List

77 | 78 |
    79 | {todos.map((todo) => ( 80 |
  • 84 | toggleTodo(todo)} 88 | className='h-5 w-5 rounded border-gray-300' 89 | /> 90 | 95 | {todo.id} {todo.text} 96 | 97 |
  • 98 | ))} 99 |
100 | 101 |
102 | 108 | 109 | 110 | {formState.success &&

Success!!!

} 111 | {formState.error && ( 112 |

{formState.error}

113 | )} 114 |
115 | ); 116 | }; 117 | 118 | export default TodoList; 119 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/final/04-useOptimistic/02-final.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useEffect, 3 | useState, 4 | useActionState, 5 | useOptimistic, 6 | useTransition, 7 | } from 'react'; 8 | import { useFormStatus } from 'react-dom'; 9 | import axios from 'axios'; 10 | import { API_URL } from '../../utils'; 11 | 12 | type FormState = { 13 | error: string | null; 14 | success: boolean; 15 | }; 16 | 17 | type Todo = { 18 | id: string; 19 | text: string; 20 | completed: boolean; 21 | }; 22 | 23 | function SubmitButton() { 24 | const { pending } = useFormStatus(); 25 | 26 | return ( 27 | 34 | ); 35 | } 36 | 37 | const TodoList = () => { 38 | const [todos, setTodos] = useState([]); 39 | const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos); 40 | const [isPending, startTransition] = useTransition(); 41 | 42 | const fetchTodos = async () => { 43 | const response = await axios.get(`${API_URL}/todos`); 44 | setTodos(response.data); 45 | }; 46 | // Action handler for form submission 47 | const [formState, formAction] = useActionState( 48 | async (prevState: FormState, formData: FormData): Promise => { 49 | const text = formData.get('todo') as string; 50 | if (!text?.trim()) { 51 | return { error: 'Todo text is required !!!', success: false }; 52 | } 53 | try { 54 | // 55 | setOptimisticTodos((prevTodos) => [ 56 | ...prevTodos, 57 | { id: 'OPTIMISTIC : ', text, completed: false }, 58 | ]); 59 | // Actual API call 60 | await axios.post(`${API_URL}/todos`, { text, completed: false }); 61 | await fetchTodos(); // Refresh the list 62 | return { error: null, success: true }; 63 | } catch (error) { 64 | return { error: 'Failed to add todo', success: false }; 65 | } 66 | }, 67 | { error: null, success: false } 68 | ); 69 | 70 | const toggleTodo = (todo: Todo) => { 71 | startTransition(async () => { 72 | const todoToUpdate = { ...todo, completed: !todo.completed }; 73 | setOptimisticTodos((prevTodos) => 74 | prevTodos.map((t) => (t.id === todo.id ? todoToUpdate : t)) 75 | ); 76 | try { 77 | await new Promise((resolve) => setTimeout(resolve, 2000)); 78 | await axios.put(`${API_URL}/todos/${todo.id}`, { 79 | ...todo, 80 | completed: !todo.completed, 81 | }); 82 | await fetchTodos(); 83 | } catch (error) { 84 | setOptimisticTodos((prevTodos) => 85 | prevTodos.map((t) => (t.id === todo.id ? todo : t)) 86 | ); 87 | } 88 | }); 89 | }; 90 | 91 | useEffect(() => { 92 | fetchTodos(); 93 | }, []); 94 | 95 | return ( 96 |
97 |

Todo List

98 | 99 |
    100 | {optimisticTodos.map((todo) => ( 101 |
  • 105 | toggleTodo(todo)} 109 | className='h-5 w-5 rounded border-gray-300' 110 | /> 111 | 116 | {todo.id} {todo.text} 117 | 118 |
  • 119 | ))} 120 |
121 | 122 |
123 | 129 | 130 | 131 | {formState.success &&

Success!!!

} 132 | {formState.error && ( 133 |

{formState.error}

134 | )} 135 |
136 | ); 137 | }; 138 | 139 | export default TodoList; 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React 19 Tutorial 2 | 3 | ## Bootstrap Project 4 | 5 | - `npm i ` 6 | - `npm run dev` 7 | 8 | ## Project Overview 9 | 10 | Will cover the entire setup of the project (step by step) towards the end of the tutorial. 11 | 12 | Boilerplate Vite React-TS. Added: 13 | 14 | - latest react and react-dom, as well as types for both 15 | - JSON Server for mock API (localhost:3001) 16 | [JSON Server](https://www.npmjs.com/package/json-server) 17 | - Axios for API calls 18 | - TailwindCSS for styling 19 | - src/utils.ts for API_URL 20 | 21 | Reference package.json 22 | 23 | ## React 19 24 | 25 | - src/final - complete source code 26 | - src/tutorial - sandbox for each step where we will test the features 27 | 28 | ## use 29 | 30 | use is a React API that lets you read the value of a resource like a Promise or context. 31 | 32 | - Key Features: 33 | - More flexible than traditional hooks - can be used in loops and conditionals 34 | - Works with both Promises and Context 35 | - Integrates with Suspense and Error Boundaries- Can be used anywhere in a component (not just at the top level) 36 | 37 | Common Use Cases: 38 | 39 | ```tsx 40 | // Reading Context 41 | const theme = use(ThemeContext); 42 | 43 | // Handling Promises 44 | function Message({ messagePromise }) { 45 | const message = use(messagePromise); 46 | return

{message}

; 47 | } 48 | ``` 49 | 50 | Important Notes: 51 | 52 | - Must be called inside a Component or Hook 53 | - For Server Components, prefer async/await over use 54 | - When using with Promises, wrap components in Suspense/Error Boundaries 55 | - Cannot be used in try-catch blocks 56 | 57 | Best Practice: 58 | 59 | ```tsx 60 | // Preferred: Create Promises in Server Components 61 | }> 62 | 63 | 64 | ``` 65 | 66 | ## React Actions 67 | 68 | - React 19 introduced a new way to handle form submissions using the `action` attribute. 69 | - This approach simplifies form handling and provides built-in loading states and error handling. 70 | - if you are familiar with `server actions` in Next.js, you will feel right at home. 71 | 72 | Evolution of Form Handling 73 | 74 | 1. Traditional HTML Forms 75 | 76 | ```tsx 77 |
78 | 79 | 80 |
81 | ``` 82 | 83 | - Simple but requires page refresh 84 | - No built-in state management 85 | - Limited user feedback 86 | 87 | 2. React's Traditional Approach 88 | 89 | ```tsx 90 | function Form() { 91 | const [isPending, setIsPending] = useState(false); 92 | 93 | const handleSubmit = async (e) => { 94 | e.preventDefault(); 95 | setIsPending(true); 96 | // handle submission 97 | }; 98 | 99 | return
...
; 100 | } 101 | ``` 102 | 103 | - Client-side handling 104 | - Manual state management 105 | - Requires preventDefault() 106 | - requires handling loading and error states 107 | 108 | 3. New React Actions (React 19) 109 | 110 | ```tsx 111 | function Form() { 112 | async function formAction(formData: FormData) { 113 | // handle submission 114 | } 115 | 116 | return
...
; 117 | } 118 | ``` 119 | 120 | - No preventDefault() needed 121 | - Automatic FormData handling 122 | - Form reset on success 123 | 124 | - Built-in loading state (useActionState) 125 | - easier Error handling (useActionState) 126 | 127 | ## useActionState hook 128 | 129 | useActionState Hook 130 | 131 | Purpose 132 | 133 | A React hook that manages state for asynchronous actions, particularly useful for form submissions. 134 | 135 | ```tsx 136 | const [state, dispatch, isPending] = useActionState(action, initialState); 137 | ``` 138 | 139 | Returns 140 | 141 | - state: Current state value 142 | - dispatch: Function to trigger the action 143 | - isPending: Boolean indicating if action is processing 144 | 145 | Example 146 | 147 | ```tsx 148 | function Form() { 149 | const [state, formAction, isPending] = useActionState( 150 | async (prevState, formData) => { 151 | // Process form data 152 | return { status: 'success' }; 153 | }, 154 | { status: 'idle' } 155 | ); 156 | 157 | return ( 158 |
159 | 162 |
163 | ); 164 | } 165 | ``` 166 | 167 | Key Points 168 | 169 | - Combines state management with async actions 170 | - Automatically handles loading states 171 | - Type-safe when used with TypeScript 172 | - Designed to work with form submissions 173 | - Similar to useReducer but with built-in async support 174 | 175 | Types 176 | 177 | ```tsx 178 | useActionState(action, initialState); 179 | ``` 180 | 181 | ## useFormStatus hook 182 | 183 | ## useOptimistic hook 184 | 185 | ## useTransition hook 186 | 187 | ## compiler 188 | 189 | will cover at the very end of the tutorial 190 | 191 | ## Create React 19 TypeScript Project 192 | 193 | - Node.js Version ? 194 | 195 | ### Bootstrap Vite Project 196 | 197 | ```bash 198 | npm create vite@latest react-19-tutorial -- --template react-ts 199 | ``` 200 | 201 | ### React 19 202 | 203 | ```json 204 | "dependencies": { 205 | "react": "^18.3.1", 206 | "react-dom": "^18.3.1" 207 | } 208 | ``` 209 | 210 | ### React 19 211 | 212 | libraries 213 | 214 | ```bash 215 | npm install react@19.0.0 react-dom@19.0.0 216 | ``` 217 | 218 | types 219 | 220 | ```bash 221 | npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0 222 | 223 | ``` 224 | 225 | ### React Compiler 226 | 227 | ```tsx 228 | import { memo, useState } from 'react'; 229 | 230 | const Parent = () => { 231 | const [count, setCount] = useState(0); 232 | const [value, setValue] = useState(0); 233 | return ( 234 |
235 |

Parent Component

236 |

Count: {count}

237 | 238 | 239 | {/* */} 240 |
241 | ); 242 | }; 243 | const Child = ({ 244 | value, 245 | setValue, 246 | }: { 247 | value: number; 248 | setValue: (value: number) => void; 249 | }) => { 250 | console.log('Child rendered'); 251 | return ( 252 |
253 |

Child Component

254 |

Value: {value}

255 | 256 |
257 | ); 258 | }; 259 | const OptimizedChild = memo(Child); 260 | 261 | export default Parent; 262 | ``` 263 | 264 | ```bash 265 | npm install -D babel-plugin-react-compiler@beta eslint-plugin-react-compiler@beta 266 | ``` 267 | 268 | eslint.config.js 269 | 270 | ```js 271 | 272 | import reactCompiler from 'eslint-plugin-react-compiler'; 273 | 274 | export default tseslint.config( 275 | ... 276 | plugins: { 277 | ... 278 | 'react-compiler': reactCompiler, 279 | }, 280 | rules: { 281 | ... 282 | 'react-compiler/react-compiler': 'error', 283 | }, 284 | 285 | ); 286 | 287 | ``` 288 | 289 | eslint.config.js 290 | 291 | ```js 292 | import js from '@eslint/js'; 293 | import globals from 'globals'; 294 | import reactHooks from 'eslint-plugin-react-hooks'; 295 | import reactRefresh from 'eslint-plugin-react-refresh'; 296 | import tseslint from 'typescript-eslint'; 297 | import reactCompiler from 'eslint-plugin-react-compiler'; 298 | 299 | export default tseslint.config( 300 | { ignores: ['dist'] }, 301 | { 302 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 303 | files: ['**/*.{ts,tsx}'], 304 | languageOptions: { 305 | ecmaVersion: 2020, 306 | globals: globals.browser, 307 | }, 308 | plugins: { 309 | 'react-hooks': reactHooks, 310 | 'react-refresh': reactRefresh, 311 | 'react-compiler': reactCompiler, 312 | }, 313 | rules: { 314 | ...reactHooks.configs.recommended.rules, 315 | 'react-refresh/only-export-components': [ 316 | 'warn', 317 | { allowConstantExport: true }, 318 | ], 319 | 'react-compiler/react-compiler': 'error', 320 | }, 321 | } 322 | ); 323 | ``` 324 | 325 | vite.config.ts 326 | 327 | ```ts 328 | import { defineConfig } from 'vite'; 329 | import react from '@vitejs/plugin-react'; 330 | const ReactCompilerConfig = {}; 331 | // https://vite.dev/config/ 332 | export default defineConfig({ 333 | plugins: [ 334 | react({ 335 | babel: { 336 | plugins: [['babel-plugin-react-compiler', ReactCompilerConfig]], 337 | }, 338 | }), 339 | ], 340 | }); 341 | ``` 342 | --------------------------------------------------------------------------------