# Accessing State Outside React Components with Zustand in Pascal Editor

> Learn how to access Zustand state outside React components in Pascal Editor. Use `useScene.getState()` to read or mutate scene state from any JavaScript module.

- Repository: [Pascal/editor](https://github.com/pascalorg/editor)
- Tags: how-to-guide
- Published: 2026-03-25

---

**Pascal Editor exports its Zustand store as a singleton from [`packages/core/src/store/use-scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/store/use-scene.ts), enabling any JavaScript module to read or mutate scene state via `useScene.getState()` without using React hooks.**

Pascal Editor manages its entire scene graph—nodes, collections, and dirty-state tracking—through a central Zustand store enhanced with the **zundo** middleware for undo/redo capabilities. Because the store is instantiated at the module level and exported as a plain object rather than wrapped in a React Context provider, it remains accessible to utility functions, keyboard shortcuts, background workers, and auto-save logic that operate outside the component tree.

## How the Scene Store Enables Universal Access

### Zustand Configuration with Temporal Middleware

In [`packages/core/src/store/use-scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/store/use-scene.ts), the store is created using `create<SceneState>()` wrapped with `temporal()` from the zundo library. This setup captures every state mutation while limiting the history stack to 50 entries via the `limit: 50` configuration (lines 92-98). The store also implements `partialize` to control which state slices are tracked for undo purposes.

Lines 15-73 implement a subscription via `useScene.temporal.subscribe()` that performs diff-detection specifically when the user triggers undo or redo operations, ensuring the UI reflects historical states correctly even when accessed through imperative calls.

### Module-Level Singleton Export

Lines 300-303 export the store instance as `export default useScene`. This pattern creates a singleton that any file in the monorepo can import. Unlike React Context, which requires a `<Provider>` tree, this Zustand store lives at the module level and initializes immediately upon import, eliminating side-effects that depend on the React component lifecycle.

## Reading and Mutating State Outside Components

### Retrieving Snapshots with getState()

Any non-React module can import the store and call **`useScene.getState()`** to retrieve the current `SceneState` snapshot. This method returns the full state object containing `nodes`, `rootNodeIds`, and the dirty-set, enabling read-only operations without triggering re-renders.

### Performing Mutations via Store Actions

Rather than exposing raw `setState` access, Pascal Editor provides typed action methods directly on the store object, including **`createNode`**, **`updateNode`**, and **`deleteNode`**. These actions are defined in the store configuration and ensure all mutations pass through the zundo temporal tracking system, maintaining undo/redo consistency regardless of whether the caller is a React component or a utility function.

## Practical Implementation Examples

### Querying Nodes from Utility Files

The following example from [`packages/editor/src/lib/scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/lib/scene.ts) (lines 1-4) demonstrates filtering wall nodes without React hooks:

```typescript
// packages/editor/src/lib/scene.ts
import useScene from '@pascal-app/core/src/store/use-scene'

export function getAllWallNodes() {
  const { nodes } = useScene.getState()
  return Object.values(nodes).filter(node => node.type === 'wall')
}

```

### Handling Keyboard Shortcuts

In [`packages/editor/src/hooks/use-keyboard.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/hooks/use-keyboard.ts) (lines 98-110), keyboard events access both the scene store and selection state to rotate objects:

```typescript
// packages/editor/src/hooks/use-keyboard.ts
import useScene from '@pascal-app/core/src/store/use-scene'

export function rotateSelectedNode(deltaY: number) {
  const { selectedNodeIds } = useEditor.getState()
  const nodeId = selectedNodeIds[0]
  if (!nodeId) return

  const node = useScene.getState().nodes[nodeId]
  const newRotation = (node.rotation ?? 0) + deltaY
  useScene.getState().updateNode(nodeId, { rotation: newRotation })
}

```

### Managing Draft Nodes in Non-React Services

This pattern from [`packages/editor/src/components/tools/item/use-draft-node.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/components/tools/item/use-draft-node.ts) (lines 74-78) creates nodes from pure logic functions:

```typescript
// packages/editor/src/components/tools/item/use-draft-node.ts
import useScene from '@pascal-app/core/src/store/use-scene'
import { ItemNode } from '@pascal-app/core/src/schema/nodes/item'

export function draftNewItem(currentLevelId: string) {
  const draft = ItemNode.parse({ /* default props */ })
  useScene.getState().createNode(draft, currentLevelId)
  return draft.id
}

```

### Implementing Auto-Save Logic

The auto-save hook in [`packages/editor/src/hooks/use-auto-save.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/hooks/use-auto-save.ts) (lines 65-75) serializes state without React subscription overhead:

```typescript
// packages/editor/src/hooks/use-auto-save.ts
import useScene from '@pascal-app/core/src/store/use-scene'

export function maybeSaveScene() {
  const { nodes, rootNodeIds } = useScene.getState()
  const payload = JSON.stringify({ nodes, rootNodeIds })
  // …send to backend or local storage
}

```

## Architectural Safety Guarantees

Using `useScene` outside React components provides three critical guarantees:

*   **Single source of truth** – All application segments read from and write to the same Zustand instance ([`packages/core/src/store/use-scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/store/use-scene.ts)), ensuring consistency across the editor.
*   **Undo/redo integration** – The `temporal` wrapper tracks every mutation performed via store actions, so imperative callers participate fully in the undo stack alongside React components.
*   **No context leakage** – The store does not rely on a `<Provider>` pattern, preventing coupling between state access and the React tree structure. This enables safe imports in web workers or Node.js scripts if needed.

## Summary

*   Pascal Editor’s scene state lives in a Zustand store at [`packages/core/src/store/use-scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/store/use-scene.ts), wrapped with zundo for undo/redo support.
*   The store is exported as a singleton (`export default useScene`), making it importable anywhere in the codebase.
*   Call **`useScene.getState()`** to read the current snapshot and access actions like `createNode` or `updateNode` for mutations.
*   State changes made outside React still propagate through the temporal middleware, preserving history and consistency.
*   This pattern eliminates the need for React Context or hooks in utilities, keyboard handlers, and background services.

## Frequently Asked Questions

### Can I use useScene.getState() inside a React component?

Yes, though components typically use the **`useScene()`** hook (the default export called as a hook) to subscribe to state changes and trigger re-renders. However, calling `useScene.getState()` inside event handlers or effects is valid when you need a one-time snapshot without establishing a reactive subscription.

### Does mutating state outside React break the undo/redo history?

No. Because Pascal Editor wraps the store with zundo’s `temporal()` middleware at initialization ([`packages/core/src/store/use-scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/store/use-scene.ts) lines 92-98), all mutations—whether from React event handlers or external utility functions—pass through the same tracking mechanism and append to the unified undo stack.

### How do I access the store if I’m not in a React file?

Simply import the default export from the core package: `import useScene from '@pascal-app/core/src/store/use-scene'`. Since the store is a module singleton with no React dependencies, it initializes correctly in plain TypeScript files, test suites, or service workers without a JSX runtime.

### What is the difference between getState() and the useScene hook?

**`useScene.getState()`** returns the current state object imperatively and does not subscribe to future changes, making it ideal for event handlers and utility functions. The **`useScene()`** hook (calling the same export as a React hook) establishes a subscription that causes components to re-render when tracked state slices change.