
本文详解为何 useState 触发重渲染时会重复执行 requestQuestions() 导致题目频繁变更,并提供基于组件拆分与 props 传递的稳定数据流解决方案。
本文详解为何 `usestate` 触发重渲染时会重复执行 `requestquestions()` 导致题目频繁变更,并提供基于组件拆分与 props 传递的稳定数据流解决方案。
在 React(包括 React Native)中,组件函数每次渲染都会从头执行。你当前的 Test 组件将 requestQuestions(category) 直接写在函数体顶层:
const { category } = useLocalSearchParams();
let questions = requestQuestions(category); // ❌ 每次渲染都重新调用!
而 requestQuestions() 内部使用了 lodash/sampleSize —— 它会随机采样(即使固定 category,每次调用也返回不同子集)。当点击按钮触发 nextQuestion() → 调用 setCurrentQuestion() → 引发组件重渲染 → requestQuestions() 再次执行 → questions 数组被完全替换 → currentQuestion 索引指向新数组中完全不同的题目。这就是你观察到“题目总在变”“category 多次打印”的根本原因。
✅ 正确做法是:将随机抽样逻辑上提至父级作用域,确保 questions 仅生成一次,并通过 props 稳定传入子组件。
✅ 推荐重构方案(两步走)
1. 修改 Test 组件:移除内部数据生成,接收 questions 为受控 prop
import React, { useState } from 'react';
import { View, Text, Image, Button, StyleSheet, SafeAreaView } from 'react-native';
import { COLORS } from '../constants';
import Header from '../constants/testHeader';
// ✅ Test 现在是一个纯展示型组件,不负责数据生成
const Test = ({ questions }) => {
const [currentQuestion, setCurrentQuestion] = useState(0);
const nextQuestion = () => {
const nextIndex = (currentQuestion + 1) % questions.length;
if (nextIndex === 0) {
alert('All questions are finished.');
} else {
setCurrentQuestion(nextIndex);
}
};
const { id, img, question, answers } = questions[currentQuestion]; // 注意:原代码中拼写为 anwsers,建议统一为 answers
return (
<safeareaview style="{styles.area}"><image source="{img" uri: style="{styles.image}"></image><text style="{styles.title}">{question}</text><safeareaview style="{styles.buttonContainer}">
{answers?.map((answer, index) => (
<button key="{index}" title="{`${index" onpress="{nextQuestion}"></button>
))}
</safeareaview></safeareaview>
);
};
export default Test;
2. 在父组件(如 _layout.tsx 或路由入口)中预生成并传递 questions
// 假设这是你的路由页面(例如 app/test/_layout.tsx 或 app/test/[category].tsx)
import React from 'react';
import { useLocalSearchParams } from 'expo-router';
import Test from './Test'; // 引入重构后的 Test
import { requestQuestions } from '../constants/support';
const TestLayout = () => {
const { category } = useLocalSearchParams();
// ✅ 只在此处调用一次,生成稳定 questions 数组
const questions = requestQuestions(category as string);
return <test questions="{questions}"></test>;
};
export default TestLayout;
⚠️ 关键注意事项
-
不要在组件内调用副作用或随机函数:
requestQuestions()含有非确定性逻辑(sampleSize),绝不可置于渲染函数体内。 -
useMemo不是万能解:虽然可用useMemo(() => requestQuestions(category), [category])缓存,但仍存在风险——若category是字符串但值不稳定(如带空格/编码差异),缓存会失效;且逻辑耦合仍在组件内,违背单一职责。 -
命名与健壮性:原代码中
anwsers应为answers;访问前务必加可选链answers?.map(...)防止空值报错。 -
性能提示:若
questions.json很大,可考虑在requestQuestions中做 shallow clone 或 memoize,但本例中sampleSize返回新数组,已天然隔离。
通过将数据获取与 UI 渲染职责分离,你不仅解决了题目跳变问题,更让组件更易测试、复用和调试 —— 这正是 React “数据自顶向下流动”设计哲学的最佳实践。











