use-multiple-choice-state.ts
1 import { useCallback, useReducer } from 'react' 2 3 export type AnswerValue = string 4 5 export type QuestionState = { 6 selectedValue?: string | string[] 7 textInputValue: string 8 } 9 10 type State = { 11 currentQuestionIndex: number 12 answers: Record<string, AnswerValue> 13 questionStates: Record<string, QuestionState> 14 isInTextInput: boolean 15 } 16 17 type Action = 18 | { type: 'next-question' } 19 | { type: 'prev-question' } 20 | { 21 type: 'update-question-state' 22 questionText: string 23 updates: Partial<QuestionState> 24 isMultiSelect: boolean 25 } 26 | { 27 type: 'set-answer' 28 questionText: string 29 answer: string 30 shouldAdvance: boolean 31 } 32 | { type: 'set-text-input-mode'; isInInput: boolean } 33 34 function reducer(state: State, action: Action): State { 35 switch (action.type) { 36 case 'next-question': 37 return { 38 ...state, 39 currentQuestionIndex: state.currentQuestionIndex + 1, 40 isInTextInput: false, 41 } 42 43 case 'prev-question': 44 return { 45 ...state, 46 currentQuestionIndex: Math.max(0, state.currentQuestionIndex - 1), 47 isInTextInput: false, 48 } 49 50 case 'update-question-state': { 51 const existing = state.questionStates[action.questionText] 52 const newState: QuestionState = { 53 selectedValue: 54 action.updates.selectedValue ?? 55 existing?.selectedValue ?? 56 (action.isMultiSelect ? [] : undefined), 57 textInputValue: 58 action.updates.textInputValue ?? existing?.textInputValue ?? '', 59 } 60 61 return { 62 ...state, 63 questionStates: { 64 ...state.questionStates, 65 [action.questionText]: newState, 66 }, 67 } 68 } 69 70 case 'set-answer': { 71 const newState = { 72 ...state, 73 answers: { 74 ...state.answers, 75 [action.questionText]: action.answer, 76 }, 77 } 78 79 if (action.shouldAdvance) { 80 return { 81 ...newState, 82 currentQuestionIndex: newState.currentQuestionIndex + 1, 83 isInTextInput: false, 84 } 85 } 86 87 return newState 88 } 89 90 case 'set-text-input-mode': 91 return { 92 ...state, 93 isInTextInput: action.isInInput, 94 } 95 } 96 } 97 98 const INITIAL_STATE: State = { 99 currentQuestionIndex: 0, 100 answers: {}, 101 questionStates: {}, 102 isInTextInput: false, 103 } 104 105 export type MultipleChoiceState = { 106 currentQuestionIndex: number 107 answers: Record<string, AnswerValue> 108 questionStates: Record<string, QuestionState> 109 isInTextInput: boolean 110 nextQuestion: () => void 111 prevQuestion: () => void 112 updateQuestionState: ( 113 questionText: string, 114 updates: Partial<QuestionState>, 115 isMultiSelect: boolean, 116 ) => void 117 setAnswer: ( 118 questionText: string, 119 answer: string, 120 shouldAdvance?: boolean, 121 ) => void 122 setTextInputMode: (isInInput: boolean) => void 123 } 124 125 export function useMultipleChoiceState(): MultipleChoiceState { 126 const [state, dispatch] = useReducer(reducer, INITIAL_STATE) 127 128 const nextQuestion = useCallback(() => { 129 dispatch({ type: 'next-question' }) 130 }, []) 131 132 const prevQuestion = useCallback(() => { 133 dispatch({ type: 'prev-question' }) 134 }, []) 135 136 const updateQuestionState = useCallback( 137 ( 138 questionText: string, 139 updates: Partial<QuestionState>, 140 isMultiSelect: boolean, 141 ) => { 142 dispatch({ 143 type: 'update-question-state', 144 questionText, 145 updates, 146 isMultiSelect, 147 }) 148 }, 149 [], 150 ) 151 152 const setAnswer = useCallback( 153 (questionText: string, answer: string, shouldAdvance: boolean = true) => { 154 dispatch({ 155 type: 'set-answer', 156 questionText, 157 answer, 158 shouldAdvance, 159 }) 160 }, 161 [], 162 ) 163 164 const setTextInputMode = useCallback((isInInput: boolean) => { 165 dispatch({ type: 'set-text-input-mode', isInInput }) 166 }, []) 167 168 return { 169 currentQuestionIndex: state.currentQuestionIndex, 170 answers: state.answers, 171 questionStates: state.questionStates, 172 isInTextInput: state.isInTextInput, 173 nextQuestion, 174 prevQuestion, 175 updateQuestionState, 176 setAnswer, 177 setTextInputMode, 178 } 179 }