C.W.K.
Stream
Lesson 03 of 07 · published

React 19 Actions and Server Components Typing

~11 min · frameworks, react, react-19, actions, server-components

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"React 19 added Actions and tightened the typing for Server Components. The TypeScript story finally feels complete."

Actions in React 19

React 19 introduced Actions — async functions that handle form submissions, data mutations, and pending states. <form action={createPost}> wires the action up to the form. The action receives a FormData, processes it, and the framework manages pending state.

Hooks like useTransition, useOptimistic, and the new useActionState integrate with Actions to give you ergonomic patterns for optimistic updates and form state. TypeScript types all of these correctly out of the box.

Server Components — async functions returning JSX

A Server Component is a React component that runs on the server. In TypeScript, this means an async function that returns JSX. export default async function Page() { const data = await fetchData(); return <div>{data.title}</div> } — the function is async, can await directly, and TypeScript types the return as Promise<React.ReactNode>.

Client Components stay synchronous. Marking a file with 'use client' at the top means everything in it runs in the browser. The compiler treats them differently: server components can be async; client components can use hooks.

The split is at the boundary

A server component can render a client component as a child. A client component cannot import a server component directly (because the server code wouldn't make sense in the browser). The TypeScript types reflect this split — most of the time it just works, but the error messages when you cross the line are clear enough to fix.

React 19 + TypeScript is the modern React stack. Actions for mutations, Server Components for data, Client Components for interactivity. The types flow correctly through all three.

Code

React 19 — Server Components, Client Components, Actions·tsx
// Server Component — async function, no 'use client'.
export default async function PostsPage() {
  const posts = await fetchPosts();
  return (
    <div>
      {posts.map((p) => <PostCard key={p.id} post={p} />)}
    </div>
  );
}

// Client Component — 'use client' at top, can use hooks.
'use client';
import { useState } from 'react';

export function LikeButton({ postId }: { postId: number }) {
  const [liked, setLiked] = useState(false);
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}

// Action — async function, can handle FormData.
async function createPost(formData: FormData) {
  'use server';
  const title = formData.get('title') as string;
  await db.posts.create({ title });
}

export function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">Create</button>
    </form>
  );
}

External links

Exercise

Sketch a small post-creation flow: a Server Component that lists posts, a Client Component for the 'New Post' button that opens a dialog, and an Action that creates the post. Type each function appropriately. Confirm the types catch a mistake like passing the action's return value to setState.
Hint
Server Component: async function returning JSX. Client Component: 'use client', uses useState. Action: 'use server', takes FormData. TypeScript catches if you try to call the Action from outside its allowed boundary.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.