This episode covers useState for local state, the synthetic event system, controlled components for form inputs, and how React batches updates and re-renders. Everything is built on the components and JSX from the previous episode.

Static components only display data; an interesting app is one that reacts to its users. Interaction starts with two things: state, which stores changing values, and event handling, which captures user actions. Episode 5 makes you master both.
We'll cover useState for local state management, React's synthetic event system, controlled components for forms, and then how React batches updates and re-renders. This is the foundation of interactivity used in every following episode.
The useState hook returns a pair of values: the current value and a function to update it. The component re-renders every time the update function is called:
import { useState } from "react"
function Counter() {
const [hitung, setHitung] = useState(0)
return (
<div>
<p>Hitungan: {hitung}</p>
<button onClick={() => setHitung(hitung + 1)}>Tambah</button>
</div>
)
}useState(0) sets the initial value 0 and returns hitung plus setHitung. Calling setHitung always replaces the value with a new one and triggers a re-render — this is the only correct way to change state.
Changing a state value directly, like hitung = hitung + 1, doesn't trigger any render. React detects changes through the update function. If the new value depends on the old one, use the callback form:
const tambah = () => setHitung((sekarang) => sekarang + 1)setHitung((sekarang) => sekarang + 1) accepts a function that receives the most recent state value. This form is required when there are several sequential updates or when the new value is computed from the old state.
Events in React are written with an on prefix and a capital letter, like onClick, onChange, and onSubmit, with the handler as the value:
function FormSederhana() {
const handleKlik = () => alert("Tombol diklik")
const handleTeks = (e) => console.log("Diketik:", e.target.value)
return (
<div>
<button onClick={handleKlik}>Klik</button>
<input onChange={handleTeks} placeholder="Ketik di sini" />
</div>
)
}onClick={handleKlik} passes a function reference, not the result of a call. Don't write onClick={handleKlik()} unless you actually want to call it during render.
React wraps browser events in a SyntheticEvent so their behavior is consistent across all browsers. These events have the same properties as the native events, including preventDefault and stopPropagation, but they're attached via delegation to the root DOM, which is more memory-efficient. Property names follow camelCase, for example onChange, not onchange.
In a controlled component, the input value is stored in state and returned through the value attribute. Every keystroke goes through onChange, which updates state, so state is always in sync with the view:
import { useState } from "react"
function FormNama() {
const [nama, setNama] = useState("")
const handleSubmit = (e) => {
e.preventDefault()
console.log("Dikirim:", nama)
}
return (
<form onSubmit={handleSubmit}>
<input value={nama} onChange={(e) => setNama(e.target.value)} />
<button type="submit">Simpan</button>
</form>
)
}e.target.value reads the current text from the event, then setNama stores it in state. Because value always comes from state, the input value never diverges from what React knows — this pattern becomes the basis for validation directly in episode 11.
React combines several setter calls in one event into a single render. That's why reading old state in the middle of a batch won't see the new value:
const handleTambah = () => {
setHitung(hitung + 1)
setHitung(hitung + 1)
}The two calls above only increment hitung by one, because both read the same value from the previous render. Use the callback form so each update uses the result of the previous one:
const handleTambah = () => {
setHitung((sekarang) => sekarang + 1)
setHitung((sekarang) => sekarang + 1)
}setHitung((sekarang) => sekarang + 1) runs sequentially, so the final result is hitung incremented by two. React runs the callbacks one by one within a single batched render.
When state changes, React re-runs the component function and compares the result with the previous render via reconciliation. Only the parts that actually changed are updated in the DOM. Understanding this pattern helps you avoid wasteful renders — a topic covered more deeply in episodes 6 and 15.
Episode 5 gave your app a brain: useState to store and update values, synthetic events to capture interactions, controlled components for forms that always stay in sync, and an understanding of batching and re-rendering that's key to reading React's behavior.
Key takeaways:
useState returns a value and an update function; never mutate state directly.value and onChange.In the next episode, episode 6, we'll cover effects & lifecycle with hooks — useEffect for side effects, effect cleanup and dependency arrays, useMemo and useCallback for optimization, and custom hooks for reusable logic. Your app starts reaching beyond the screen: loading data, listening to global events, and managing timers.