🔥 React 零基础入门(下):Props + State + useEffect 生命周期深度解析

0 阅读7分钟

🔥 React 零基础入门(下):Props + State + useEffect 生命周期深度解析

摘要:本篇是 React 零基础入门系列的下篇,重点讲解 Props 数据传递、State 状态管理、useEffect 生命周期,以及事件处理和列表渲染。最后通过一个完整的待办事项 App 串联所有知识点。


📌 前言

上篇我们搞定了环境搭建、组件思想和 JSX 的 9 条规则。本篇继续,讲解 React 的数据流和生命周期——这是让组件"活"起来的关键。

📖 系列回顾:上篇讲了环境搭建、组件、JSX 深度解析。还没看的同学建议先看上篇。


📚 一、Props:给组件传数据(父 → 子)

1.1 基本用法

// 父组件传递数据
function App() {
  return (
    <div>
      <UserCard
        name="张三"
        age={25}              {/* 数字用花括号 */}
        isAdmin={true}        {/* 布尔值用花括号 */}
        hobbies={['读书', '游泳']}  {/* 数组用花括号 */}
      />
    </div>
  );
}

// 子组件接收数据(解构 props)
function UserCard({ name, age, isAdmin, hobbies }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>年龄:{age}</p>
      <p>{isAdmin ? '管理员' : '普通用户'}</p>
      <p>爱好:{hobbies.join('、')}</p>
    </div>
  );
}

1.2 TypeScript 写法

interface UserCardProps {
  name: string;
  age: number;
  isAdmin?: boolean;        // ? 表示可选
  hobbies?: string[];
}

function UserCard({ name, age, isAdmin = false, hobbies = [] }: UserCardProps) {
  return (
    <div>
      <h2>{name}</h2>
      <p>年龄:{age}</p>
      <p>{isAdmin ? '管理员' : '普通用户'}</p>
      <p>爱好:{hobbies.join('、')}</p>
    </div>
  );
}

1.3 children:组件的"插槽"

当组件标签之间有内容时,这些内容会作为 children 传入:

function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">
        {children}  {/* 渲染标签之间的内容 */}
      </div>
    </div>
  );
}

// 使用
<Card title="我的卡片">
  <p>这段文字会作为 children 传入</p>
  <button>点击我</button>
</Card>

💡 children 是什么类型? 可以是 JSX、字符串、数字、数组,甚至是一个函数。

1.4 Props 核心规则

规则说明
只读子组件不能修改 Props
单向流数据只能从父组件流向子组件
任意类型可以传字符串、数字、布尔、数组、对象、函数
默认值可以用解构设置默认值
children标签之间的内容会作为 children 传入

💡 Props Drilling 问题:当数据需要从顶层传到深层子组件时,中间层被迫传递不需要的 Props。解决方案是 Context 或状态管理库(进阶内容)。


📚 二、State:让组件"记住"数据

2.1 useState 基本用法

import { useState } from 'react';

function Counter() {
  // 格式:const [状态变量, 更新函数] = useState(初始值)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>点我</button>
    </div>
  );
}

2.2 不同数据类型的 State

function Demo() {
  const [count, setCount] = useState(0);           // 数字
  const [name, setName] = useState('');             // 字符串
  const [user, setUser] = useState(null);           // 对象或 null
  const [todos, setTodos] = useState([]);           // 数组
  const [loading, setLoading] = useState(false);    // 布尔值

  const handleClick = () => {
    setCount(count + 1);                            // 数字:直接加
    setName('李四');                                 // 字符串:直接赋值
    setUser({ id: 1, name: '张三' });               // 对象:替换整个对象
    setTodos([...todos, '新任务']);                   // 数组:创建新数组
  };

  return <button onClick={handleClick}>更新状态</button>;
}

2.3 函数式更新:基于上一个状态计算

function Counter() {
  const [count, setCount] = useState(0);

  // ❌ 有问题的写法:连续调用时会出 Bug
  const handleClick = () => {
    setCount(count + 1); // 0 → 1
    setCount(count + 1); // 0 → 1(还是旧值!)
    setCount(count + 1); // 0 → 1(还是旧值!)
    // 结果:count = 1,不是 3
  };

  // ✅ 正确写法:用函数式更新,接收上一个状态值
  const handleClickCorrect = () => {
    setCount(prev => prev + 1); // 0 → 1
    setCount(prev => prev + 1); // 1 → 2
    setCount(prev => prev + 1); // 2 → 3
    // 结果:count = 3 ✅
  };

  return <button onClick={handleClickCorrect}>count: {count}</button>;
}

🔑 什么时候用函数式更新? 当新状态依赖旧状态时(如计数器、队列操作),用 prev => prev + 1 更安全。

2.4 State 核心规则

规则说明
必须用 setter永远不要直接修改:count++ ❌,setCount(count + 1)
触发重渲染状态改变 → 组件自动重新渲染 → UI 更新
批量更新React 会批量处理多个 setState,异步合并
不可变更新对象/数组时,要创建新引用
函数式更新新状态依赖旧状态时,用 setX(prev => newVal) 更安全

💡 核心心法UI = f(state) — 界面是状态的函数。你只需要管理状态,React 自动更新界面。


📚 三、生命周期与 useEffect:让组件"活"起来(重点)

3.1 什么是副作用?

组件不仅仅是在页面上渲染 UI,它还需要做一些"额外的事情":

  • 从服务器获取数据
  • 设置定时器
  • 手动修改 DOM
  • 订阅 WebSocket
  • 写入 localStorage

这些"额外的事情"就叫副作用(Side Effect)。React 提供了 useEffect Hook 来处理它们。

3.2 useEffect 基本语法

import { useState, useEffect } from 'react';

function App() {
  const [count, setCount] = useState(0);

  // useEffect(回调函数, 依赖数组)
  useEffect(() => {
    document.title = `你点击了 ${count} 次`;
  }, [count]); // 依赖数组:count 变化时才执行

  return <button onClick={() => setCount(count + 1)}>点我</button>;
}

语法拆解:

useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理函数(可选)
  };
}, [依赖1, 依赖2]);

3.3 依赖数组的 3 种形态

// 形态 1:不传依赖数组 → 每次渲染都执行
useEffect(() => {
  console.log('每次渲染都会执行');
});

// 形态 2:空数组 → 只在组件挂载时执行一次
useEffect(() => {
  console.log('只在组件首次渲染时执行');
}, []);

// 形态 3:指定依赖 → 依赖变化时执行
useEffect(() => {
  console.log('count 变化时执行');
}, [count]);
依赖数组执行时机类比生命周期
不传每次渲染后componentDidMount + componentDidUpdate
[]首次渲染后componentDidMount
[a, b]a 或 b 变化时componentDidUpdate(指定条件)

3.4 清理函数:组件"临终遗言"

有些副作用需要在组件卸载前清理,比如定时器、事件监听器:

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // 返回清理函数(组件卸载时执行)
    return () => {
      clearInterval(timer);
      console.log('定时器已清理');
    };
  }, []);

  return <p>已经过了 {seconds} 秒</p>;
}

🔑 执行顺序:组件渲染 → 执行副作用 → 组件卸载前 → 执行清理函数

3.5 实战:数据请求

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        const response = await fetch('https://api.example.com/users');
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>出错了:{error}</p>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

⚠️ 注意useEffect 的回调函数不能直接是 async 函数,需要在内部定义一个 async 函数再调用。

3.6 实战:监听窗口大小

function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);

    // 清理:移除监听器
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <p>窗口宽度:{width}px</p>;
}

3.7 实战:竞态条件与 AbortController

当依赖变化触发新的数据请求时,旧请求可能比新请求晚返回,导致页面显示过时数据:

// ❌ 有竞态问题的写法
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data));  // 旧请求可能覆盖新数据
  }, [userId]);

  return <div>{user?.name}</div>;
}

解决方案:用 AbortController 取消旧请求

// ✅ 正确写法
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, {
      signal: controller.signal
    })
      .then(res => res.json())
      .then(data => setUser(data))
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err);
        }
      });

    // 清理函数:取消过期请求
    return () => controller.abort();
  }, [userId]);

  return <div>{user?.name}</div>;
}

💡 执行顺序:userId 变化 → abort 旧请求 → 发新请求

3.8 useEffect 常见踩坑

踩坑原因解决方案
无限循环useEffect 里更新状态,触发重渲染检查依赖数组,确保有终止条件
忘记依赖用了外部变量但没加到依赖数组ESLint 插件会警告,补上依赖
直接写 asyncuseEffect 回调不能是 async在内部定义 async 函数再调用
竞态条件旧请求覆盖新请求用 AbortController 取消旧请求

3.9 生命周期速记图

组件首次渲染
  ↓
执行 useEffect(依赖=[])    ← componentDidMount
  ↓
状态更新 → 组件重新渲染
  ↓
执行清理函数(上次的 effect)  ← componentWillUnmount(旧)
执行 useEffect(依赖变化)    ← componentDidUpdate
  ↓
组件卸载
  ↓
执行清理函数                  ← componentWillUnmount

📚 四、事件处理:响应用户操作

function Form() {
  const [inputValue, setInputValue] = useState('');
  const [items, setItems] = useState([]);

  const handleChange = (event) => {
    setInputValue(event.target.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault(); // 阻止页面刷新!
    if (inputValue.trim()) {
      setItems([...items, inputValue]);
      setInputValue('');
    }
  };

  const handleDelete = (index) => {
    setItems(items.filter((_, i) => i !== index));
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={inputValue}
          onChange={handleChange}
          placeholder="输入内容..."
        />
        <button type="submit">添加</button>
      </form>

      <ul>
        {items.map((item, index) => (
          <li key={index}>
            {item}
            <button onClick={() => handleDelete(index)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

要点: 事件名用驼峰、接收 event 对象、表单要 preventDefault、传参用箭头函数包裹。


📚 五、列表渲染:用 map 循环

function TodoList() {
  const todos = [
    { id: 1, text: '学习 React', done: true },
    { id: 2, text: '做个项目', done: false },
    { id: 3, text: '写篇文章', done: false },
  ];

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span style={{
            textDecoration: todo.done ? 'line-through' : 'none'
          }}>
            {todo.text}
          </span>
        </li>
      ))}
    </ul>
  );
}

关键点:map 遍历数组,每个元素必须有唯一的 key(用 ID,不要用 index)。


📝 实战:一个完整的待办事项 App

把以上所有概念组合起来:

import { useState } from 'react';

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

  // 添加待办
  const addTodo = () => {
    if (input.trim()) {
      setTodos([...todos, { id: Date.now(), text: input, done: false }]);
      setInput('');
    }
  };

  // 切换完成状态
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  };

  // 删除待办
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
      <h1>📝 待办清单</h1>

      <div style={{ display: 'flex', gap: '10px' }}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && addTodo()}
          placeholder="输入待办..."
          style={{ flex: 1, padding: '8px' }}
        />
        <button onClick={addTodo}>添加</button>
      </div>

      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map(todo => (
          <li key={todo.id} style={{
            display: 'flex',
            alignItems: 'center',
            gap: '10px',
            padding: '8px 0',
            borderBottom: '1px solid #eee'
          }}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{
              flex: 1,
              textDecoration: todo.done ? 'line-through' : 'none'
            }}>
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>

      {todos.length === 0 && <p style={{ color: '#999' }}>还没有待办,添加一个吧!</p>}
      <p>总计:{todos.length} 项</p>
    </div>
  );
}

export default App;

这个 App 用到了哪些概念?

概念用法
组件function App()
JSX{input}{todos.map(...)}
StateuseState([])useState('')
事件onClickonChangeonKeyDown
列表渲染todos.map(todo => <li key={todo.id}>)
条件渲染{todos.length === 0 && <p>...</p>}

💡 下篇总结

概念一句话理解
Props父传子的数据,只读,单向流
State组件内部可变数据,用 setState 更新,触发重渲染
useEffect处理副作用,依赖数组控制执行时机,返回清理函数
事件驼峰命名,传函数引用,表单要 preventDefault
列表map 渲染,每个元素必须有唯一的 key

核心心法:

  1. UI = f(state) — 界面是状态的函数
  2. 数据单向流:父 → 子(Props),子 → 父(回调函数)
  3. 状态不可变:永远用 setState,不要直接修改
  4. 副作用用 useEffect,清理函数在卸载前执行
  5. 组件化思维:拆分 UI 为独立、可复用的小块

🗺️ 下一步学什么?

阶段学习内容推荐资源
进阶 1React Router — 多页面路由react-router-dom
进阶 2自定义 Hooks — 封装复用逻辑React 官方文档
进阶 3Context — 跨组件传值React 官方文档
进阶 4性能优化 — useMemouseCallbackReact.memoReact 官方文档
进阶 5状态管理 — Zustand 或 ReduxZustand 官方文档
进阶 6React 19 新特性 — Server Components、use()React 官方博客

🔗 参考资料


💬 交流讨论

你在学 React 的过程中踩过什么坑?欢迎在评论区分享!

觉得有用?点个赞👍收藏⭐关注👆,后续继续更新 React Router 实战、React 19 新特性!