Python state, React UI. One runtime for notebook apps and addon backends.
Remote State is a Python-first framework for building stateful React frontends. It lets you define application state, actions, and queries in Python, then render the UI in React/TypeScript over a WebSocket bridge.
The library is designed around two primary use cases:
- React frontends for Python code - especially notebook-driven UIs, where a Jupyter cell or Python script owns the state and the browser only renders the interface.
- Addon and plugin backends for frontend apps - where a frontend addon can ship a TS/React UI and, optionally, a Python backend that provides server-side state, actions, and queries.
In both cases, Python is the source of truth for business state and behavior. React handles presentation, interaction, and reactivity on the browser side.
- Python-owned application state - store nested state in a
Storeand mutate it through actions. - Read and write separation - use
@actionfor mutations and@queryfor read-only calls. - Reactive bridge caching - the frontend fetches values lazily and re-renders when state changes.
- Progress updates - long-running actions and queries can emit progress events to the UI.
- Notebook rendering - show the UI inline in Jupyter or open it in a browser.
- Addon-friendly architecture - bundle a React UI and an optional Python backend behind one API surface.
- Typed TypeScript bridge - consume the backend from React with
createRemoteState,RemoteStateProvider, and hooks.
Remote State splits responsibilities cleanly:
- Python owns state, domain logic, actions, queries, and progress reporting.
- TypeScript/React owns rendering, local interaction, and typed bridge access.
- WebSocket transport connects both sides and carries state reads, invalidations, and task updates.
That makes the package useful both as a notebook UI runtime and as a backend layer for a frontend addon system.
import remotestate as rs
store = rs.Store(
{
"count": 0,
"user": {"name": "forman"},
}
)
class MyService(rs.Service):
@rs.action
async def increment(self):
self.store.set("count", self.store.get("count") + 1)
@rs.query
async def compute(self, x: float) -> float:
self.progress(name="Computing...", progress=50)
return x * self.store.get("count")
rs.serve(MyService(store), dist_dir="my-ui/dist")// MyService.ts - typed contract for the Python service
export interface MyService {
increment(): Promise<void>;
compute(x: number): Promise<number>;
}import { RemoteStateProvider, useRemoteStateClient, useState } from "remotestate";
import type { MyService } from "./MyService";
function AppInner() {
const remoteState = useRemoteStateClient<MyService>();
const [count, setCount] = useRemoteState<number>("count", 0);
const [name] = useRemoteState<string>("user.name");
return (
<div>
<p>Hello, {name ?? "..."}! Count: {count ?? "..."}</p>
<button onClick={() => void setCount((n) => (n ?? 0) + 1)}>+1</button>
<button
onClick={async () => {
const result = await remoteState.query("compute", [5.0]);
console.log(result);
}}
>
Compute
</button>
</div>
);
}
export default function App() {
return (
<RemoteStateProvider url="ws://localhost:9753/ws">
<AppInner />
</RemoteStateProvider>
);
}my-notebook-project/
app.ipynb
service.py
ui/
src/
App.tsx
MyService.ts
dist/
Use this shape when the notebook or a Python script is the main entry point and the browser is just the renderer.
my-addon/
frontend/
src/
App.tsx
addon.ts
backend/
service.py
dist/
Use this shape when a frontend app exposes an addon API and the addon optionally ships a Python backend for stateful behavior.
pip install remotestateOr with pixi:
pixi add remotestatenpm install remotestateHolds application state. Supports nested dicts, lists, Pydantic models, and dataclasses.
store = rs.Store({"items": [], "user": UserModel(name="Norman")})
store.get("user.name") # "Norman"
store.set("items[0].label", "foo")Paths use a JSONPath-inspired syntax such as user.name or items[3].label.
Declares a method that mutates the store. All store.set() calls are batched
and sent as one invalidation after the handler finishes.
Declares a read-only method that returns a value. Store mutations are forbidden inside queries.
Reports progress of the current action or query to the frontend.
@rs.query
async def process(self, path: str) -> dict:
self.progress(name="Loading data", progress=10)
# ... do work ...
self.progress(name="Processing", progress=80)
return resultStarts the Remote State server and connects it to a frontend bundle.
| Parameter | Default | Description |
|---|---|---|
service |
required | A Service instance |
dist_dir |
None |
Path to the React build output (dist/) |
host |
"localhost" |
Server host |
port |
9753 |
Server port |
open_browser |
auto | Open in browser, default outside Jupyter |
open_iframe |
auto | Render as IFrame, default in Jupyter |
iframe_height |
600 |
IFrame height in pixels |
Re-running the same Jupyter cell restarts the server automatically.
Creates a typed Remote State bridge.
const remoteState = createRemoteState<MyService>("ws://localhost:9753/ws");React context wrapper for a Remote State bridge bound to a WebSocket URL, plus a hook to access it.
<RemoteStateProvider url="ws://localhost:9753/ws">
<App />
</RemoteStateProvider>
const remoteState = useRemoteStateClient<MyService>();React-like state hook backed by the Python store. It returns [value, setValue].
const [count, setCount] = useRemoteState<number>("count", 0);
await setCount((prev) => (prev ?? 0) + 1);Calls a Python @action. Fire-and-forget by default.
await remoteState.action("increment");
await remoteState.action("set_name", ["Norman"]);
await remoteState.action("save", [], {}, { awaitInvalidate: true });Calls a Python @query and returns the result.
const result = await remoteState.query("compute", [5.0]);Low-level read hook for store values. Returns undefined while loading and
re-renders on invalidation.
RemoteStateis the bridge object that used to be calledClient.useRemoteStateClient()returns that bridge object.useRemoteStore()returns the cached store view used by the hooks.useState()remains the ergonomic path-bound state hook for components.- We use
useRemoteStateClient()instead ofuseRemoteState()to keep it clearly distinct fromuseState()anduseRemoteStore().
- Python >= 3.11
- Node.js >= 18
- pixi recommended, or pip + venv
git clone https://github.com/your-username/remotestate
cd remotestate
# Python
cd remotestate-py
pixi install
pixi run pytest
# TypeScript
cd ../remotestate-ts
npm install
npm run tests
npm run checks# Terminal 1 - Python server
cd examples/basic
pixi run python server.py
# Terminal 2 - Vite dev server
cd examples/basic/ui
npm run devremotestate generate my_service.py --out ui/src/MyService.tsPython (source of truth) TypeScript / React (renderer)
────────────────────────────── ──────────────────────────────
Store StoreImpl (cache)
state lazy fetch per path
actions + queries ──► invalidate -> re-render
progress events ──► task updates
Service
@action -> mutate state ──► remoteState.action()
@query -> read state/result ──► remoteState.query()
WebSocket transport
ws://localhost:9753/ws
Protocol messages (WebSocket, JSON):
| Direction | Type | Purpose |
|---|---|---|
| JS -> PY | get |
Fetch a single store value |
| JS -> PY | action |
Call a @action method |
| JS -> PY | query |
Call a @query method |
| PY -> JS | get_result |
Response to get |
| PY -> JS | invalidate |
Batch store update |
| PY -> JS | query_result |
Response to query |
| PY -> JS | task_update |
Progress from self.progress() |
| PY -> JS | error |
Any error |
Contributions are very welcome. Please open an issue first to discuss larger changes.
# Run all tests
cd remotestate-py && pixi run pytest
cd remotestate-ts && npm run tests
# Lint
cd remotestate-py && pixi run ruff check src
cd remotestate-ts && npm run checksPlease follow the existing code style: ruff + black on the Python side, ESLint and TypeScript strict mode on the JavaScript side.
MIT © Norman Fomferra