# Syncing editable interactables
URL: /concepts/components/syncing-editable-interactables
When you want a user-editable component to stay aligned with the props that Tambo controls, you only need a thin wrapper: keep the edited props in state, pass them straight into the interactable, and adopt any remote changes with a `useEffect`.
## Why props fall out of sync
* The component stores user edits in local state, so Tambo never sees the new props.
* Tambo updates the registry, but the component keeps rendering the stale value.
Fix both directions by treating the wrapper state as the single source of truth.
## Step by step
### 1. Presentational component
```tsx
type NoteProps = {
title: string;
content: string;
color?: "white" | "yellow" | "blue" | "green";
};
function Note({ title, content, color = "yellow" }: NoteProps) {
return (
);
}
```
* No state, just props. Tambo controls whatever you pass in.
### 2. Register it with `withInteractable`
```tsx
import { withInteractable } from "@tambo-ai/react";
import { z } from "zod";
const InteractableNote = withInteractable(Note, {
componentName: "Note",
description: "A note that both users and Tambo can edit",
propsSchema: z.object({
title: z.string(),
content: z.string(),
color: z.enum(["white", "yellow", "blue", "green"]).optional(),
}),
});
```
* The schema tells Tambo exactly which props it can update.
### 3. Wrap it with local state + a sync effect
```tsx
import { useEffect, useState } from "react";
import { useCurrentInteractablesSnapshot } from "@tambo-ai/react";
function EditableNote(initial: NoteProps) {
const [note, setNote] = useState(initial);
const [interactableId, setInteractableId] = useState(null);
const snapshot = useCurrentInteractablesSnapshot();
// Pull in any updates that Tambo makes to this interactable.
useEffect(() => {
if (!interactableId) return;
const match = snapshot.find((item) => item.id === interactableId);
if (!match) return;
const next = match.props as Partial;
setNote((prev) => ({ ...prev, ...next }));
}, [snapshot, interactableId]);
return (
);
}
```
* `useState` keeps the canonical props that both the UI and Tambo share.
* Updating state immediately rerenders the interactable with the new props, so the registry stays current.
* The `useEffect` mirrors any Tambo-driven updates back into the textarea.
### 4. Use it anywhere
```tsx
export function Page() {
return (
);
}
```
## Full example
Show complete code
```tsx
import { useEffect, useState } from "react";
import {
useCurrentInteractablesSnapshot,
withInteractable,
} from "@tambo-ai/react";
import { z } from "zod";
type NoteProps = {
title: string;
content: string;
color?: "white" | "yellow" | "blue" | "green";
};
function Note({ title, content, color = "yellow" }: NoteProps) {
return (
);
}
const InteractableNote = withInteractable(Note, {
componentName: "Note",
description: "A note that both users and Tambo can edit",
propsSchema: z.object({
title: z.string(),
content: z.string(),
color: z.enum(["white", "yellow", "blue", "green"]).optional(),
}),
});
function EditableNote(initial: NoteProps) {
const [note, setNote] = useState(initial);
const [interactableId, setInteractableId] = useState(null);
const snapshot = useCurrentInteractablesSnapshot();
useEffect(() => {
if (!interactableId) return;
const match = snapshot.find((item) => item.id === interactableId);
if (!match) return;
const next = match.props as Partial;
setNote((prev) => ({ ...prev, ...next }));
}, [snapshot, interactableId]);
return (
);
}
export function Page() {
return (
);
}
```
## Quick checklist
* Update the wrapper state whenever the user edits; the interactable sees the new props on the same render.
* Use `useEffect` to adopt registry updates so the UI reflects Tambo’s changes.
* Only pass serializable props into the interactable—avoid callbacks or refs.