State & Hooks
Props let a component receive data from its parent, but props alone can't handle data that changes
because of something that happens inside the component itself — a button tap, a
typed character, a network response arriving. For that, React Native components use
state. State is data that can change over time, and whenever it changes, React
automatically re-renders the component to reflect the new value. This lesson covers the two hooks
you'll reach for constantly: useState for state itself, and useEffect for
running code in response to it.
The useState Hook
useState is a function that gives a component its own piece of state. It returns an
array with exactly two things: the current value, and a setter
function used to update it — which is why it's almost always written with array
destructuring.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 0 is the initial value
// count -> the current state value
// setCount -> call this to update it and trigger a re-render
}
Worked Example: A Counter Component
Here's a complete, self-contained counter with an increment button — the classic first example for any hooks-based UI.
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<View style={{ alignItems: 'center', padding: 20 }}>
<Text>Count: {count}</Text>
<Button title="Increment" onPress={() => setCount(count + 1)} />
</View>
);
}
Tapping the button calls setCount(count + 1), React re-runs the component function with
the new count, and the screen shows the updated number.
An Important Rule: Setters Don't Update Instantly
This is one of the most commonly misunderstood points about React state, so it's worth a clear
example: calling setCount(count + 1) schedules a re-render with the
new value — it does not immediately, synchronously change the count
variable in the code that's currently running.
function handlePress() {
setCount(count + 1);
console.log(count); // still logs the OLD value here — the update hasn't applied yet
}
setCount as "please re-render with this new
value soon," not "change this variable right now." The updated count is only visible
the next time the component function runs, with the new value baked into that render.The useEffect Hook
Rendering JSX is meant to be pure — no reaching outside the component. But real apps need to fetch
data, set up subscriptions, or start timers, all of which are "side effects" that happen outside
the normal render flow. The useEffect hook is where that code belongs.
import React, { useState, useEffect } from 'react';
import { View, Text } from 'react-native';
export default function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
async function loadUser() {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
setUser(data);
}
loadUser();
}, []); // empty array: run once, when the component first mounts
if (!user) {
return <Text>Loading...</Text>;
}
return (
<View>
<Text>{user.name}</Text>
</View>
);
}
The Dependency Array, Explained
The second argument to useEffect — the dependency array — controls exactly when the
effect runs. Getting this wrong is a common source of bugs, so it's worth memorizing the three
cases.
| Dependency array | When the effect runs |
|---|---|
useEffect(fn, []) | Once — right after the component first mounts. Ideal for a one-time data fetch. |
useEffect(fn, [userId]) | On mount, and again any time userId changes between renders. |
useEffect(fn) (no array at all) | After every single render — usually not what you want, and easy to turn into an infinite loop if the effect itself triggers a re-render. |
The Rules of Hooks
Hooks like useState and useEffect come with two rules that must always be
followed:
- Only call hooks at the top level of a component function — never inside an
ifstatement, a loop, or a nested function. - Only call hooks from React components (or from other custom hooks), never from regular JavaScript functions.
// wrong — a hook inside a condition
if (isLoggedIn) {
const [user, setUser] = useState(null); // don't do this
}
Why this matters: React tracks each hook by the order it was called in,
not by name. If a hook call is skipped on some renders (because it was inside an if) but
not others, React loses track of which piece of state belongs to which useState call —
leading to state getting mixed up between unrelated hooks. Keeping every hook call unconditional at
the top level is what keeps that call order stable across every render.