The frontend had fragmented state management with multiple issues:
- No centralized question state - Questions lived in local
useStateacrossQuestionGeneration.tsxandReviewEdit.tsx, causing data inconsistency between views. - Global single boolean for generation status -
questionsGeneratingwas a singlebooleaninplanSlice, meaning only one quiz could track generation at a time. Switching quizzes during generation lost the state. window.storehack -LearningObjectives.tsxusedisQuestionsGenerating()which readwindow.store.getState()directly, bypassing React's reactivity system.appSlicedead code -appSliceduplicated course/quiz data that was never synced with the backend.- Direct API calls scattered everywhere - Each component independently called
questionsApiwith no shared cache.
| File | Purpose |
|---|---|
src/store/slices/questionSlice.ts |
Redux slice for per-quiz question management. Thunks: fetchQuestions, deleteQuestion, updateQuestion, deleteAllQuestions. Reducers: setQuestionsForQuiz, addQuestionForQuiz, clearQuestionsForQuiz. |
src/store/selectors.ts |
Memoizable selectors: selectQuestionsByQuiz, selectQuestionsLoading, selectIsGenerating, selectGenerationStatus. |
- Replaced
questionsGenerating: boolean+questionGenerationStartTime+currentQuizIdwithgenerationStatusByQuiz: Record<string, GenerationStatus>. setQuestionsGeneratingnow takes{ generating, quizId, totalQuestions? }and writes to a per-quiz map.- Added
clearGenerationStatusreducer.
- Added
questionSliceto the store's reducer config.
- Made
quizIdrequired inSetQuestionsGeneratingPayload. - Removed
store.getState()call for question count.
- Removed
isQuestionsGenerating()function (thewindow.storehack). - Replaced all 6 call sites with
useSelector+selectIsGenerating(state, quizId). - Removed debug
useEffectand test button.
- Replaced local
questions/setQuestions/hasExistingQuestionsuseState with ReduxselectQuestionsByQuiz. loadExistingQuestions()replaced withdispatch(fetchQuestions(quizId)).handleDeleteExistingQuestionsusesdeleteAllQuestionsthunk.handleGoBackToPlanusessetQuestionsForQuiz.onBatchCompleteSSE callback dispatches to Redux.- PubSub subscriptions (
OBJECTIVES_DELETED,QUESTIONS_DELETED) dispatchfetchQuestions.
- Added
useDispatch<AppDispatch>()anduseSelectorwithselectQuestionsByQuiz. - Replaced direct
questionsApi.getQuestions()load withdispatch(fetchQuestions(quizId)). - Added
useEffectto syncreduxQuestions-> localquestionsstate (preservingisEditingUI flag). - PubSub subscriptions (
QUESTION_GENERATION_COMPLETED,OBJECTIVES_DELETED) now dispatchfetchQuestionsinstead of calling API directly. deleteQuestionrenamed tohandleDeleteQuestion, uses ReduxdeleteQuestionthunk + immediate local state update.- 30+ editing functions unchanged - They continue using local
setQuestions(questions.map(...))for in-memory edits. OnlysaveQuestionpersists to backend. This is intentional: editing is transient UI state, Redux holds the source of truth from the database.
Database (MongoDB)
|
v
Redux questionSlice (source of truth, per-quiz)
|
v useEffect sync
Local useState in ReviewEdit (adds isEditing flag)
|
v 30+ editing functions mutate local state
saveQuestion() -> questionsApi.updateQuestion() -> Redux re-fetches
src/store/slices/appSlice.ts stores courses, activeCourse, activeQuiz etc. but this data is never synced with the backend - it's always fetched fresh via API calls in components. Either:
- Remove
appSliceentirely and rely on component-level fetching. - Or make it the real source of truth by wiring it to backend API thunks.
The file is far too large. Recommended split:
src/components/review/
ReviewEdit.tsx - Main container, state management (~150 lines)
QuestionCard.tsx - Single question display/edit (~200 lines)
QuestionEditForm.tsx - Edit form for a question (~200 lines)
ManualQuestionForm.tsx - Manual question creation form (~150 lines)
InteractiveQuestionView.tsx - Interactive preview mode (~200 lines)
questionEditHandlers.ts - All 30+ editing functions extracted as pure functions
reviewTypes.ts - ExtendedQuestion, form state types
src/components/generation/
QuestionGeneration.tsx - Main container (~150 lines)
ApproachSelector.tsx - Pedagogical approach cards (~100 lines)
CustomFormulaEditor.tsx - Custom formula configuration (~150 lines)
StreamingProgress.tsx - SSE streaming progress display (~150 lines)
useStreamingGeneration.ts - SSE + streaming state hook (~200 lines)
generationTypes.ts - Types for streaming, formula, etc.
Currently saved to localStorage with 24-hour expiry. Should be stored in the quiz document or a separate config collection so it persists across devices/sessions.
If user navigates away from QuestionGeneration during streaming, the SSE connection drops and progress is lost. Options:
- Move SSE management to a Redux middleware or a global hook.
- Use a service worker for background SSE.
- At minimum, persist
sessionIdso reconnection is possible.
Currently saveQuestion calls questionsApi.updateQuestion and updates local state only. It should also dispatch updateQuestion thunk or setQuestionsForQuiz so Redux stays in sync without a full re-fetch.
Several events (QUESTION_GENERATION_COMPLETED, OBJECTIVES_DELETED) are published via PubSub and then consumed by components to dispatch Redux actions. This could be simplified by handling these events directly in Redux middleware, eliminating the PubSub middleman for Redux-managed state.