Skip to content
状态驱动的 UI 刷新
组件是 React Native 界面的基本单位。React Native 把 UI 视为状态的函数:给定某一时刻的状态,组件渲染出对应的界面;状态发生变化时,框架自动重新渲染受影响的组件,不需要手动调用原生视图 API。
这是声明式编程:描述每种状态下界面应该呈现的内容,剩下的由 React 处理。对比命令式做法——先找到某个视图节点,再调用方法修改它的内容——声明式避免了 UI 状态与数据状态不同步的问题。
下面的代码用一个 status 变量驱动三段界面:
tsx
const [status, setStatus] = useState<'initial' | 'loading' | 'done'>('initial');
if (status === 'initial') {
return <Text>等待操作</Text>;
}
if (status === 'loading') {
return <ActivityIndicator />;
}
return <Text>操作完成</Text>;status 变化时,组件重新执行函数体,进入对应分支并返回不同的 JSX。整个过程不存在"找到某个 Text 并修改它的 children"这类操作。当界面逻辑涉及多个状态变量组合出五六种界面形态时,声明式写法的可维护性差异会更明显。
useState:状态变量的声明与更新
useState 是函数组件中声明状态变量的 Hook。调用后返回一个包含两个元素的数组:当前状态值和一个更新函数。约定用数组解构赋值来接收这两个值:
tsx
const [count, setCount] = useState(0);useState 接受一个参数作为初始值。初始值只在组件首次渲染时使用,后续渲染中会被忽略。初始值可以是任意类型——数字、字符串、对象、数组,也可以是函数。传入函数时,React 会调用它来获取初始值,这称为惰性初始化,适合初始值计算开销较大的场景:
tsx
const [data, setData] = useState(() => {
return expensiveComputation();
});更新函数 setCount 接受两种形式:直接传入新值,或者传入一个基于前值的更新函数。
tsx
// 直接设置新值
setCount(5);
// 基于前值更新
setCount(prev => prev + 1);两种形式的差别在连续多次调用更新函数时才会体现。在一个事件处理中多次调用 setCount(count + 1),由于每次读取的 count 都是当前渲染中的值,最终只会加一次。而 setCount(prev => prev + 1) 使用更新函数,React 会将多个更新函数排队执行,每次基于最新的前值计算,结果是正确的累加。
调用 setState 不会改变当前执行中已读取的 state 变量。它是一个"请求下一次渲染时使用新值"的动作。同一个事件处理中的多个 setState 调用也会被 React 批量处理,合并为一次重新渲染。
在 React Native 中,状态常用于控制组件的可见性、文本内容、样式等:
tsx
const [isModalVisible, setModalVisible] = useState(false);
<TouchableOpacity onPress={() => setModalVisible(true)}>
<Text>打开弹窗</Text>
</TouchableOpacity>
<Modal visible={isModalVisible} onRequestClose={() => setModalVisible(false)}>
{/* 弹窗内容 */}
</Modal>状态中的对象和数组应视为只读。 不要直接修改状态引用的对象属性或数组元素,而应该创建新的对象或数组来替换旧值。React 使用 Object.is 比较新旧状态来决定是否触发重新渲染,原地修改不会产生新的引用,可能导致更新被跳过。
tsx
// 错误
form.firstName = 'Taylor';
setForm(form);
// 正确
setForm({ ...form, firstName: 'Taylor' });关于状态结构的选择:避免冗余状态。能用现有状态计算出来的值,就不应该单独放在状态里。例如 fullName 可以从 firstName 和 lastName 拼接得到,不需要另存为状态变量。冗余状态增加了需要同步更新的数据点,遗漏同步会导致不一致。
useEffect:副作用的执行与清理
useEffect 用于在组件中执行副作用。"副作用"指那些不发生在渲染阶段的计算:从远端拉取数据、订阅事件、操作计时器、手动修改原生视图等。这些操作在渲染期间不能执行——渲染应该保持纯净,只负责根据 props 和 state 计算返回的 JSX。
基本用法:
tsx
useEffect(() => {
// 副作用逻辑
return () => {
// 可选的清理函数
};
}, [依赖数组]);第一个参数是一个函数,称为 setup 函数。React 在组件提交到屏幕后执行它。setup 函数可以返回一个函数,称为清理函数。清理函数在下一次 effect 执行前或组件卸载前被调用。
useEffect 不会阻塞屏幕绘制,适合处理不需要即时反馈的操作。如果副作用需要在绘制前同步执行——比如测量布局并据此调整——应该用 useLayoutEffect。在 React Native 中这种情况不常见,因为原生视图的布局由 Yoga 引擎处理,不需要 JavaScript 层面介入。
依赖数组控制 effect 的触发时机,具体见下一节。下面是一个计时器示例,同时展示了清理函数的用途:
tsx
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
clearInterval(timer);
};
}, []);组件挂载后启动计时器,组件卸载时清理函数清除计时器。如果没有清理,组件卸载后计时器继续运行,不仅浪费资源,还可能尝试访问已经不存在的组件状态。
依赖数组:控制 effect 触发
useEffect 的第二个参数是一个依赖数组。React 通过 Object.is 比较每个依赖项的前后值,只有当至少一个依赖项发生变化时,effect 才会重新执行。三种用法:
不传依赖数组——每次渲染后都执行 effect。这很少是预期行为,通常意味着遗漏了依赖数组。
tsx
useEffect(() => {
console.log('每次渲染后执行');
});空数组 []——只在组件挂载后执行一次,卸载时执行清理函数。适合初始化操作:发送一次请求、建立一次订阅。
tsx
useEffect(() => {
// 仅在挂载后执行
fetchInitialData();
}, []);指定依赖项 [a, b]——挂载后执行,并在 a 或 b 变化时重新执行。每次重新执行前,上一次的清理函数会先运行。
tsx
useEffect(() => {
const subscription = subscribe(userId);
return () => {
subscription.unsubscribe();
};
}, [userId]);userId 变化时,先取消旧用户的订阅,再建立新用户的订阅。依赖数组保证了 effect 和它的依赖保持同步。
在 effect 内部使用了组件作用域中的响应式变量——包括 props、state 以及由它们计算出的值——就应该把它们列入依赖数组。遗漏依赖会导致 effect 内部读取到过期的闭包值。React 的严格模式在开发环境下会通过双重调用 effect 来帮助暴露这类问题,但这只是检测手段,不能替代正确声明依赖。
fetch 请求远端数据
在 React Native 中发起网络请求,标准做法是使用 fetch API。它是 JavaScript 运行环境内置的,不需要额外安装库。基本用法:
tsx
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
// 使用 data
})
.catch(error => {
// 处理错误
});fetch 返回一个 Promise,解析后得到 Response 对象。response.json() 将响应体解析为 JSON,同样返回 Promise。fetch 只在网络层面的请求失败(如 DNS 解析失败、连接被拒绝)时 reject,HTTP 错误状态码(如 404、500)并不会导致 reject。需要检查 response.ok 或 response.status 来手动处理服务端返回的错误。
将请求逻辑放在 useEffect 中:
tsx
useEffect(() => {
let cancelled = false;
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => {
if (!cancelled) {
setData(data);
}
})
.catch(error => {
if (!cancelled) {
setError(error);
}
});
return () => {
cancelled = true;
};
}, []);清理函数设置了一个标志位 cancelled = true。如果组件在请求完成前被卸载,then 回调中的 setData 调用会被跳过,避免对已卸载的组件调用状态更新。React 会在开发模式中对此发出警告。
在 React Native 开发模式下,严格模式会导致 effect 执行两次——挂载、卸载、再挂载,这会让 fetch 请求发送两次。清理函数中的 cancelled = true 使第一次请求的结果被忽略,第二次请求正常处理。生产模式下不会出现双重调用,因此这只影响开发体验。如果开发时需要避免双重请求,可以使用 AbortController 真正取消第一次请求:
tsx
useEffect(() => {
const controller = new AbortController();
fetch('https://api.example.com/data', { signal: controller.signal })
.then(res => res.json())
.then(setData);
return () => {
controller.abort();
};
}, []);将数据写入状态并更新界面
fetch 拿到数据后,流程回到状态驱动的循环:通过 setState 把响应数据存入状态变量,状态更新触发重新渲染,组件根据新状态绘制界面。
下面是一个完整的组件,从远端加载列表数据并展示:
tsx
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
let cancelled = false;
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => {
if (!cancelled) {
setUsers(data);
}
})
.catch(console.error);
return () => {
cancelled = true;
};
}, []);
return (
<ScrollView>
{users.map(user => (
<View key={user.id} style={{ padding: 12, borderBottomWidth: 1 }}>
<Text style={{ fontWeight: 'bold' }}>{user.name}</Text>
<Text style={{ color: '#666' }}>{user.email}</Text>
</View>
))}
</ScrollView>
);
};数据流是单向的:组件挂载 → effect 发起请求 → 响应到达 → setUsers 更新状态 → React 重新渲染 → 新的 users 数组驱动 map 生成列表项。没有哪个步骤直接操作界面节点。
更复杂的场景——比如将远端数据和本地用户输入合并,或者根据数据类型切换渲染策略——也只是增加状态维度和渲染分支,核心循环不变。
请求状态的三段式:加载中、成功与出错
上面的例子能用,但用户体验不完整。从发起请求到界面展示数据之间有一个等待期,这段时间内界面应该给用户反馈;网络可能出问题,这时需要提示而非空白屏。这三种状态——loading、success、error——是大部分网络驱动界面的基本状态机。
实现方式是用状态变量显式跟踪当前处于哪个阶段:
tsx
type Status = 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('loading');
const [data, setData] = useState<DataType | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setStatus('loading');
fetch('https://api.example.com/data')
.then(res => {
if (!res.ok) {
throw new Error(`请求失败 (${res.status})`);
}
return res.json();
})
.then(data => {
if (!cancelled) {
setData(data);
setStatus('success');
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setStatus('error');
}
});
return () => {
cancelled = true;
};
}, []);
// 渲染
if (status === 'loading') {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
<Text style={{ marginTop: 8 }}>加载中...</Text>
</View>
);
}
if (status === 'error') {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }}>
<Text style={{ color: 'red', fontSize: 16 }}>加载失败</Text>
<Text style={{ marginTop: 8 }}>{error?.message}</Text>
</View>
);
}
// status === 'success'
return (
<ScrollView>
{/* 渲染 data */}
</ScrollView>
);三种状态各自对应一套界面,状态切换时界面整体替换。error 分支中还可以加上重试按钮,点击后重新设置 status 为 'loading' 并重新发起请求。
data 和 error 分在两个独立的状态变量中,而不是用一个包含所有字段的对象,是为了避免类型上的混乱——loading 阶段 data 为 null 是正常的,error 阶段 data 为 null 也是正常的。分开管理让每个变量的含义更清晰,也方便在渲染分支中针对性地使用。
练习:动态天气卡片
构建一个天气卡片组件,从公开 API 获取指定城市的天气数据并展示。
功能要求:
- 组件接受
cityprop,根据city变化重新请求数据 - 展示天气状况、温度、湿度
- 处理加载中和请求失败的界面状态
- 使用 fetch 获取数据(可以使用 OpenWeatherMap 或其他公开天气 API,需要注册获取 API key)
实现提示:
把 city 放在 useEffect 的依赖数组中,city 变化时清理上一次请求并重新获取。状态结构可以设计为:
tsx
type WeatherStatus = 'loading' | 'success' | 'error';
const [status, setStatus] = useState<WeatherStatus>('loading');
const [weather, setWeather] = useState(null);
const [error, setError] = useState(null);请求 URL 示例(需要替换 API_KEY):
https://api.openweathermap.org/data/2.5/weather?q={city}&appid={API_KEY}&units=metric关键点:city 变化时先 setStatus('loading') 清除旧状态,再用标志位或 AbortController 忽略过期请求的结果。清理函数中处理取消逻辑。
注意点
严格模式下 effect 双重调用。 React Native 开发环境中的严格模式会额外挂载-卸载一次组件,导致 effect 执行两次。这可以暴露未正确清理的副作用。用 cancelled 标志位或 AbortController 处理即可,不影响生产行为。
fetch 不 reject HTTP 错误。 状态码 4xx 和 5xx 不会导致 fetch reject,必须在 response.ok 或 response.status 上做判断。
过期闭包。 effect 中引用的 props 和 state 应出现在依赖数组中。如果依赖数组为空但内部引用了外部变量,effect 会一直使用首次渲染时的值。
不要直接修改状态中的对象和数组。 用展开语法或 map/filter 等方法创建新副本再 setState,否则可能导致渲染被跳过。
依赖数组的比较方式是 Object.is。 这意味着对象、数组、函数作为依赖项时,每次渲染都会创建新的引用,导致 effect 每次都执行。如果确实需要以对象作为依赖,应该在组件外部定义常量,或者用 useMemo 缓存引用。
状态提升。 如果多个组件需要共享同一份网络数据,把状态和 effect 提到最近的公共父组件中,通过 props 向下传递。
