Skip to content

fix: centralize WebContainer boot state in Zustand store#95

Open
dimasd-angga wants to merge 1 commit into
piyushdotcomm:mainfrom
dimasd-angga:fix/issue-47-centralize-webcontainer-boot-state-with-
Open

fix: centralize WebContainer boot state in Zustand store#95
dimasd-angga wants to merge 1 commit into
piyushdotcomm:mainfrom
dimasd-angga:fix/issue-47-centralize-webcontainer-boot-state-with-

Conversation

@dimasd-angga
Copy link
Copy Markdown

@dimasd-angga dimasd-angga commented May 14, 2026

Summary

Moves WebContainer boot state out of module-level singletons in useWebContainer.ts and into a Zustand store. Previously, webContainerInstance and bootPromise lived as file-scope variables in the hook module, meaning state was invisible to other consumers and the boot lifecycle was not globally reactive. The store now owns the full lifecycle, and the hook becomes a thin bridge.

Changes

  • modules/webcontainers/store/webcontainer-store.ts (new): Zustand store with devtools middleware holding instance, status (idle | booting | booted | error), isLoading, isBooted, error, bootWebContainer, and teardown. A module-level bootPromise in the store file acts as the idempotency guard — concurrent bootWebContainer() calls share the same in-flight promise instead of triggering parallel boots.
  • modules/webcontainers/hooks/useWebContainer.ts: Removes module-level webContainerInstance and bootPromise singletons and all local useState for boot state. useEffect now calls bootWebContainer() from the store. destory delegates to teardown(). Public return shape (serverUrl, isLoading, error, instance, writeFileSync, destory) is unchanged so callers in app/playground/[id]/page.tsx and webcontainer-preview.tsx require no edits.

Testing

npm test passes. The WebContainer API requires a browser context so the boot path cannot be exercised in Jest; the existing smoke tests (repo structure checks) continue to pass. Manual verification: boot state is now visible in Redux DevTools under the webcontainer-store slice, and navigating away and back to the playground no longer triggers a second boot attempt.

Notes

serverUrl remains hardcoded to null in the hook return — this matched the pre-refactor behaviour (it was never set) and is out of scope for this issue.

Closes #47

Summary by CodeRabbit

  • Refactor
    • Restructured WebContainer state management through a centralized store, consolidating lifecycle initialization and teardown operations.

Review Change Stack

Multiple consumers (terminal, preview) each managed their own boot
lifecycle, leading to redundant boots and state drift when navigating
between views. Consolidating into a Zustand store with a module-level
promise guard ensures a single boot attempt is shared across all
concurrent callers.
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Walkthrough

This PR refactors WebContainer lifecycle management by introducing a Zustand store that centralizes boot state, instance reference, and async initialization logic with concurrent request deduplication. The useWebContainer hook is updated to consume the store instead of managing module-level singleton and local React state.

Changes

WebContainer State Centralization

Layer / File(s) Summary
Zustand Store Lifecycle Management
modules/webcontainers/store/webcontainer-store.ts
New Zustand store tracks WebContainer status (idle/booting/booted/error), instance reference, and loading flags. bootWebContainer() uses a module-level bootPromise guard to deduplicate concurrent boot attempts. teardown() safely clears the instance and resets state. Devtools integration added for state inspection.
useWebContainer Hook Integration
modules/webcontainers/hooks/useWebContainer.ts
Hook refactored to consume instance, isLoading, error, and lifecycle methods from the store. Module-level singleton removed. Mount effect triggers single bootWebContainer() call. writeFileSync and destory callback now operate on store-owned instance and teardown method. Return type updated to wire instance directly from store.

Sequence Diagram

sequenceDiagram
  participant Component
  participant useWebContainer
  participant useWebContainerStore
  participant WebContainer

  Component->>useWebContainer: mount
  useWebContainer->>useWebContainerStore: bootWebContainer()
  alt Already booted
    useWebContainerStore->>useWebContainerStore: return early
  else Booting or first call
    useWebContainerStore->>WebContainer: WebContainer.boot()
    WebContainer-->>useWebContainerStore: instance ready
    useWebContainerStore->>useWebContainerStore: set status=booted, instance
  end
  useWebContainerStore-->>useWebContainer: return {instance, isLoading, error}
  useWebContainer-->>Component: return hook result

  Component->>useWebContainer: writeFileSync(path, content)
  useWebContainer->>WebContainer: instance.fs.mkdir(parent, {recursive})
  useWebContainer->>WebContainer: instance.fs.writeFile(path, content)

  Component->>useWebContainer: cleanup / destroy
  useWebContainer->>useWebContainerStore: teardown()
  useWebContainerStore->>WebContainer: instance.teardown()
  useWebContainerStore->>useWebContainerStore: reset to idle
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • piyushdotcomm/Editron#73: Performs the same core refactor—centralizing WebContainer lifecycle into a Zustand store and rewiring the useWebContainer hook to boot via the store, use the store-owned instance, and tear down via store methods.

Suggested reviewers

  • piyushdotcomm

Poem

🐰 Hops of joy for centralized state!
The WebContainer boots without debate,
Zustand keeps concurrency neat,
No race conditions, the sync's complete!
One source of truth makes the code sweet! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely describes the main change: centralizing WebContainer boot state into a Zustand store, which aligns with the primary objective of the refactoring.
Description check ✅ Passed Description covers summary of changes, specific files modified, testing approach, and links to issue #47. However, it lacks explicit type of change checkboxes and manual verification details from template.
Linked Issues check ✅ Passed The PR successfully implements all requirements from issue #47: creates useWebContainerStore in webcontainer-store.ts, migrates boot state (isLoading, isBooted, instance, error), refactors hook as bridge, ensures idempotent boot with module-level promise guard, and maintains unchanged public API.
Out of Scope Changes check ✅ Passed All changes are directly scoped to centralizing WebContainer boot state management. No unrelated modifications; serverUrl hardcoded to null is explicitly noted as pre-existing behavior and out of scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/issue-47-centralize-webcontainer-boot-state-with-

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/webcontainers/store/webcontainer-store.ts`:
- Around line 43-69: The boot completion can overwrite store after teardown
because the shared bootPromise may resolve later; add a generation token (e.g.,
bootGeneration number stored in the module/closure or in the store) that you
increment when starting a new boot and again in teardown, capture its value in
the WebContainer.boot() then/catch handlers and only call set(...) or mutate
bootPromise when the captured generation still matches the current generation;
update teardown to increment/reset the generation and clear bootPromise so any
in-flight resolution is ignored, and ensure boot start checks the current
generation to avoid launching parallel boots.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9cf67272-f248-4321-9473-a2bd3c106357

📥 Commits

Reviewing files that changed from the base of the PR and between fbafa14 and 8538ccf.

📒 Files selected for processing (2)
  • modules/webcontainers/hooks/useWebContainer.ts
  • modules/webcontainers/store/webcontainer-store.ts

Comment on lines +43 to +69
bootPromise = WebContainer.boot()
.then((instance) => {
set({ instance, status: "booted", isLoading: false, isBooted: true });
})
.catch((err) => {
bootPromise = null;
set({
status: "error",
isLoading: false,
error: err instanceof Error ? err.message : "Failed to initialize WebContainer",
});
});

await bootPromise;
},

teardown: () => {
const { instance } = get();
if (instance) {
try {
instance.teardown();
} catch {
// ignore teardown errors
}
}
bootPromise = null;
set({ instance: null, status: "idle", isLoading: false, isBooted: false, error: null });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent an in-flight boot from reviving the store after teardown.

If teardown() runs before WebContainer.boot() settles, Line 68 clears the shared promise and Line 69 resets the store, but the original boot can still resolve and Line 45 will flip the store back to booted. That also opens a window for a second bootWebContainer() call to start a parallel boot while the first one is still finishing. Please guard the boot completion with a generation/cancellation token and only commit the result if that boot attempt is still current.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/webcontainers/store/webcontainer-store.ts` around lines 43 - 69, The
boot completion can overwrite store after teardown because the shared
bootPromise may resolve later; add a generation token (e.g., bootGeneration
number stored in the module/closure or in the store) that you increment when
starting a new boot and again in teardown, capture its value in the
WebContainer.boot() then/catch handlers and only call set(...) or mutate
bootPromise when the captured generation still matches the current generation;
update teardown to increment/reset the generation and clear bootPromise so any
in-flight resolution is ignored, and ensure boot start checks the current
generation to avoid launching parallel boots.

Copy link
Copy Markdown
Owner

@piyushdotcomm piyushdotcomm left a comment

Choose a reason for hiding this comment

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

you need to check coderabbit and other reviewers comment before directly tagging maintainers

@dimasd-angga
Copy link
Copy Markdown
Author

i will fix the code

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.

Centralize WebContainer Boot State with Zustand

2 participants