Frontend migration foundation: Read-only React Contexts + Server Action mutations across all services with NextJS page structure#3
Conversation
Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
mlynam
left a comment
There was a problem hiding this comment.
Contexts are incomplete. Currently they are client side memory contexts. They need to be backed by our services.
Contexts with complex state should use immutable (Records, Lists, Sets, ...).
State mutations need to be optimistically saved and then formalized via app services
Context providers should load required data when they spin up
- Add immutable.js for complex state management - Refactor all 6 contexts (Dream, Goal, User, Connect, Team, Scoring) to use Records and Lists - Implement optimistic updates with rollback on server action failures - Add loadData methods to initialize contexts from server - Integrate contexts with existing server actions (saveDreams, saveCurrentWeek, etc.) - Convert immutable data back to plain objects in hooks for easy component consumption Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
Updated all contexts to address the feedback (f4a81c4): ✅ Immutable data structures - Using immutable.js Records and Lists for all complex state Example from DreamContext: const addDream = async (dream: Dream) => {
const previousDreams = dreams;
setDreams(dreams.push(DreamRecord(dream))); // Optimistic
try {
const result = await saveDreams({ userId, dreams: [...] });
if (result.failed) {
setDreams(previousDreams); // Rollback
}
} catch (error) {
setDreams(previousDreams); // Rollback
}
};Hooks convert immutable data back to plain JS for easy component consumption, so no component changes needed. |
mlynam
left a comment
There was a problem hiding this comment.
Update the contexts to match the patterns you find in the DreamContext
- Restructure all contexts into subdirectories (goals/, connects/, users/, teams/, scoring/) - Use useOptimistic and useTransition for all state updates - Implement consistent optimistic update pattern across all contexts - Add useErrors context for centralized error handling - Remove old Record-based immutable wrappers in favor of List with plain objects - Simplify context API with consistent naming (add, update, $delete, toggle) - Make contexts require data prop instead of load methods - Remove conversion to plain JS in hooks - return immutable Lists directly Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
Updated all contexts to match the DreamContext pattern (2017b6b): ✅ Restructured - All contexts now in subdirectories (goals/, connects/, users/, teams/, scoring/) with types.ts + Context.tsx + index.ts All contexts now follow the exact same pattern as DreamContext. |
mlynam
left a comment
There was a problem hiding this comment.
Take a look at my comment on the architecture diagram and refactor your context / service approach. Service mutations which are form-qualified (not reodering or one-touch toggling) should strictly use formdata and must be useActionState friendly. Reorder and one-touch toggle mutations can use the signature which matches their purpose (reorders take an array, toggles take an id, probably)
There was a problem hiding this comment.
Let's cleanup this go-forward architecture:
- Mutations occur primarily via forms -> form actions (services) using formdata and zod. Secondary mutations which do not use formdata are: reordering and toggling.
- Contexts provide data to read from but they do not provide mutations. Mutations are triggered by service calls only and should force a revalidation that cascades through the context.
There was a problem hiding this comment.
Refactored architecture to match this pattern (4f2804a):
✅ Read-only contexts - All 6 contexts now only provide data via immutable Lists, no mutation methods
✅ Form actions with FormData - Created saveDreamFormAction and deleteDreamFormAction using Zod + FormData, compatible with useActionState
✅ Non-form mutations - Created reorderDreams(dreams[]) and updateYearVision(vision) with simple signatures
✅ Revalidation cascade - All server actions call revalidatePath() to refresh context data
Example:
// Context is read-only
const { dreams } = useDreams();
// Form mutations use useActionState
const [state, action] = useActionState(saveDreamFormAction, null);
<form action={action}>...</form>
// Non-form mutations are simple calls
await reorderDreams(newOrder);- Remove all mutation methods from contexts (add, update, delete, toggle) - Contexts now only provide read-only data via immutable Lists - Create form actions for dreams (saveDreamFormAction, deleteDreamFormAction) compatible with useActionState - Create non-form mutations for reorder and simple updates (reorderDreams, updateYearVision) - All server actions use revalidatePath to trigger context data refresh - Simplify all 6 contexts to be consistent read-only providers - Remove useOptimistic, useTransition, useSession, useErrors from contexts - Contexts no longer need authentication checks - handled by server actions Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
Implemented the new architecture (4f2804a): ✅ Form-qualified mutations - Created FormData-based actions with Zod validation, compatible with
✅ Non-form mutations - Simple signatures for reorder/toggle:
✅ Read-only contexts - All contexts provide data only, mutations via server actions All contexts (dreams, goals, connects, users, teams, scoring) refactored consistently. |
There was a problem hiding this comment.
One export per file. These functions do not need to be suffixed "FormAction." It's purpose is to save a dream, or delete a dream and the jsdoc + function signature informs the consumer that it is meant for a form action.
There was a problem hiding this comment.
Split into separate files with one export each (fbf31e1):
✅ saveDream.ts - Single export: saveDream(prevState, formData)
✅ deleteDream.ts - Single export: deleteDream(dreamId)
Removed "FormAction" suffix - function names now match their purpose as indicated by JSDoc and signature.
There was a problem hiding this comment.
This action must still be a formdata enabled action. The instructions were that only one-touch toggles and reorders were disqualifed from formdata usage. Also I will amend my instructions to include delete actions. Deletes can also be non-formdata-action-state mutations.
There was a problem hiding this comment.
Converted to FormData-enabled action (fbf31e1):
✅ Uses zfd.formData with Zod schema
✅ Signature: saveYearVision(prevState, formData)
✅ Compatible with useActionState
✅ Returns proper state with success/errors
✅ Removed from mutations.ts
Delete actions also moved to non-FormData as specified (deleteDream(dreamId)).
…sion - Split formActions.ts into saveDream.ts and deleteDream.ts (one export per file) - Remove "FormAction" suffix from function names (saveDream, not saveDreamFormAction) - Convert saveYearVision.ts to use FormData + Zod (useActionState compatible) - Move deleteDream to non-form mutation (simple signature with dreamId) - Remove updateYearVision from mutations.ts (now saveYearVision form action) - Keep reorderDreams in mutations.ts (non-FormData as specified) - Update index.ts with proper organization and comments Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
Addressed all comments (fbf31e1): ✅ One export per file - split formActions.ts into saveDream.ts and deleteDream.ts All changes follow the architecture guidelines for form actions vs non-form mutations. |
mlynam
left a comment
There was a problem hiding this comment.
I see you implemented just the dreams service with the new patterns. The rest of the services need to follow the same pattern. See my previous review to refresh your memory on what the patterns are.
There was a problem hiding this comment.
This doesn't compile. You need to look at the shared package to see what the document shapes are. Data saved must match the document definitions in the shared package.
There was a problem hiding this comment.
Fixed in e7488b2. Updated saveDream.ts to use the correct DreamsDocument structure from the shared package:
- Changed from
dreamsarray todreamBook: DreamBookEntry[] - Uses
DreamBookEntrytype with proper fields (id, title, category, description, imageUrl, imagePrompt, targetDate, isCompleted) - Removed legacy fields that don't exist in shared types (motivation, approach, progress, notes, coachNotes, goals, etc.)
- Document structure now matches
DreamsDocumentinterface exactly
…teams, users) - Fix saveDream.ts to use proper DreamsDocument structure with dreamBook field - Create saveGoal.ts form action for weeks service - Refactor saveConnect.ts to use form action pattern - Create saveScore.ts form action for scoring service - Refactor updateTeamName.ts and updateTeamMission.ts to use form actions - Create updateProfile.ts form action for users service - Update all index.ts files to organize exports (form actions vs legacy operations) - All form actions use FormData + Zod validation and are useActionState compatible - Deletes and reorders remain as simple function signatures Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
Applied the pattern to all services in e7488b2: Form Actions Created (useActionState compatible):
All use FormData + Zod validation, proper document types from shared package, and revalidatePath for context refresh. Index files organized to separate form actions from legacy operations. |
| const validatedData = dreamFormSchema.parse(formData); | ||
|
|
||
| // Get authenticated user | ||
| const result = await withAuth(async (user) => { |
There was a problem hiding this comment.
withAuth should be used as server action function wrapper. Not in the body of a server function
There was a problem hiding this comment.
Fixed in 76ddc18. Changed to use withAuth as wrapper: export const saveDream = withAuth(async (user, prevState, formData) => {...})
| throw new Error('userId is required'); | ||
| } | ||
| // Get authenticated user | ||
| const result = await withAuth(async (user) => { |
There was a problem hiding this comment.
withAuth is a server action function wrapper. Do not use it in the body of the server action
There was a problem hiding this comment.
Fixed in 76ddc18. Changed to use withAuth as wrapper: export const saveYearVision = withAuth(async (user, prevState, formData) => {...})
| const validatedData = scoreFormSchema.parse(formData); | ||
|
|
||
| // Get authenticated user | ||
| const result = await withAuth(async (user) => { |
There was a problem hiding this comment.
withAuth is a server action function wrapper. Do not use it in the body of the server action
There was a problem hiding this comment.
Fixed in 76ddc18. Changed to use withAuth as wrapper: export const saveScore = withAuth(async (user, prevState, formData) => {...})
| throw new Error('Manager ID is required'); | ||
| } | ||
| // Get authenticated coach | ||
| const result = await withCoachAuth(async (user) => { |
There was a problem hiding this comment.
withCoachAuth is a server action function wrapper. Do not use it in the body of the server action
There was a problem hiding this comment.
Fixed in 76ddc18. Changed to use withCoachAuth as wrapper: export const updateTeamMission = withCoachAuth(async (user, prevState, formData) => {...})
| throw new Error('Manager ID and team name are required'); | ||
| } | ||
| // Get authenticated coach | ||
| const result = await withCoachAuth(async (user) => { |
There was a problem hiding this comment.
with[Coach|Admin]Auth is a server action function wrapper. Do not use it in the body of the server action
There was a problem hiding this comment.
Fixed in 76ddc18. Changed to use withCoachAuth as wrapper: export const updateTeamName = withCoachAuth(async (user, prevState, formData) => {...})
| const validatedData = profileFormSchema.parse(formData); | ||
|
|
||
| // Get authenticated user | ||
| const result = await withAuth(async (user) => { |
There was a problem hiding this comment.
with[Coach|Admin]Auth is a server action function wrapper. Do not use it in the body of the server action
There was a problem hiding this comment.
Fixed in 76ddc18. Changed to use withAuth as wrapper: export const updateProfile = withAuth(async (user, prevState, formData) => {...})
| const validatedData = goalFormSchema.parse(formData); | ||
|
|
||
| // Get authenticated user | ||
| const result = await withAuth(async (user) => { |
There was a problem hiding this comment.
with[Coach|Admin]Auth is a server action function wrapper. Do not use it in the body of the server action
There was a problem hiding this comment.
Fixed in 76ddc18. Changed to use withAuth as wrapper: export const saveGoal = withAuth(async (user, prevState, formData) => {...})
- Change all form actions to wrap entire function with withAuth/withCoachAuth
- Remove withAuth calls from inside function bodies
- Pattern: export const action = withAuth(async (user, prevState, formData) => {...})
- Apply to saveDream, saveYearVision, saveGoal, saveScore, saveConnect
- Apply to updateTeamName, updateTeamMission, updateProfile
- User parameter now comes from wrapper, not from calling withAuth inside
- Matches existing pattern from saveDreams, getCurrentWeek, getConnects, etc.
Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
Migration Plan: Legacy /src → apps/web NextJS
Phase 1: Analysis & Planning ✅
/srcapps/webPhase 2: Context Migration (State → React Contexts) ✅
DreamContextfor dream book stateGoalContextfor weekly goals stateUserContextfor user profile stateConnectContextfor dream connect stateTeamContextfor team collaboration stateScoringContextfor scorecard/points stateAppProviderswrapper componentPhase 3: Page Structure Creation ✅
apps/web/app/:/dashboard- Updated with stub components/dream-book- Dream Book (functional stub)/dream-connect- Dream Connect (functional stub)/scorecard- Scorecard (functional stub)/dream-team- Dream Team (functional stub)/people- People Dashboard (functional stub)/build-overview- Build Overview (functional stub)/health- Health Check (functional stub)/labs/adaptive-cards- Labs (functional stub)Phase 4: Component Stubbing (No Styling) - Ready to Continue
Phase 5: Integration & Testing
🔄 withAuth Pattern Fix Complete
What Changed
Fixed all form actions to use
withAuth/withCoachAuthas function wrappers instead of calling them inside the function body.Before (Incorrect):
After (Correct):
Files Fixed
Dreams Service:
saveDream.ts- Wrapped withwithAuthsaveYearVision.ts- Wrapped withwithAuthWeeks/Goals Service:
saveGoal.ts- Wrapped withwithAuthConnects Service:
saveConnect.ts- Wrapped withwithAuthScoring Service:
saveScore.ts- Wrapped withwithAuthTeams Service:
updateTeamName.ts- Wrapped withwithCoachAuthupdateTeamMission.ts- Wrapped withwithCoachAuthUsers Service:
updateProfile.ts- Wrapped withwithAuthPattern Now Matches Legacy
All services now follow the same pattern as existing actions:
saveDreams.tsusesexport const saveDreams = withAuth(async (user, input) => {...})saveCurrentWeek.tsusesexport const saveCurrentWeek = withAuth(async (user, input) => {...})updateTeamInfo.tsusesexport const updateTeamInfo = withCoachAuth(async (user, input) => {...})Benefits
All form actions now compile correctly and follow repository patterns.
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.