rn-state-flowsSkill rn-state-flows

React Native中复杂多步骤操作的实现指南,包括状态机模式、显式流程实现、流程对象、流程状态跟踪、集成测试流程和流程文档。

移动开发 0 次安装 0 次浏览 更新于 3/3/2026

复杂状态流

问题陈述

多步骤操作之间存在依赖关系,容易出现顺序错误、遗漏先决条件和未测试的边缘情况。即使没有正式的状态机库,用状态和转换思考也能预防错误。


模式:状态机思维

**问题:**复杂流程有隐含状态未被建模,导致无效转换。

示例 - 重考流程状态:

IDLE → LOADING_COMPLETED → ENABLING_RETAKE → CLEARING_ANSWERS → READY → ANSWERING → MERGING → SUBMITTING → COMPLETE
                                                                                                              ↓
                                                                                                           ERROR

每个转换应包含:

  1. 先决条件 - 此步骤之前必须为真的条件
  2. 动作 - 此步骤期间发生的事情
  3. 后决条件 - 此步骤之后必须为真的条件
  4. 错误处理 - 如果此步骤失败,应该做什么
// 明确记录流程
/*
 * RETAKE FLOW
 * 
 * State: IDLE
 * Precondition: assessment exists
 * Action: loadCompletedAssessmentAnswers
 * Postcondition: completedAssessmentAnswers populated
 * 
 * State: LOADING_COMPLETED
 * Precondition: completedAssessmentAnswers loaded
 * Action: enableSkillAreaRetake
 * Postcondition: skillArea in retakeAreas set
 * 
 * State: ENABLING_RETAKE
 * Precondition: skillArea in retakeAreas
 * Action: clearSkillAreaAnswers
 * Postcondition: existing answers for skillArea removed
 * 
 * ... continue for each state
 */

模式:显式流程实现

**问题:**流程逻辑分散在多个函数中,难以验证顺序。

// 错误 - 隐式流程,容易遗漏步骤或顺序错误
async function startRetake(assessmentId: string, skillArea: string) {
  loadCompletedAssessmentAnswers(assessmentId); // Missing await!
  await enableSkillAreaRetake(skillArea);
  await clearSkillAreaAnswers(skillArea);
}

// 正确 - 显式流程带验证
async function startRetake(assessmentId: string, skillArea: string) {
  const flowId = `retake-${Date.now()}`;
  logger.info(`[${flowId}] Starting retake flow`, { assessmentId, skillArea });
  
  // 第1步:加载完成的答案
  await loadCompletedAssessmentAnswers(assessmentId);
  const completedAnswers = useStore.getState().completedAssessmentAnswers;
  if (Object.keys(completedAnswers).length === 0) {
    throw new Error(`[${flowId}] Failed to load completed answers`);
  }
  logger.debug(`[${flowId}] Loaded ${Object.keys(completedAnswers).length} answers`);
  
  // 第2步:启用技能区域重考
  await enableSkillAreaRetake(skillArea);
  const retakeAreas = useStore.getState().retakeAreas;
  if (!retakeAreas.has(skillArea)) {
    throw new Error(`[${flowId}] Failed to enable retake for ${skillArea}`);
  }
  logger.debug(`[${flowId}] Enabled retake for ${skillArea}`);
  
  // 第3步:清除现有答案
  await clearSkillAreaAnswers(skillArea);
  logger.debug(`[${flowId}] Cleared answers for ${skillArea}`);
  
  logger.info(`[${flowId}] Retake flow completed`);
}

模式:流程对象

**问题:**长异步函数包含多个步骤变得难以处理。

interface FlowStep<TContext> {
  name: string;
  execute: (context: TContext) => Promise<void>;
  validate?: (context: TContext) => void;  // 后决条件检查
}

interface RetakeContext {
  assessmentId: string;
  skillArea: string;
  flowId: string;
}

const retakeSteps: FlowStep<RetakeContext>[] = [
  {
    name: 'loadCompletedAnswers',
    execute: async (ctx) => {
      await loadCompletedAssessmentAnswers(ctx.assessmentId);
    },
    validate: (ctx) => {
      const answers = useStore.getState().completedAssessmentAnswers;
      if (Object.keys(answers).length === 0) {
        throw new Error(`[${ctx.flowId}] No completed answers loaded`);
      }
    },
  },
  {
    name: 'enableRetake',
    execute: async (ctx) => {
      await enableSkillAreaRetake(ctx.skillArea);
    },
    validate: (ctx) => {
      const retakeAreas = useStore.getState().retakeAreas;
      if (!retakeAreas.has(ctx.skillArea)) {
        throw new Error(`[${ctx.flowId}] Retake not enabled for ${ctx.skillArea}`);
      }
    },
  },
  {
    name: 'clearAnswers',
    execute: async (ctx) => {
      await clearSkillAreaAnswers(ctx.skillArea);
    },
  },
];

async function executeFlow<TContext>(
  steps: FlowStep<TContext>[],
  context: TContext,
  flowName: string
) {
  const flowId = `${flowName}-${Date.now()}`;
  logger.info(`[${flowId}] Starting flow`, context);
  
  for (const step of steps) {
    logger.debug(`[${flowId}] Executing: ${step.name}`);
    try {
      await step.execute(context);
      if (step.validate) {
        step.validate(context);
      }
      logger.debug(`[${flowId}] Completed: ${step.name}`);
    } catch (error) {
      logger.error(`[${flowId}] Failed at: ${step.name}`, { error: error.message });
      throw error;
    }
  }
  
  logger.info(`[${flowId}] Flow completed`);
}

// 使用方法
await executeFlow(retakeSteps, { assessmentId, skillArea, flowId }, 'retake');

模式:流程状态跟踪

**问题:**组件需要知道当前流程状态以提供UI反馈。

type RetakeFlowState =
  | { status: 'idle' }
  | { status: 'loading'; step: string }
  | { status: 'ready' }
  | { status: 'answering'; answeredCount: number }
  | { status: 'submitting' }
  | { status: 'complete' }
  | { status: 'error'; message: string; step: string };

const useRetakeStore = create<{
  flowState: RetakeFlowState;
  setFlowState: (state: RetakeFlowState) => void;
}>((set) => ({
  flowState: { status: 'idle' },
  setFlowState: (flowState) => set({ flowState }),
}));

async function startRetake(assessmentId: string, skillArea: string) {
  const { setFlowState } = useRetakeStore.getState();
  
  try {
    setFlowState({ status: 'loading', step: 'loadingAnswers' });
    await loadCompletedAssessmentAnswers(assessmentId);
    
    setFlowState({ status: 'loading', step: 'enablingRetake' });
    await enableSkillAreaRetake(skillArea);
    
    setFlowState({ status: 'loading', step: 'clearingAnswers' });
    await clearSkillAreaAnswers(skillArea);
    
    setFlowState({ status: 'ready' });
  } catch (error) {
    setFlowState({ 
      status: 'error', 
      message: error.message,
      step: useRetakeStore.getState().flowState.step,
    });
  }
}

// 组件使用
function RetakeScreen() {
  const flowState = useRetakeStore((s) => s.flowState);
  
  if (flowState.status === 'loading') {
    return <Loading step={flowState.step} />;
  }
  
  if (flowState.status === 'error') {
    return <Error message={flowState.message} step={flowState.step} />;
  }
  
  // ...根据状态渲染
}

模式:集成测试流程

**问题:**单元测试单个函数无法捕获流程级别的错误。

describe('Retake Flow', () => {
  beforeEach(() => {
    useAssessmentStore.getState()._reset();
  });

  it('persists answers through complete retake flow', async () => {
    const assessmentId = 'test-assessment';
    const skillArea = 'fundamentals';
    const store = useAssessmentStore;
    
    // 设置:模拟现有完成的评估
    store.getState().setCompletedAnswers(assessmentId, mockCompletedAnswers);
    
    // 执行完整流程
    await store.getState().loadCompletedAssessmentAnswers(assessmentId);
    
    // 验证后决条件
    expect(Object.keys(store.getState().completedAssessmentAnswers).length)
      .toBeGreaterThan(0);
    
    await store.getState().enableSkillAreaRetake(skillArea);
    
    // 验证后决条件
    expect(store.getState().retakeAreas.has(skillArea)).toBe(true);
    
    await store.getState().clearSkillAreaAnswers(skillArea);
    
    // 模拟用户回答
    await store.getState().saveAnswer('q1', 4);
    
    // 关键检查 - 答案是否持久化?
    expect(store.getState().userAnswers['q1']).toBe(4);
    
    // 完成流程
    await store.getState().submitRetake(assessmentId);
    
    // 验证最终状态
    expect(store.getState().flowState.status).toBe('complete');
  });

  it('handles error at each step', async () => {
    // 测试第1步错误处理
    mockApi.loadAnswers.mockRejectedValueOnce(new Error('Network error'));
    
    await expect(
      store.getState().startRetake(assessmentId, skillArea)
    ).rejects.toThrow('Network error');
    
    expect(store.getState().flowState.status).toBe('error');
    expect(store.getState().flowState.step).toBe('loadingAnswers');
  });
});

模式:流程文档

用图表记录复杂流程,以便团队理解:

## 重考流程

### 快乐路径

┌─────────┐ ┌──────────────┐ ┌───────────────┐ ┌───────────────┐ │ Start │────▶│ Load Answers │────▶│ Enable Retake │────▶│ Clear Answers │ └─────────┘ └──────────────┘ └───────────────┘ └───────────────┘ │ │ │ ▼ ▼ ▼ Postcondition: Postcondition: Postcondition: answers.length > 0 retakeAreas.has(x) cleared for area

                                                               │
                                                               ▼

┌──────────┐ ┌─────────┐ ┌───────────────┐ ┌──────────────────┐ │ Complete │◀────│ Merge │◀────│ User Answers │◀────│ Ready for Input │ └──────────┘ └─────────┘ └───────────────┘ └──────────────────┘


### 错误状态

任何步骤失败 → 转换到ERROR状态,并带有步骤上下文。
从ERROR:用户可以重试(返回IDLE)或退出。

清单:设计复杂流程

实施前:

  • [ ] 绘制状态图(即使是在纸上)
  • [ ] 确定所有状态,包括错误状态
  • [ ] 记录每个转换的先决条件
  • [ ] 记录后决条件以验证
  • [ ] 计划如何向UI展示状态

实施中:

  • [ ] 验证每个步骤之前的先决条件
  • [ ] 验证每个步骤之后的后决条件
  • [ ] 用流程ID记录状态转换
  • [ ] 每个步骤处理错误并带有上下文
  • [ ] 为UI反馈展示流程状态

实施后:

  • [ ] 集成测试快乐路径
  • [ ] 集成测试每个步骤的错误
  • [ ] 验证日志是否足够用于调试
  • [ ] 为团队记录流程

何时使用XState

考虑在以下情况下使用XState:

  • 流程有> 6个状态
  • 复杂的分支/并行状态
  • 需要可视化/调试工具
  • 状态机在团队中共享

对于更简单的流程,显式步骤带验证(如上所示)通常足够且更易读。