You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+TS Quiz应用条件渲染时如何合规调用自定义Hook

React Context 复用自定义Hook时的加载状态实现方案

问题背景

  • 基于React + TypeScript开发测验应用,封装QuizProvider做状态上下文,通过自定义Hook useQuiz 对外暴露游戏全量逻辑:读取URL分类参数、处理后端题目数据,返回游戏运行所需的状态与操作方法。
  • 现有矛盾:如果在JSX中加条件判断(比如加载题目时展示加载页),担心违反Hooks调用规则;如果把useQuiz逻辑全内联到Provider里,又会导致代码冗余、可读性差。
  • 当前临时处理:首次渲染questions为undefined时,引入本地questionsJson作为默认mock数据兜底,仅作演示用,需要替换为正式的加载逻辑。

现有代码

QuizProvider 实现

import { createContext, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { Question, Provider } from '../types';
import useQuiz from '../hooks/useQuiz';
import questionsJson from '../lib/questions.json';
import useFetch from '../hooks/useFetch';

export const QuizContext = createContext({} as ReturnType<typeof useQuiz>);

export default function _QuizProvider({ children }: Provider) {
    const { pathname } = useLocation();
    const { category } = useParams();
    const { fetchData: fetchQuestions, data: questions, loading, error } = useFetch<Question[]>();
    
    useEffect(() => {
        fetchQuestions(`${pathname}/questions`, 'GET');
    } , []);

    return (
        <QuizContext.Provider value={useQuiz({ category: category!, questions: questions || questionsJson.geography })}>
            {children}
        </QuizContext.Provider>
    );
}

useQuiz 自定义Hook实现

import { useState } from 'react';
import { Answer, Quiz, useQuiz } from '../types';

export default function _useQuiz({ category, questions }: useQuiz): Quiz {
    const QUESTIONS_TIMER  = 10 * 60;

    const [questionNo, setQuestionNo] = useState(1);
    const [answers, setAnswers] = useState(new Array<Answer>(questions.length));
    const [finish, setFinish] = useState(false);

    return {
        category,
        questions,
        timer: QUESTIONS_TIMER,
        questionNo,
        totalQuestions: questions.length,
        currentQuestion: questions[questionNo - 1],
        finish,
        
        nextQuestion: () => setQuestionNo(questionNo + 1),
        previousQuestion: () => setQuestionNo(questionNo - 1),
        toggleQuestion: (index: number) => setQuestionNo(index),

        onAnswer: (answer: Answer) => {
            const newAnswers = [...answers];
            newAnswers[questionNo - 1] = answer;
            setAnswers(newAnswers);
        },
        
        isSelected: (answer: Answer) =>
            JSON.stringify(answers[questionNo - 1]) === JSON.stringify(answer),
        
        isAnswersMarked: () =>
            answers.every(answer => answer !== undefined),
        
        finishQuiz: () => setFinish(true),

        score: () => 
            answers.filter(answer => answer && answer.correct).length
    };
}

实现方案

Hooks规则的核心要求是每次组件渲染时,Hook的调用顺序、调用次数必须保持一致,并没有禁止条件渲染——只要所有Hook都放在组件顶层、不包裹在条件/循环/嵌套函数内,Hook执行完成后再做条件判断返回不同UI,完全符合规范。

改造后的QuizProvider

把useQuiz调用移到组件顶层,加载/错误状态判断放在所有Hook执行完成后,同时移除临时mock数据依赖:

import { createContext, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { Question, Provider } from '../types';
import useQuiz from '../hooks/useQuiz';
import useFetch from '../hooks/useFetch';

export const QuizContext = createContext({} as ReturnType<typeof useQuiz>);

export default function _QuizProvider({ children }: Provider) {
    // 所有Hook顶层无条件调用,保证渲染顺序稳定
    const { pathname } = useLocation();
    const { category } = useParams();
    const { fetchData: fetchQuestions, data: questions, loading, error } = useFetch<Question[]>();
    
    useEffect(() => {
        fetchQuestions(`${pathname}/questions`, 'GET');
    }, [pathname, fetchQuestions]); // 补全依赖项,避免闭包陈旧问题

    // 加载阶段传入空数组兜底,保证Hook初始化不报错
    const quizContextValue = useQuiz({ 
        category: category ?? '', 
        questions: questions ?? [] 
    });

    // Hook全部执行完成后再做条件渲染,不违反任何规则
    if (loading) return <div className="loading">题目加载中...</div>;
    if (error || !questions?.length) return <div className="error">题目加载失败,请刷新重试</div>;

    return (
        <QuizContext.Provider value={quizContextValue}>
            {children}
        </QuizContext.Provider>
    );
}

适配useQuiz的边界逻辑

针对加载阶段传入空数组的场景,补充简单的边界判断,避免数组越界报错:

import { useState } from 'react';
import { Answer, Quiz, useQuiz as UseQuizProps } from '../types';

export default function _useQuiz({ category, questions }: UseQuizProps): Quiz {
    const QUESTIONS_TIMER = 10 * 60;
    const [questionNo, setQuestionNo] = useState(1);
    const [answers, setAnswers] = useState<Answer[]>(() => new Array(questions.length));
    const [finish, setFinish] = useState(false);

    return {
        category,
        questions,
        timer: QUESTIONS_TIMER,
        questionNo,
        totalQuestions: questions.length,
        currentQuestion: questions[questionNo - 1] ?? null,
        finish,
        
        nextQuestion: () => questionNo < questions.length && setQuestionNo(prev => prev + 1),
        previousQuestion: () => questionNo > 1 && setQuestionNo(prev => prev - 1),
        toggleQuestion: (index: number) => index >= 1 && index <= questions.length && setQuestionNo(index),

        onAnswer: (answer: Answer) => {
            setAnswers(prev => {
                const newAnswers = [...prev];
                newAnswers[questionNo - 1] = answer;
                return newAnswers;
            });
        },
        
        isSelected: (answer: Answer) => {
            const current = answers[questionNo - 1];
            return current ? JSON.stringify(current) === JSON.stringify(answer) : false;
        },
        
        isAnswersMarked: () => questions.length > 0 && answers.every(ans => ans !== undefined),
        
        finishQuiz: () => setFinish(true),

        score: () => answers.filter(ans => ans?.correct).length
    };
}

方案优势

  • 完全符合Hooks调用规则,没有任何规范问题
  • 保留useQuiz的独立封装,不需要把逻辑内联到Provider中,代码分层清晰
  • 移除了临时mock数据依赖,完整支持加载、错误状态的UI展示
  • 补充了原代码中缺失的useEffect依赖、操作边界判断,修复了潜在的闭包问题、数组越界问题

内容的提问来源于stack exchange,提问作者Shalev Sror

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 23:39:24