Chapter 3: React - Declarative UI (Goodbye Jinja2)
As a Python backend developer, you're probably most familiar with Jinja2. In Flask or Django, you fetch data from the database, 'inject' it into an HTML template, the server 'renders' an HTML string, and sends it to the browser. This process is imperative - you tell the template engine 'loop here, insert this variable here'...
Part Two: Next.js Core Paradigms - App Router & RSC
Chapter 3: React - Declarative UI (Goodbye Jinja2)
As a Python backend developer, you're probably most familiar with Jinja2. In Flask or Django, you fetch data from the database, "inject" it into an HTML template, the server "renders" an HTML string, and sends it to the browser. This process is Imperative - you tell the template engine "loop here, insert this variable here".
Welcome to the world of React. React is Declarative.
- Jinja2 (Imperative):
{% for item in items %} <li>{{ item.name }}</li> {% endfor %}. You describe how to build the list. - React (Declarative):
items.map(item => <li key={item.id}>{item.name}</li>). You only describe what you want (a list oflibased on theitemsarray), and React will "automatically" update the DOM most efficiently when data changes.
You're no longer "manipulating HTML", you're "describing UI as a function of state".
3.1. JSX and Component Thinking
The first thing you'll notice is JSX. It looks like HTML, but it's in a JavaScript file.
// This is not an HTML string!
const greeting = <h1 className="title">Hello, World!</h1>;- It's not HTML: Note that
classbecameclassName. JSX is actually "syntactic sugar" for theReact.createElement()function. - It's not a template: It has the full power of JavaScript. You can
map,filter,if/else(using ternary operators).
Component Thinking
In Jinja2, you might use {% include "nav.html" %}. In React, everything is a component.
A Component is a JavaScript function (or class, but we only use functions in our project) that returns JSX.
-
Python analogy: A React component is just a Python function, except its
returnvalue is a UI description. -
Real project: Our
src/components/ui/button.tsxis aButtoncomponent. You can reuse it anywhere:import { Button } from '@/components/ui/button'; function MyPage() { return ( <div> <Button variant="primary">Click Me</Button> <Button variant="destructive">Delete</Button> </div> ); }
3.2. Client Core: State, Props & "use client"
This is the core of React and the entire Next.js App Router.
Props (Properties)
Props are how data flows from parent components to child components.
-
Python analogy: Props are function parameters.
-
Jinja2 analogy: Similar to
{% include "nav.html" with username=user.name %}. -
React example: In the
Buttonexample above,variant="primary"is aprop.// `src/components/ui/button.tsx` (simplified) // Note the type definition - this is TS's "contract" export function Button(props: { variant: string, children: React.ReactNode }) { // 'children' is a special prop representing content between <Button> tags return ( <button className={...}>{props.children}</button> ); }Data flow is unidirectional, from top (parent) to bottom (child).
State (状态)
State is how a component "remembers" information. This is the source of React's interactivity.
- Python analogy:
Stateis like a local variable within a function, but with "magic": when this variable changes, React automatically re-runs the function (re-renders the component). - How to use: You can't directly modify
State. You must useuseState, a "Hook".
import { useState } from 'react';
function Counter() {
// useState returns an array: [current value, function to update value]
const [count, setCount] = useState(0); // Initial value is 0
return (
<div>
<p>You clicked {count} times</p>
{/* onClick is an event handler.
It calls setCount, triggering React to re-render this component
*/}
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}"use client" (Client Boundary)
Now, let's combine the above two concepts with Next.js App Router.
By default, all components in Next.js App Router are React Server Components (RSC)**.
- RSC (Server Components): They only run on the server. They're like "super Jinja2 templates". They can directly access databases (like
src/db/), but they cannot useuseStateoronClick. They have no interactivity. - Client Components: To add interactivity (using
useState,onClick,useEffect), you must mark a file as a client component.
You only need to add one line of string at the very top of the file:
"use client"; // This is not a comment, this is a required directive
import { useState } from 'react';
// Now this component can run on the client and use State
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}Key mindset shift: "use client" does not mean "this component only runs on the client". It means this component is "hybrid": it will be pre-rendered on the server (generating initial HTML), then "hydrated" on the client to become interactive.
3.3. [Deep Dive]: Deep into "use client" Boundaries
"use client" is a Boundary. Once you define a client component, it acts like a "virus" - all other components it imports also become part of the client module graph.
Core Rules:
- Server Components (RSC) can import RSC. (Server -> Server)
- Server Components (RSC) can import Client Components. (Server -> Client)
- Client Components can import Client Components. (Client -> Client)
- Client Components absolutely cannot import Server Components! (❌ Client -> Server ❌)
Why? Because client component code eventually runs in the browser. The browser cannot run a server component that needs to directly access the database or read server files.
So, how to display server content in interactive components? Answer: Use the "Slot" pattern, which is the children prop.
This is one of the most advanced patterns in Next.js. Suppose our src/components/ui/dialog.tsx (a dialog) is a client component (because it needs useState to manage open/close state), but we want to display server content fetched from the database inside the dialog.
1. Server Component (page.tsx):
// This is an RSC, it can access the database
import { db } from '@/db';
import { Modal } from '@/components/ui/modal'; // This is a client component
import { ServerContent } from '@/components/server-content'; // This is an RSC
export default async function Page() {
// RSC can 'await' at the top level
const data = await db.query.users.findFirst(...);
return (
<main>
<h1>My Page</h1>
{/* We "stuff" an RSC (<ServerContent />)
into the 'children' prop of the client component <Modal>
*/}
<Modal>
<ServerContent data={data} />
</Modal>
</main>
);
}2. Client Component (src/components/ui/modal.tsx):
"use client";
import { useState } from 'react';
// 'children' type is React.ReactNode
export function Modal({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<div className="modal-content">
{/* The Modal component doesn't know what 'children' is at all.
It just renders the HTML passed from the server as-is.
This is the "slot".
*/}
{children}
</div>
)}
</div>
);
}Summary: Our architectural goal is to "push interactivity as deep as possible into the leaf nodes of the component tree". Keep page.tsx as RSC, and only use "use client" where you truly need onClick or useState (like Button or Modal).
3.4. [Code Analysis]: Analyzing src/components/ui/ (Radix UI) and src/stores/ (Zustand) in Real Project
src/components/ui/ (Radix UI + Tailwind)
This is our SaaS template's "design system".
- Radix UI: It's a Headless UI library. It provides functionality and accessibility (keyboard navigation, ARIA attributes) for all complex components (like
DropdownMenu,Dialog,Select), but provides no styling. - Tailwind CSS: We use it to add styles to Radix's "skeleton".
src/components/ui/button.tsx: Opening this file, you'll find it's not a simple<button>. It usescva(Class Variance Authority) to manage different style variants (primary,destructive,ghost) and apply Tailwind classes.- Connection to
"use client": Components likeDialogorDropdownMenuneeduseStateinternally to manage toggle state, so they are all client components.
src/stores/ (Zustand)
Problem: useState is great for state within a component. But what if both our Header component and SettingsPage component need to know and modify the user's credits?
"Props Drilling": We could put the credits state in the top-level Layout, then pass it down layer by layer through props. But this is painful and fragile.
Solution: Zustand, a minimalist global state manager.
- Python analogy: If
useStateis a local variable within a function, then aZustandstore is like a global singleton or a module-level variable you canimport. - How it works:
1. Create Store (src/stores/useCreditStore.ts)
import { create } from 'zustand';
// 1. Define "contract" (Chapter 2)
interface CreditStoreState {
credits: number;
setCredits: (amount: number) => void;
deductCredits: (amount: number) => void;
}
// 2. Create store
export const useCreditStore = create<CreditStoreState>((set) => ({
credits: 0, // Initial state
// 'set' function is used to update state
setCredits: (amount) => set({ credits: amount }),
deductCredits: (amount) => set(
(state) => ({ credits: state.credits - amount })
),
}));2. Use Store in Components (Must be client components!)
"use client";
import { useCreditStore } from '@/stores/useCreditStore';
import { useEffect } from 'react';
// Component A: Display credits
function HeaderCreditDisplay() {
// 1. "Subscribe" to the `credits` value in store
const credits = useCreditStore((state) => state.credits);
return <span>Credits: {credits}</span>;
}
// Component B: Modify credits (e.g., load initial value from API)
function UserProfile() {
// 1. "Subscribe" to the `setCredits` method in store
const setCredits = useCreditStore((state) => state.setCredits);
useEffect(() => {
// Assume we fetch user data from API on page load
fetch('/api/user')
.then(res => res.json())
.then(data => {
// 2. Call action to update global state
setCredits(data.credits);
});
}, [setCredits]);
// ...
}Summary: Zustand allows our client components to share and modify global state without passing through props. It's the perfect complement to useState and the core that makes src/credits/ functionality possible.
Categories
src/components/ui/ (Radix UI + Tailwind)src/stores/ (Zustand)More Posts
Chapter 14: CI/CD (GitHub Actions & Vercel)
Welcome to Part Six: DevOps and Quality Assurance. In previous chapters, we've built a fully functional SAAS application covering all core features from database (Drizzle), authentication (better-auth), payments (Stripe) to operations (React Email). Now, it's time to ensure we can deliver these features to our users safely, reliably, and quickly...
Chapter 16: SAAS Testing Strategy and Quality Assurance
In Chapter 14, we established a CI/CD pipeline that acts as the 'gatekeeper' for our SAAS application. In Chapter 15, we used Biome (Linter) to ensure 'static quality' of code. However, a gatekeeper that only checks syntax is far from enough. Our CI process also includes a pnpm test command—this is the core of quality assurance.
Chapter 18: Feature Releases: A/B Testing & Feature Flags
Welcome to Part Seven: Advanced Integration & Architecture Practices. In Part Six, we built a complete DevOps and observability stack. We now have: