useState
State is the value a component remembers between renders, and only its setter can change it.
const [state, setState] = useState(initialState)State is the memory of a component. A plain variable declared inside a function component is created again on every render, so it cannot remember anything. useState gives you a value React keeps between renders, plus a function that changes it and asks React to draw the component again.
How it works
useState(initialState) returns a two-element array: the current value and a setter. The initial value is used only on the first render; after that React ignores it and hands back what it has stored. The destructuring is shorthand. const [count, setCount] = useState(0) is exactly the same as const s = useState(0); const count = s[0]; const setCount = s[1]; The two names are yours to choose; setX is a convention, not a rule.
Calling the setter does not change the variable you are holding. count is a snapshot of this render. Read it immediately after setCount(1) in the same handler and it is still the old number. The new value arrives with the next render.
What trips people up
Assigning to the variable does nothing visible. count = count + 1 changes a local binding, nothing re-renders, and under strict TypeScript it will not even compile, because destructuring with const makes it a constant. Only the setter schedules a render.
Hooks must be called at the top level of the component, in the same order on every render. Never inside an if, inside a loop, or after an early return — React matches hooks to their stored values by call order.
Example
import { useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
// On screen: the word "Taps", a large number under it, and a button.
// Each press raises the number by one.
export default function Counter() {
// useState returns a pair: the stored value, and a function that replaces it.
const [count, setCount] = useState(0);
// The line above is shorthand for exactly this longhand:
// const state = useState(0);
// const count = state[0];
// const setCount = state[1];
return (
<View style={styles.screen}>
<Text style={styles.label}>Taps</Text>
{/* A number must still be inside <Text>; a bare value in a View throws. */}
<Text style={styles.count}>{count}</Text>
<Pressable style={styles.button} onPress={() => setCount(count + 1)}>
<Text style={styles.buttonText}>Tap me</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
// flexDirection defaults to column here, not row as in CSS.
screen: { flex: 1, alignItems: "center", justifyContent: "center", gap: 12 },
label: { fontSize: 16, color: "#555555" },
count: { fontSize: 48, fontWeight: "600" },
button: { backgroundColor: "#1f6feb", paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8 },
buttonText: { color: "#ffffff", fontSize: 16 },
});When to use it
- Track whether a panel is open so one button can show and hide it
- Hold the current text of a search field so other parts of the screen can read it
- Remember which tab or filter chip the user selected
- Count something the user does, such as items added to a cart
More examples
One useState call per independent value
Three separate values, so three calls. Keeping them apart means each setter replaces only its own value and you never rebuild an object by hand. Note where a type annotation is needed: useState("free") widens to string, so a union like Plan has to be stated. On screen: an email field, a switch with a label, and a line showing the current plan that toggles when pressed.
import { useState } from "react";
import { Pressable, StyleSheet, Switch, Text, TextInput, View } from "react-native";
type Plan = "free" | "pro";
export default function SignUpFields() {
const [email, setEmail] = useState(""); // inferred as string
const [agreed, setAgreed] = useState(false); // inferred as boolean
const [plan, setPlan] = useState<Plan>("free"); // annotated: "free" alone would infer string
return (
<View style={styles.box}>
<TextInput
style={styles.input}
value={email}
onChangeText={setEmail}
placeholder="[email protected]"
keyboardType="email-address"
autoCapitalize="none"
/>
<View style={styles.row}>
<Switch value={agreed} onValueChange={setAgreed} />
<Text>I agree to the terms</Text>
</View>
<Pressable onPress={() => setPlan(plan === "free" ? "pro" : "free")}>
<Text>Plan: {plan} (tap to change)</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
box: { padding: 16, gap: 12 },
input: { borderWidth: 1, borderColor: "#cccccc", borderRadius: 6, padding: 10 },
row: { flexDirection: "row", alignItems: "center", gap: 8 },
});An expensive initial value goes in a function
The lazy initialiser. Reach for it when the starting value costs real work to produce, such as parsing stored data or building a large array. Destructuring only the first element is fine when nothing changes the value. On screen: one line of text giving the cell count.
import { useState } from "react";
import { StyleSheet, Text, View } from "react-native";
function buildGrid(size: number): number[] {
return Array.from({ length: size * size }, (_, index) => index);
}
export default function Board() {
// Passing a FUNCTION makes React call it on the first render only.
const [cells] = useState(() => buildGrid(40));
// useState(buildGrid(40)) also works, but then buildGrid runs on every
// render and the result is thrown away on all but the first.
return (
<View style={styles.screen}>
<Text>{cells.length} cells ready</Text>
</View>
);
}
const styles = StyleSheet.create({
screen: { padding: 16 },
});Assignment does nothing; the setter is the only way
The two mistakes side by side. Pressing "Like" raises the number; pressing "Broken like" changes nothing on screen. The console.log inside the working handler is the part worth remembering: the state variable keeps its old value for the rest of that handler.
import { useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
export default function Likes() {
const [likes, setLikes] = useState(0);
function wrong() {
// Uncommenting this fails to compile: likes is a constant for this render.
// likes = likes + 1;
console.log("nothing happens");
}
function right() {
setLikes(likes + 1); // asks React to render again with the new value
console.log(likes); // still the OLD number: this render's snapshot
}
return (
<View style={styles.screen}>
<Text style={styles.count}>{likes}</Text>
<Pressable onPress={right}>
<Text>Like</Text>
</Pressable>
<Pressable onPress={wrong}>
<Text>Broken like</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { padding: 16, gap: 12 },
count: { fontSize: 32 },
});
Discussion