skills/cjharmath/rn-state-flows/SKILL.md
Complex multi-step operations in React Native. Use when implementing flows with multiple async steps, state machine patterns, or debugging flow ordering issues.
npx skillsauth add aiskillstore/marketplace rn-state-flowsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Multi-step operations with dependencies between steps are prone to ordering bugs, missing preconditions, and untested edge cases. Even without a formal state machine library, thinking in states and transitions prevents bugs.
Problem: Complex flows have implicit states that aren't modeled, leading to invalid transitions.
Example - Retake flow states:
IDLE → LOADING_COMPLETED → ENABLING_RETAKE → CLEARING_ANSWERS → READY → ANSWERING → MERGING → SUBMITTING → COMPLETE
↓
ERROR
Each transition should have:
// Document the flow explicitly
/*
* 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
*/
Problem: Flow logic scattered across multiple functions, hard to verify ordering.
// WRONG - implicit flow, easy to miss steps or misordering
async function startRetake(assessmentId: string, skillArea: string) {
loadCompletedAssessmentAnswers(assessmentId); // Missing await!
await enableSkillAreaRetake(skillArea);
await clearSkillAreaAnswers(skillArea);
}
// CORRECT - explicit flow with validation
async function startRetake(assessmentId: string, skillArea: string) {
const flowId = `retake-${Date.now()}`;
logger.info(`[${flowId}] Starting retake flow`, { assessmentId, skillArea });
// Step 1: Load completed answers
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`);
// Step 2: Enable retake for skill area
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}`);
// Step 3: Clear existing answers
await clearSkillAreaAnswers(skillArea);
logger.debug(`[${flowId}] Cleared answers for ${skillArea}`);
logger.info(`[${flowId}] Retake flow completed`);
}
Problem: Long async functions with many steps become unwieldy.
interface FlowStep<TContext> {
name: string;
execute: (context: TContext) => Promise<void>;
validate?: (context: TContext) => void; // Postcondition check
}
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`);
}
// Usage
await executeFlow(retakeSteps, { assessmentId, skillArea, flowId }, 'retake');
Problem: Components need to know current flow state for UI feedback.
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,
});
}
}
// Component usage
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} />;
}
// ... render based on state
}
Problem: Unit tests for individual functions don't catch flow-level bugs.
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;
// Setup: Simulate existing completed assessment
store.getState().setCompletedAnswers(assessmentId, mockCompletedAnswers);
// Execute full flow
await store.getState().loadCompletedAssessmentAnswers(assessmentId);
// Verify postcondition
expect(Object.keys(store.getState().completedAssessmentAnswers).length)
.toBeGreaterThan(0);
await store.getState().enableSkillAreaRetake(skillArea);
// Verify postcondition
expect(store.getState().retakeAreas.has(skillArea)).toBe(true);
await store.getState().clearSkillAreaAnswers(skillArea);
// Simulate user answering
await store.getState().saveAnswer('q1', 4);
// THE CRITICAL CHECK - does the answer persist?
expect(store.getState().userAnswers['q1']).toBe(4);
// Complete flow
await store.getState().submitRetake(assessmentId);
// Verify final state
expect(store.getState().flowState.status).toBe('complete');
});
it('handles error at each step', async () => {
// Test error handling at step 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');
});
});
Document complex flows with diagrams for team understanding:
## Retake Flow
### Happy Path
┌─────────┐ ┌──────────────┐ ┌───────────────┐ ┌───────────────┐ │ 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 States
Any step can fail → transition to ERROR state with step context.
From ERROR: user can retry (back to IDLE) or exit.
Before implementing:
During implementation:
After implementation:
Consider XState when:
For simpler flows, explicit steps with validation (as shown above) are often sufficient and more readable.
development
Apple Human Interface Guidelines for content display components. Use this skill when the user asks about charts component, collection view, image view, web view, color well, image well, activity view, lockup, data visualization, content display, displaying images, rendering web content, color pickers, or presenting collections of items in Apple apps. Also use when the user says how should I display charts, what's the best way to show images, should I use a web view, how do I build a grid of items, what component shows media, or how do I present a share sheet. Cross-references: hig-foundations for color/typography/accessibility, hig-patterns for data visualization patterns, hig-components-layout for structural containers, hig-platforms for platform-specific component behavior.
tools
Automate HelpDesk tasks via Rube MCP (Composio): list tickets, manage views, use canned responses, and configure custom fields. Always search tools first for current schemas.
testing
Expert Haskell engineer specializing in advanced type systems, pure functional design, and high-reliability software. Use PROACTIVELY for type-level programming, concurrency, and architecture guidance.
tools
GraphQL gives clients exactly the data they need - no more, no less. One endpoint, typed schema, introspection. But the flexibility that makes it powerful also makes it dangerous. Without proper controls, clients can craft queries that bring down your server. This skill covers schema design, resolvers, DataLoader for N+1 prevention, federation for microservices, and client integration with Apollo/urql. Key insight: GraphQL is a contract. The schema is the API documentation. Design it carefully.