Skip to content

⚡ Bolt: Optimize objective state updates#335

Open
daggerstuff wants to merge 1 commit intostagingfrom
perf/TreatmentPlanManager-shallow-copy-12345-8955394391944180226
Open

⚡ Bolt: Optimize objective state updates#335
daggerstuff wants to merge 1 commit intostagingfrom
perf/TreatmentPlanManager-shallow-copy-12345-8955394391944180226

Conversation

@daggerstuff
Copy link
Copy Markdown
Owner

@daggerstuff daggerstuff commented Mar 31, 2026

💡 What: Replaced JSON.parse/stringify with shallow copies in state updates.
🎯 Why: JSON.parse/stringify is slow for large objects and blocks the main thread.
📊 Impact: Faster UI response when modifying goals and objectives.
🔬 Measurement: Profile the component with DevTools while adding/editing/removing objectives.


PR created automatically by Jules for task 8955394391944180226 started by @daggerstuff

Summary by Sourcery

Optimize treatment plan objective and goal state updates for better performance and immutability in the TreatmentPlanManager component.

Enhancements:

  • Replace JSON.parse/stringify deep cloning with shallow copying when initializing and resetting new treatment plan form state.
  • Update add, update, and remove objective handlers to use immutable shallow copies of goals and objectives arrays instead of JSON-based deep cloning.
  • Adjust edit-plan initialization to shallow-copy goals and objectives while ensuring objectives arrays are always present.
  • Add inline comments documenting the performance-focused change to state update logic.

Summary by cubic

Optimized state updates in TreatmentPlanManager by replacing deep JSON cloning with shallow, immutable updates for goals and objectives. This reduces main-thread work and makes adding/editing/removing objectives feel faster.

  • Refactors
    • Replaced JSON.parse/stringify with array/object spreads in add, update, and delete objective flows.
    • Reset and initialize form state with shallow copies of initialNewPlanData and plan goals.
    • Preserved immutability by copying arrays/items before updates and ensuring objectives arrays exist.

Written for commit 12242c4. Summary will update on new commits.

Co-authored-by: daggerstuff <261005129+daggerstuff@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pixelated Ready Ready Preview, Comment Mar 31, 2026 10:09pm

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Warning

Rate limit exceeded

@daggerstuff has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 0 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 6 minutes and 0 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c750a290-84d5-4bcb-a40e-6f55085ac7ff

📥 Commits

Reviewing files that changed from the base of the PR and between 919a5fd and 12242c4.

📒 Files selected for processing (1)
  • src/components/therapy/TreatmentPlanManager.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/TreatmentPlanManager-shallow-copy-12345-8955394391944180226

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 31, 2026

Reviewer's Guide

Replaces JSON-based deep cloning with shallow, immutable updates for treatment plan goals/objectives in TreatmentPlanManager, improving performance and keeping React state updates structurally shared.

Sequence diagram for addObjective immutable state update

sequenceDiagram
  actor Therapist
  participant TreatmentPlanManager
  participant ReactStateNewPlanData as React_state_newPlanData

  Therapist->>TreatmentPlanManager: click AddObjective(goalIndex)
  activate TreatmentPlanManager
  TreatmentPlanManager->>TreatmentPlanManager: addObjective(goalIndex, isEdit=false)
  TreatmentPlanManager->>TreatmentPlanManager: create newObjective
  TreatmentPlanManager->>TreatmentPlanManager: updatedGoals = [...newPlanData.goals]
  TreatmentPlanManager->>TreatmentPlanManager: updatedGoals[goalIndex] = { ...goal, objectives: [...goal.objectives, newObjective] }
  TreatmentPlanManager->>ReactStateNewPlanData: setNewPlanData(prev => ({ ...prev, goals: updatedGoals }))
  deactivate TreatmentPlanManager
  ReactStateNewPlanData-->>Therapist: UI re-renders with new objective
Loading

Class diagram for TreatmentPlanManager state and data models

classDiagram
  class TreatmentPlanManager {
    - error : string | null
    - isCreateModalOpen : boolean
    - newPlanData : FormNewPlanData
    - planToDelete : TreatmentPlan | null
    - isEditModalOpen : boolean
    - editingPlanData : FormUpdatePlanData | null
    + addObjective(goalIndex number, isEdit boolean) void
    + updateObjectiveField(goalIndex number, objIndex number, field string, value any, isEdit boolean) void
    + removeObjective(goalIndex number, objIndex number, isEdit boolean) void
    + handleSubmitNewPlan() Promise~void~
    + openEditModal(plan TreatmentPlan) void
    + openCreateModal() void
  }

  class FormNewPlanData {
    + clientId : string
    + startDate : string
    + diagnosis : string
    + goals : ClientSideNewGoal[]
  }

  class FormUpdatePlanData {
    + id : string
    + clientId : string
    + startDate : string
    + diagnosis : string
    + goals : EditableGoal[]
  }

  class TreatmentPlan {
    + id : string
    + clientId : string
    + startDate : string
    + diagnosis : string
    + goals : EditableGoal[]
  }

  class ClientSideNewGoal {
    + id : string
    + description : string
    + objectives : ClientSideNewObjective[]
  }

  class EditableGoal {
    + id : string
    + description : string
    + objectives : EditableObjective[]
  }

  class ClientSideNewObjective {
    + description : string
    + targetDate : string
    + status : string
  }

  class EditableObjective {
    + id : string
    + description : string
    + targetDate : string
    + status : string
  }

  TreatmentPlanManager --> FormNewPlanData : uses
  TreatmentPlanManager --> FormUpdatePlanData : uses
  TreatmentPlanManager --> TreatmentPlan : edits
  FormNewPlanData --> ClientSideNewGoal : has
  FormUpdatePlanData --> EditableGoal : has
  TreatmentPlan --> EditableGoal : has
  ClientSideNewGoal --> ClientSideNewObjective : has
  EditableGoal --> EditableObjective : has
Loading

File-Level Changes

Change Details Files
Initialize and reset new plan form state using shallow copies instead of JSON deep clones.
  • Replace JSON.parse/JSON.stringify usage when initializing newPlanData with an object spread of initialNewPlanData and a shallow-cloned goals array.
  • Update post-create reset of newPlanData to use the same shallow-copy initialization pattern.
  • Update openCreateModal to reset newPlanData via shallow copy instead of JSON deep clone.
src/components/therapy/TreatmentPlanManager.tsx
Update objective add/edit/remove helpers to use immutable shallow copies instead of JSON deep clones and in-place mutation.
  • In addObjective, replace JSON-based cloning of goals with array spread, and update a single goal via object spread while immutably appending to its objectives array for both editingPlanData and newPlanData paths.
  • In updateObjectiveField, replace JSON-based cloning with array spread, clone the objectives array, and replace the targeted objective via object spread with the updated field for both editingPlanData and newPlanData paths.
  • In removeObjective, replace JSON-based cloning with array spread, clone the objectives array, splice from the cloned array, and reassign the updated objectives back to the cloned goal for both editingPlanData and newPlanData paths.
  • Add comments documenting the Bolt optimization rationale near the modified objective management logic.
src/components/therapy/TreatmentPlanManager.tsx
Load editable plan data with shallow-copied goals and objectives instead of JSON-based deep cloning.
  • In handleEdit, replace JSON.parse/JSON.stringify around plan.goals with a map that shallow-copies each goal and its objectives array, while still ensuring objectives is always an array.
src/components/therapy/TreatmentPlanManager.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The logic for resetting/cloning newPlanData (in useState initialization, openCreateModal, and after successful create) is duplicated; consider extracting a small helper like createEmptyNewPlanData() so any future shape changes to initialNewPlanData only need to be updated in one place.
  • When mapping plan.goals in openEditModal, you now shallow-copy the objectives array but keep each objective object by reference; if those objects are ever mutated elsewhere this could lead to subtle bugs, so you may want to also spread each objective (objectives: g.objectives ? g.objectives.map(o => ({ ...o })) : []) or at least document the assumption that objectives are treated as immutable.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The logic for resetting/cloning `newPlanData` (in `useState` initialization, `openCreateModal`, and after successful create) is duplicated; consider extracting a small helper like `createEmptyNewPlanData()` so any future shape changes to `initialNewPlanData` only need to be updated in one place.
- When mapping `plan.goals` in `openEditModal`, you now shallow-copy the `objectives` array but keep each objective object by reference; if those objects are ever mutated elsewhere this could lead to subtle bugs, so you may want to also spread each objective (`objectives: g.objectives ? g.objectives.map(o => ({ ...o })) : []`) or at least document the assumption that objectives are treated as immutable.

## Individual Comments

### Comment 1
<location path="src/components/therapy/TreatmentPlanManager.tsx" line_range="310-313" />
<code_context>
-        JSON.stringify(editingPlanData.goals || []),
-      ) as EditableGoal[]
+      const updatedGoals = [...(editingPlanData.goals || [])]
       if (
         updatedGoals[goalIndex] &&
+        updatedGoals[goalIndex].objectives &&
         updatedGoals[goalIndex].objectives[objIndex]
       ) {
-        ;(updatedGoals[goalIndex].objectives[objIndex])[
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Guarding on `objectives` presence may hide inconsistent state without recovery

In `updateObjectiveField`, the new guard `updatedGoals[goalIndex].objectives && updatedGoals[goalIndex].objectives[objIndex]` means that if `objectives` is unexpectedly `undefined`, the function will silently no-op. If `objectives` should always exist in this code path, consider normalizing it to an empty array when constructing/updating goals (as when mapping from `plan.goals`) so this check only needs to validate the index and inconsistent state becomes visible instead of silently ignored.

Suggested implementation:

```typescript
    isEdit = false,
  ) => {
    if (isEdit && editingPlanData) {
      const updatedGoals = (editingPlanData.goals || []).map((goal) => ({
        ...goal,
        objectives: goal.objectives || [],
      }))
      if (updatedGoals[goalIndex]?.objectives[objIndex]) {
        const updatedObjectives = [...updatedGoals[goalIndex].objectives]

```

To fully enforce the invariant that `objectives` is always an array and avoid future silent no-ops, you should also:
1. Normalize `objectives` to `[]` anywhere goals are constructed or updated (e.g., when mapping from `plan.goals` into `editingPlanData` or `newPlanData`).
2. Ensure any goal-creation helpers or initial state definitions set `objectives: []` instead of leaving it `undefined`.
This keeps `updateObjectiveField` simple (only index checks) and ensures inconsistent state is surfaced earlier in the data flow.
</issue_to_address>

### Comment 2
<location path="src/components/therapy/TreatmentPlanManager.tsx" line_range="524-527" />
<code_context>
         ? new Date(plan.startDate).toISOString().split('T')[0]
         : '',
       goals: plan.goals
-        ? JSON.parse(
-            JSON.stringify(
-              plan.goals.map((g) => ({ ...g, objectives: g.objectives || [] })),
-            ),
-          )
-        : [], // Deep copy goals, ensure objectives is array
+        ? plan.goals.map((g) => ({
+            ...g,
+            objectives: g.objectives ? [...g.objectives] : [],
+          }))
+        : [], // Shallow copy goals, ensure objectives is array
</code_context>
<issue_to_address>
**question (bug_risk):** `plan.goals` mapping now uses a shallow copy of objectives; ensure this matches expectations

This used to deep-clone each goal and objective; now only goals and their `objectives` arrays are copied, while the objective objects themselves are shared. That’s a behavior change: if any consumer mutates objective objects in place (outside the controlled update paths that spread them on edit), those mutations will now affect both `plan.goals` and `editingPlanData.goals`. Please confirm no code depends on the previous full detachment.
</issue_to_address>

Fix all in Cursor


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 310 to 313
if (
updatedGoals[goalIndex] &&
updatedGoals[goalIndex].objectives &&
updatedGoals[goalIndex].objectives[objIndex]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Guarding on objectives presence may hide inconsistent state without recovery

In updateObjectiveField, the new guard updatedGoals[goalIndex].objectives && updatedGoals[goalIndex].objectives[objIndex] means that if objectives is unexpectedly undefined, the function will silently no-op. If objectives should always exist in this code path, consider normalizing it to an empty array when constructing/updating goals (as when mapping from plan.goals) so this check only needs to validate the index and inconsistent state becomes visible instead of silently ignored.

Suggested implementation:

    isEdit = false,
  ) => {
    if (isEdit && editingPlanData) {
      const updatedGoals = (editingPlanData.goals || []).map((goal) => ({
        ...goal,
        objectives: goal.objectives || [],
      }))
      if (updatedGoals[goalIndex]?.objectives[objIndex]) {
        const updatedObjectives = [...updatedGoals[goalIndex].objectives]

To fully enforce the invariant that objectives is always an array and avoid future silent no-ops, you should also:

  1. Normalize objectives to [] anywhere goals are constructed or updated (e.g., when mapping from plan.goals into editingPlanData or newPlanData).
  2. Ensure any goal-creation helpers or initial state definitions set objectives: [] instead of leaving it undefined.
    This keeps updateObjectiveField simple (only index checks) and ensures inconsistent state is surfaced earlier in the data flow.

Fix in Cursor

Comment on lines 524 to +527
goals: plan.goals
? JSON.parse(
JSON.stringify(
plan.goals.map((g) => ({ ...g, objectives: g.objectives || [] })),
),
)
: [], // Deep copy goals, ensure objectives is array
? plan.goals.map((g) => ({
...g,
objectives: g.objectives ? [...g.objectives] : [],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (bug_risk): plan.goals mapping now uses a shallow copy of objectives; ensure this matches expectations

This used to deep-clone each goal and objective; now only goals and their objectives arrays are copied, while the objective objects themselves are shared. That’s a behavior change: if any consumer mutates objective objects in place (outside the controlled update paths that spread them on edit), those mutations will now affect both plan.goals and editingPlanData.goals. Please confirm no code depends on the previous full detachment.

Fix in Cursor

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Copy link
Copy Markdown

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching from JSON deep-cloning to shallow copies is a good direction for performance, but several handlers still derive updates from closed-over state while using functional setters, which can drop updates under batching/rapid interactions. Resetting from initialNewPlanData can also yield a stale startDate because it’s computed at module initialization time. Finally, openEditModal now shares objective object references with plan, which can leak mutations back into plans if any in-place edits occur.

Additional notes (2)
  • Maintainability | src/components/therapy/TreatmentPlanManager.tsx:262-262
    These handlers still build updatedGoals from the closed-over editingPlanData/newPlanData values, but then call a functional setter (setEditingPlanData((prev) => ...) / setNewPlanData((prev) => ...)). This mismatch can drop updates under rapid successive interactions because the copied base may be stale.

If you’re going to use functional updates, compute from prev inside the setter and avoid referencing editingPlanData/newPlanData directly in the handler.

  • Maintainability | src/components/therapy/TreatmentPlanManager.tsx:522-530
    openEditModal now only shallow-copies objectives arrays, but not the objective objects themselves. If any code path mutates an objective object in-place (even outside the objective handlers), it can accidentally mutate the underlying plan object (and therefore plans state) because those object references are still shared.
Summary of changes

What changed

  • Replaced repeated JSON.parse(JSON.stringify(...)) deep-cloning with shallow copy + immutable updates for goals/objectives state transitions.
  • Updated objective handlers (addObjective, handleObjectiveChange, removeObjective) to:
    • copy the goals array via [...]
    • replace the edited goal via { ...goal, objectives: [...] }
    • avoid in-place mutation of nested arrays.
  • Updated create/reset flows (openCreateModal, post-create reset) to reinitialize newPlanData via object/array spreads.
  • Updated openEditModal to map goals and ensure objectives is an array, without JSON serialization.

Comment on lines +121 to +124
const [newPlanData, setNewPlanData] = useState<FormNewPlanData>({
...initialNewPlanData,
goals: [...initialNewPlanData.goals],
} as FormNewPlanData)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialNewPlanData is instantiated once at module load time (it contains startDate: new Date()...). Resetting form state by spreading initialNewPlanData means the “default” startDate can become stale (e.g., leaving the tab open overnight, then opening the create modal).

Also, the repeated as FormNewPlanData assertions make it easier to accidentally drift from the real shape without TypeScript helping you.

Suggestion

Introduce a factory function and reuse it for initialization + resets so startDate is always fresh and you can avoid as:

const makeInitialNewPlanData = (): FormNewPlanData => ({
  ...initialNewPlanData,
  startDate: new Date().toISOString().split('T')[0],
  goals: [],
})

const [newPlanData, setNewPlanData] = useState<FormNewPlanData>(makeInitialNewPlanData)

Then replace the reset call sites with setNewPlanData(makeInitialNewPlanData()).

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant