CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
React Native 开发入门/课时/State 与 JSX
RawBlame
课时目录12 节

React Native 开发入门

  1. 01RN 与 Expo
  2. 02核心组件
  3. 03布局与列表
  4. 04平台差异
  5. 05导航与手势
  6. 06State 与 JSX
  7. 07网络
  8. 08本地存储
  9. 09原生模块
  10. 10动画
  11. 11性能优化
  12. 12测试与发布

State 与 JSX

组件状态管理。

30 分钟

State 与 JSX

这一课你将能做到

  1. 在 RN 中正确使用 useState / useEffect 管理组件状态和副作用,避开 stale closure 陷阱
  2. 用 Context 做跨层级数据共享,写出可复用的 AuthProvider
  3. 抽取自定义 Hook 复用请求逻辑,并处理 App 前后台切换这个 RN 特有场景

为什么要学

你已经会 React,这一课严格说「一半是复习」。但 RN 环境有三个特殊性:没有浏览器的 localStorage,状态想持久化要走原生存储;组件生命周期受 App 前后台切换影响(Web 里没有这个概念);长列表重渲染的代价比 Web 高得多。这一课就是把你会的 React 状态管理「翻译」到 RN 语境,并补上这三块短板。

人话:state 是组件的「短期记忆」——杀了进程就没了;props 是「父母给的交代」——只能从上传到下;Context 是「小区公告栏」——谁需要谁自己去看,不用挨家挨户送信。什么时候用哪个,是这一课的核心判断。

先建立图像

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

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

  useEffect(() => {
    console.log('count 变了:', count);
  }, [count]);

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text style={{ fontSize: 32 }}>{count}</Text>
      <Pressable onPress={() => setCount(c => c + 1)}>
        <Text style={{ color: '#007bff', marginTop: 16 }}>点我 +1</Text>
      </Pressable>
    </View>
  );
}

渲染结果:屏幕中央一个数字,点「点我 +1」数字递增,控制台同步打印。数据流的循环永远是:state 变了 → 组件重新渲染 → 界面更新。你改的不是界面,是数据;界面是数据的投影。

跟着做

步骤 1:useState 的四类常见状态

RN 项目里 90% 的 useState 逃不出这四类,先建立分类直觉:

const [text, setText] = useState('');          // ① 表单值:输入框、选择器
const [visible, setVisible] = useState(false); // ② UI 开关:弹窗、展开收起
const [list, setList] = useState([]);          // ③ 列表数据:接口返回的数组
const [loading, setLoading] = useState(false); // ④ 加载标志:请求三态

更新数组和对象时必须返回新引用,直接改原对象 React 感知不到:

setList([...list, newItem]);                    // ✅ 新增:展开旧数组造新数组
setList(list.filter(i => i.id !== id));         // ✅ 删除:filter 返回新数组
setList(list.map(i => i.id === id ? {...i, done: true} : i)); // ✅ 改某一项
list.push(newItem);                             // ❌ 直接改原数组,界面不更新

步骤 2:setState 的异步性与函数式更新

// ❌ 连续调用基于旧值计算,三次只加了一次
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

// ✅ 函数式更新:每次都拿到最新值,三次加三
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);

setState 不立即改值,它只安排「下一次渲染用新值」。所以 setState 后立刻读 state 拿到的还是旧值——要看变化后的值,用 useEffect 监听。

步骤 3:useEffect 请求数据 + cleanup

useEffect(() => {
  let cancelled = false;

  fetch('https://jsonplaceholder.typicode.com/users')
    .then(r => r.json())
    .then(data => { if (!cancelled) setList(data); });

  return () => { cancelled = true; };  // cleanup:组件卸载后不再 setState
}, []);  // 空依赖 = 只在挂载时执行一次

依赖数组是 useEffect 的灵魂:空数组只跑一次,写上 [keyword] 则 keyword 每次变化都重新执行(先跑上一次的 cleanup)。effect 里用到的所有外部变量都要写进依赖,否则闭包拿到的是创建那一刻的旧值——这就是著名的 stale closure(过期闭包):

// ❌ keyword 变了 effect 不会重跑,搜的永远是旧关键词
useEffect(() => { search(keyword); }, []);

// ✅ 依赖写全
useEffect(() => { search(keyword); }, [keyword]);

步骤 4:Context 共享登录状态

登录用户这种「全局一份、到处要用」的数据,用 props 传五层是灾难。Context 的正解:

// context/AuthContext.js
import { createContext, useContext, useState, useMemo } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  // value 用 useMemo 固定引用,user 没变就不惊动消费组件
  const value = useMemo(() => ({ user, setUser }), [user]);
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export const useAuth = () => useContext(AuthContext);
// App.js 根部包一层
<AuthProvider>
  <NavigationContainer>...</NavigationContainer>
</AuthProvider>

// 任何深层组件一行取用,不用管自己在第几层
const { user, setUser } = useAuth();

步骤 5:自定义 Hook 复用请求逻辑

每个列表页都写一遍「loading + data + error」太蠢了,抽成 Hook:

// hooks/useFetch.js
import { useState, useEffect } from 'react';

export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(r => r.json())
      .then(d => { if (!cancelled) { setData(d); setLoading(false); } })
      .catch(e => { if (!cancelled) { setError(e); setLoading(false); } });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// 页面里一行搞定
const { data, loading, error } = useFetch('https://api.example.com/list');

自定义 Hook 就是「把 useState + useEffect 的组合打包成函数」,命名以 use 开头即可。这是 React 逻辑复用的主力手段。

步骤 6:AppState——监听前后台切换(RN 特有)

用户切到微信回了个消息再切回来,你可能需要刷新未读数。Web 没有这个场景,RN 用 AppState:

import { useEffect } from 'react';
import { AppState } from 'react-native';

useEffect(() => {
  const sub = AppState.addEventListener('change', (state) => {
    if (state === 'active') {
      refetch(); // 从后台切回前台,刷新数据
    }
  });
  return () => sub.remove(); // 别忘了清理
}, []);

state 的三个值:active(前台)、background(后台)、inactive(iOS 特有,来电/切多任务时的过渡态)。

讲清楚要点

  1. 状态放哪层的判断:只有本组件用 → useState;父子共享 → 提升到最近共同父组件;跨多层级(登录用户、主题)→ Context;服务端列表数据 → 第 7 课的请求层或专门数据库。
  2. 不可变更新是铁律:数组对象更新必须返回新引用,push / 直接改属性都不会触发重渲染。
  3. Context 不是状态管理器:它只解决「传递」问题。value 一变,所有消费组件无脑重渲染——所以 value 要用 useMemo 固定,高频数据别放进来。
  4. 列表 item 用 React.memo:FlatList 的 renderItem 组件包一层 memo,父组件 state 变化就不会牵连几百行重渲染(第 11 课详讲)。
  5. 依赖数组写全:useEffect 用到的外部变量全写进依赖,否则就是 stale closure,数据永远是旧的。
  6. RN 没有 localStorage:state 只在内存里,杀掉 App 全没了。想持久化登录态要用 AsyncStorage/SecureStore(第 8 课)。

你可能会懵的地方

  • setState 之后立刻读,怎么还是旧值? setState 是异步的,它只安排下一次渲染。要用最新值参与计算就用函数式更新 setCount(c => c + 1);要在变化后做事就用 useEffect 监听该值。

  • Context 和 Redux/Zustand 怎么选? 低频共享(用户信息、主题、语言)用 Context 足够;高频更新(表单联动、实时数据)或大型项目用 Zustand(轻量、按 selector 精确订阅);Redux 留给需要时间旅行、中间件审计的团队。

  • 为什么子组件没用到 user,setUser 之后它也重渲染了? 它消费的 Context value 整个对象引用变了。三个解法:拆分 Context(user 和 setUser 分开)、value 用 useMemo 缓存、子组件包 memo。

  • useEffect 在开发模式跑了两次? React 18 StrictMode 故意「挂载→卸载→再挂载」,帮你暴露没写 cleanup 的副作用。生产环境只跑一次。cleanup 写规范就不怕它。

  • 组件卸载后继续 setState 会怎样? React 18 不再报警告,但逻辑上仍是错的(比如给已离开的页面更新数据)。cleanup 标志位是防御性好习惯。

错法对照

错法问题正解
直接 list.push(x)不触发重渲染setList([...list, x]) 新数组
setCount(count + 1) 连续调基于旧值,更新丢失setCount(c => c + 1)
useEffect 不写依赖数组每次渲染都发请求加 [] 或写全依赖
依赖漏写外部变量stale closure,拿到旧值用到的全写进依赖
Context value 每次新建对象所有消费者无脑重渲染useMemo(() => ({user, setUser}), [user])
props 传 5 层中间组件被迫转发Context 或状态库
登录 token 存 useState杀 App 就没了持久化到 SecureStore(第 8 课)
AppState 监听不 remove监听器泄漏,重复触发cleanup 里 sub.remove()

卡点急救

  1. 界面不更新:第一嫌疑是直接改了 state 对象/数组没返回新引用;第二是 setState 写在了 render 函数体里(死循环)。
  2. useEffect 死循环:依赖里放了每次渲染都新建的对象/数组/函数——用 useMemo/useCallback 固定,或把常量移出组件。
  3. 数据总是「慢一拍」:stale closure 典型症状,检查 effect 依赖数组是不是漏了变量。
  4. Context 更新后某组件没变:确认它确实在 Provider 树下(Provider 包错层级是常见事故)。

面试常问

  • RN 状态管理和 Web 有何不同? 核心 API 完全一样(useState/useEffect/Context)。差异在场景:RN 要用 AppState 感知前后台切换并决定是否刷新;没有 localStorage,持久化必须走 AsyncStorage/SecureStore 等原生存储;长列表渲染成本高,memo 和引用稳定更关键。

  • Context 的优缺点?什么情况下不够用? 优点:免 props drilling、React 原生零依赖。缺点:value 变化导致所有消费者重渲染,无法按字段订阅。高频更新场景不够用时换 Zustand——它支持 selector 精确订阅,组件只在自己关心的切片变化时重渲染。

  • 什么是 stale closure,怎么避免? effect 或回调在创建时捕获了当时的 state 值,之后 state 更新但闭包里仍是旧值。解法:依赖数组写全让 effect 重建;回调里要最新值用函数式 setState 或 useRef 镜像一份。

自测三问

  1. 为什么直接 list.push() 界面不更新?新增、删除、修改某一项的正确写法各是什么?
  2. Context 适合存什么、不适合存什么?value 为什么要 useMemo?
  3. 如何监听 App 从后台切回前台并刷新数据?清理函数怎么写?

小结一句话

RN 的状态管理就是你已会的 React 那一套,加上三条 RN 补丁:AppState 感知前后台、持久化走原生存储、长列表记得 memo 和引用稳定。

延伸

  • React 官方文档:状态管理
  • 下一课:网络请求——fetch、axios 封装与三态界面。
加载本节练习…
← 上一课导航与手势下一课 →网络