React Native Academy · บทเรียน

การจัดการสถานะคอมโพเนนต์ด้วย useState

ทำความรู้จักฮุก useState จัดเก็บค่าตัวนับหรือค่าการสลับไว้ในสถานะ และทำให้เกิดการเรนเดอร์ใหม่ด้วยการเรียกฟังก์ชันตั้งค่าสถานะ

บทเรียน 2 จาก 413 ขั้นตอน

การจัดการสถานะคอมโพเนนต์ด้วย useState เป็นบทเรียน React Native Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน React Native Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส React Native Academy มีบทเรียนทั้งหมด 4 บทเรียน

บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ

What Is State?

State is data that can change over time and that a component owns and manages internally. Unlike props (which come from the parent and are read-only), state is created and updated by the component itself. When state changes, React automatically re-renders the component to reflect the new values in the UI. Common state examples include: whether a modal is open, the text the user has typed, the currently selected tab, or items fetched from an API. State is what makes React components interactive.

import { View, Text, TouchableOpacity } from 'react-native';
import { useState } from 'react';

export default function Counter() {
  // Declare state: count starts at 0
  const [count, setCount] = useState(0);

  return (
    <View style={{ padding: 24, alignItems: 'center' }}>
      <Text style={{ fontSize: 48, fontWeight: 'bold' }}>{count}</Text>
      <TouchableOpacity
        onPress={() => setCount(count + 1)}
        style={{ marginTop: 16, padding: 12, backgroundColor: '#4f86f7', borderRadius: 8 }}
      >
        <Text style={{ color: '#fff', fontSize: 18 }}>Increment</Text>
      </TouchableOpacity>
    </View>
  );
}

The useState Hook Signature

useState is a React Hook that you call at the top level of your component function. It takes one argument — the initial state value — and returns an array with two elements: the current state value and a setter function. You destructure these using array destructuring. The naming convention is [value, setValue]. The setter function is what you call to update state — never mutate the state variable directly. Each state variable is independent; call useState multiple times for multiple pieces of state.

import { useState } from 'react';

function MyComponent() {
  // Syntax: const [value, setter] = useState(initialValue);
  const [count, setCount] = useState(0);          // number
  const [name, setName] = useState('');            // string
  const [isVisible, setIsVisible] = useState(false); // boolean
  const [items, setItems] = useState([]);          // array
  const [user, setUser] = useState(null);          // object or null

  // Update state by calling the setter:
  // setCount(5);          ← sets to 5
  // setName('Alice');     ← sets to 'Alice'
  // setIsVisible(true);   ← sets to true
}

State Updates Trigger Re-Renders

When you call a state setter, React schedules a re-render of the component. During the next render, useState returns the new value. React is smart about this — it batches multiple state updates that happen in the same event handler and does a single re-render. Also important: the state value inside the current render is immutable. Calling setCount(count + 1) does not immediately change count in the current function execution — the new value appears in the next render cycle.

import { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';

export default function Example() {
  const [count, setCount] = useState(0);

  function handlePress() {
    // These do NOT stack: count is still 0 in this render
    setCount(count + 1); // schedules count = 1
    setCount(count + 1); // also schedules count = 1, not 2!

    // To correctly base on previous value, use functional update:
    // setCount(prev => prev + 1); // prev = 0, result = 1
    // setCount(prev => prev + 1); // prev = 1, result = 2
  }

  return (
    <TouchableOpacity onPress={handlePress}>
      <Text>{count}</Text>
    </TouchableOpacity>
  );
}

Functional Updates with Previous State

When your new state depends on the previous state, always use the functional update form: setState(prev => prev + 1). React guarantees that prev is the most recent state value, even if multiple updates are batched. This is critical when incrementing counters, toggling booleans, or adding items to arrays in rapid succession (e.g., fast button taps). The object literal form (setState(state + 1)) can produce bugs when multiple updates happen before the component re-renders.

import { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';

export default function SafeCounter() {
  const [count, setCount] = useState(0);

  function increment() {
    // Safe: uses previous state, handles batching correctly
    setCount(prev => prev + 1);
  }

  function decrement() {
    setCount(prev => Math.max(0, prev - 1)); // min 0
  }

  function reset() {
    setCount(0); // direct value ok when not based on previous
  }

  return (
    <View style={{ alignItems: 'center', gap: 12, padding: 24 }}>
      <Text style={{ fontSize: 48 }}>{count}</Text>
      <TouchableOpacity onPress={increment}><Text>+</Text></TouchableOpacity>
      <TouchableOpacity onPress={decrement}><Text>-</Text></TouchableOpacity>
      <TouchableOpacity onPress={reset}><Text>Reset</Text></TouchableOpacity>
    </View>
  );
}

Toggling Boolean State

A very common pattern is toggling a boolean state between true and false. Use the functional update form with the logical NOT operator: setIsOpen(prev => !prev). This is cleaner than setIsOpen(!isOpen) because it correctly handles rapid presses. Common use cases include showing/hiding a modal, expanding/collapsing an accordion section, switching between play and pause, or toggling a favorite star icon. Booleans are perhaps the most frequently used type of component state.

import { useState } from 'react';
import { View, Text, TouchableOpacity, Modal, StyleSheet } from 'react-native';

export default function ToggleModal() {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <View style={styles.container}>
      <TouchableOpacity
        style={styles.btn}
        onPress={() => setIsVisible(prev => !prev)}
      >
        <Text style={styles.btnText}>Toggle Modal</Text>
      </TouchableOpacity>

      <Modal visible={isVisible} transparent animationType='fade'>
        <View style={styles.overlay}>
          <Text style={styles.modalText}>Hello from the modal!</Text>
          <TouchableOpacity onPress={() => setIsVisible(false)}>
            <Text style={{ color: '#fff', marginTop: 16 }}>Close</Text>
          </TouchableOpacity>
        </View>
      </Modal>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  btn: { padding: 12, backgroundColor: '#4f86f7', borderRadius: 8 },
  btnText: { color: '#fff', fontWeight: 'bold' },
  overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'center', alignItems: 'center' },
  modalText: { color: '#fff', fontSize: 20, fontWeight: 'bold' },
});

State with Strings: Controlled Text Input

Binding a TextInput to state creates a controlled input. The value prop sets the displayed text (from state), and the onChangeText callback updates the state whenever the user types. This makes the state the single source of truth for the input's value — you can read, validate, or transform the text at any time by reading the state variable. Without binding to state (uncontrolled), you would need a ref to read the input value, which is less idiomatic in React.

import { useState } from 'react';
import { View, Text, TextInput, StyleSheet } from 'react-native';

export default function SearchBox() {
  const [query, setQuery] = useState('');

  return (
    <View style={styles.container}>
      <TextInput
        value={query}
        onChangeText={setQuery} // same as: (text) => setQuery(text)
        placeholder='Search...'
        style={styles.input}
      />
      <Text style={styles.preview}>
        You typed: <Text style={{ fontWeight: 'bold' }}>{query}</Text>
      </Text>
      <Text style={styles.count}>{query.length} characters</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { padding: 16, gap: 8 },
  input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 12, fontSize: 16 },
  preview: { fontSize: 14, color: '#555' },
  count: { fontSize: 12, color: '#999' },
});

State with Arrays: Adding and Removing Items

When state is an array, never mutate it directly with push or splice. Instead, create a new array with the change and pass it to the setter. To add an item: setItems(prev => [...prev, newItem]). To remove an item: setItems(prev => prev.filter(item => item.id !== id)). To update an item: setItems(prev => prev.map(item => item.id === id ? {...item, done: true} : item)). These immutable patterns let React detect changes and schedule efficient re-renders.

import { useState } from 'react';
import { View, Text, TouchableOpacity, TextInput, FlatList } from 'react-native';

export default function TodoList() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');

  function addTodo() {
    if (!input.trim()) return;
    setTodos(prev => [...prev, { id: Date.now().toString(), text: input.trim(), done: false }]);
    setInput('');
  }

  function toggleDone(id) {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
  }

  return (
    <View style={{ flex: 1, padding: 16 }}>
      <TextInput value={input} onChangeText={setInput} placeholder='Add a todo' />
      <TouchableOpacity onPress={addTodo}><Text>Add</Text></TouchableOpacity>
      <FlatList
        data={todos}
        keyExtractor={t => t.id}
        renderItem={({ item }) => (
          <TouchableOpacity onPress={() => toggleDone(item.id)}>
            <Text style={{ textDecorationLine: item.done ? 'line-through' : 'none' }}>
              {item.text}
            </Text>
          </TouchableOpacity>
        )}
      />
    </View>
  );
}

State with Objects

For related pieces of state, group them into a single object state variable. When updating object state, spread the previous state to keep unchanged fields: setState(prev => ({ ...prev, name: 'Alice' })). This only overwrites the name field while keeping all other fields intact. Alternatively, each field can be its own useState — neither approach is universally better. Use an object when the fields always change together (like a form with name, email, and password); use separate states when they change independently.

import { useState } from 'react';
import { View, TextInput, Text, StyleSheet } from 'react-native';

export default function ProfileForm() {
  const [form, setForm] = useState({
    name: '',
    email: '',
    bio: '',
  });

  function updateField(field, value) {
    setForm(prev => ({ ...prev, [field]: value }));
  }

  return (
    <View style={{ padding: 16, gap: 12 }}>
      <TextInput
        value={form.name}
        onChangeText={v => updateField('name', v)}
        placeholder='Name'
        style={styles.input}
      />
      <TextInput
        value={form.email}
        onChangeText={v => updateField('email', v)}
        placeholder='Email'
        style={styles.input}
      />
      <Text style={{ color: '#666' }}>Preview: {form.name} — {form.email}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 12 },
});

Lazy Initial State

If computing the initial state is expensive (reading from AsyncStorage, parsing a large dataset), pass a function to useState instead of a value: useState(() => computeExpensiveValue()). React calls this initializer function only once on the component's first render, not on every subsequent render. This is called lazy initialization. Passing an expensive computation as a plain value (useState(expensiveCompute())) would run it on every render, causing unnecessary work even though the initial state is only used once.

import { useState } from 'react';

// Without lazy init: computeExpensiveList() runs on EVERY render
const [list, setList] = useState(computeExpensiveList()); // BAD

// With lazy init: function is called ONCE on mount
const [list, setList] = useState(() => computeExpensiveList()); // GOOD

// Real example: JSON.parse is cheap, but still good practice:
const [settings, setSettings] = useState(() => {
  const stored = SomeStorage.get('settings');
  return stored ? JSON.parse(stored) : { theme: 'light', lang: 'en' };
});

Derived Values vs. Redundant State

Don't store values in state if they can be derived from existing state or props. For example, if you have a list of todos in state, don't also store a completedCount in state — compute it as todos.filter(t => t.done).length during render. Storing derived values in state creates synchronization bugs: you forget to update one when you update the other. Only put things in state that you can't derive, can't compute during render, or that are asynchronously set (like API response data). This rule keeps state minimal and bug-free.

import { useState } from 'react';
import { Text, View } from 'react-native';

export default function TodoStats() {
  const [todos, setTodos] = useState([
    { id: '1', text: 'Buy groceries', done: true },
    { id: '2', text: 'Write code', done: false },
    { id: '3', text: 'Go for a walk', done: true },
  ]);

  // DERIVED: computed from existing state, not stored in state
  const total = todos.length;
  const completed = todos.filter(t => t.done).length;
  const remaining = total - completed;

  return (
    <View style={{ padding: 16 }}>
      <Text>Total: {total}</Text>
      <Text>Completed: {completed}</Text>
      <Text>Remaining: {remaining}</Text>
    </View>
  );
}

useState vs useRef: When Not to Use State

Not every value that changes in a component needs to be in state. If a value changes but does not need to trigger a re-render, store it in a useRef instead. Common examples: a timer ID returned from setTimeout, a flag tracking whether a network request is in flight, or the previous value of a prop. Putting these in state causes unnecessary re-renders. A useRef persists its value across renders (like state) but updating it does not schedule a re-render. The golden rule: if the UI doesn't need to change when the value changes, use useRef.

import { useState, useRef, useEffect } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';

export default function Timer() {
  const [elapsed, setElapsed] = useState(0);
  const intervalRef = useRef(null); // timer ID in ref — no re-render needed

  function start() {
    if (intervalRef.current) return; // already running
    intervalRef.current = setInterval(() => {
      setElapsed(prev => prev + 1); // state — needs re-render to show
    }, 1000);
  }

  function stop() {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  }

  useEffect(() => () => clearInterval(intervalRef.current), []); // cleanup

  return (
    <View style={{ alignItems: 'center', padding: 24, gap: 16 }}>
      <Text style={{ fontSize: 48 }}>{elapsed}s</Text>
      <TouchableOpacity onPress={start}><Text>Start</Text></TouchableOpacity>
      <TouchableOpacity onPress={stop}><Text>Stop</Text></TouchableOpacity>
    </View>
  );
}

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: useState declares state variables and provides a setter that triggers re-renders, functional updates safely derive new state from the previous value, and derived values should be computed during render instead of stored as redundant state. Next up we explore lifting state up to share data between sibling components.

เริ่มต้นได้ฟรี

เรียนรู้ JavaScript ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
30
บทเรียน
120

คำถามที่พบบ่อย

บทเรียน “การจัดการสถานะคอมโพเนนต์ด้วย useState” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การจัดการสถานะคอมโพเนนต์ด้วย useState” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส React Native Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส React Native Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การจัดการสถานะคอมโพเนนต์ด้วย useState”

ทำความรู้จักฮุก useState จัดเก็บค่าตัวนับหรือค่าการสลับไว้ในสถานะ และทำให้เกิดการเรนเดอร์ใหม่ด้วยการเรียกฟังก์ชันตั้งค่าสถานะ คุณปฏิบัติ React Native Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน React Native Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน React Native Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน

บทเรียน “การจัดการสถานะคอมโพเนนต์ด้วย useState” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน React Native Academy นี้ได้ไหม

ได้ บทเรียน React Native Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การส่งข้อมูลด้วยพร็อพส์
  2. การจัดการสถานะคอมโพเนนต์ด้วย useState
  3. การยกสถานะขึ้น
  4. การสร้างแอปตัวนับแบบโต้ตอบ
← กลับไปที่ React Native Academy