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
}
Note: Think of 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 arrayWhen 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 if statement, 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.

Next up: Time to put components, props, and hooks together in a real, runnable project structure — head to Your First App.