This episode covers local state and React hooks in Next.js, the use client directive and server-client boundaries, shared state with Context API, Zustand, and TanStack Query, and the lifecycle differences between client components and server components.

Every interactive application stores changing data: form input values, tab selections, or user data. How that data is stored and shared between components is the domain of state management.
Episode 7 covers local state with React hooks, the use client directive concept and the boundary between server and client components, shared state with Context API, Zustand, and TanStack Query, and the lifecycle differences between the two component types.
Local state is used for data that only one component uses. The useState hook stores a value, and useEffect runs side effects like client-side data fetching:
"use client"
import { useState } from "react"
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Hitungan: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Tambah</button>
</div>
)
}The counter component above is the simplest example of local state. Note the "use client" directive — without it, hooks like useState can't be used because the component is treated as a server component.
A rule of thumb for local state: keep state as low as possible in the component tree. If only one component uses the value, keep it in that component; if several components share it, lift the state up to a common parent or use Context.
The App Router executes components in two places: the server and the client. Server components can read data directly and pass props to client components, but can't use interactive hooks. Client components can use hooks and handle events, but their code is sent to the browser.
The "use client" directive marks the boundary: that component and its descendants become client components. The rule of thumb — components that need interactivity are placed at the bottom of the component tree, while structure and data stay in the server component. This minimizes the JavaScript sent to the browser, a topic we dive into in episode 15.
The directive may only appear at the very top of the file, before any other imports. Placing it wrong produces a compile error — so you won't be lost for long.
The Context API lets state be shared without passing props down every level. It suits global data that rarely changes, like theme or language:
"use client"
import { createContext, useContext, useState } from "react"
const TemaContext = createContext(null)
export function TemaProvider({ children }) {
const [tema, setTema] = useState("terang")
return (
<TemaContext.Provider value={{ tema, setTema }}>
{children}
</TemaContext.Provider>
)
}
export function useTema() {
return useContext(TemaContext)
}useTema() can be called from any component below TemaProvider. Context suits small, rarely changing state; for state that changes often and is large, consider an external store.
Zustand is a minimal store library with a hooks-like API that works well with React 18 and Next.js. It suits complex global state. Meanwhile, TanStack Query (React Query) focuses on server state: caching, retry, and synchronization with APIs — ideal paired with fetching in client components. TanStack Query also handles invalidation: after a successful mutation, cached data can be marked stale and refetched automatically, preventing the view from showing outdated data. The choice depends on the type of state: use Context or Zustand for UI state, and TanStack Query for server state.
Server components render once on the server at build time or per request — they're never re-rendered by the browser and have no interactive hooks. Client components go through the full React lifecycle: mount, update when state changes, and unmount. Therefore:
Understanding this difference avoids classic bugs like hydration mismatch — the difference between server HTML and the initial client render — which usually appears because a component renders random values or local time without following hydration rules.
In addition, server components receiving props from client components must be serializable: functions, Date, or class instances can't be sent as props. Keep props between components limited to primitives, objects, and arrays that can be converted to JSON.
To capture data on both sides, a common pattern is used: the server component fetches data, then passes it as props to the client component that handles interactions. This minimizes the number of fetches that need to be synchronized between server and client.
Here's what to take away:
In the next episode, episode 8, we'll discuss configuration and environment — the next.config.mjs configuration, environment variables and secrets, image optimization and asset handling, and best practices for performance and build optimization. Your application is starting to be tidied up for production.