Shared Notepad

A notepad where text is synchronized across all framework implementations. Type in the React version and see it update in Vue, Svelte, Solid, and Angular — all through a single valdres store.

This example demonstrates the core value proposition: your state is framework-agnostic. The same atoms power every framework.

Shared state

import { atom, selector, store } from "valdres"

export const noteAtom = atom("")
export const charCountSelector = selector(get => get(noteAtom).length)
export const wordCountSelector = selector(get => {
    const text = get(noteAtom).trim()
    return text ? text.split(/\s+/).length : 0
})

// One store shared across all frameworks
export const appStore = store()

One store, many frameworks
The appStore instance is imported by every framework. When React updates noteAtom, Vue and Svelte subscribers are notified immediately — there's no bridge, adapter, or event bus.

React

import { Provider, useAtom, useValue } from "valdres-react"
import { noteAtom, charCountSelector, wordCountSelector, appStore } from "./state"

function Notepad() {
    const [note, setNote] = useAtom(noteAtom)
    const chars = useValue(charCountSelector)
    const words = useValue(wordCountSelector)

    return (
        <div>
            <textarea
                value={note}
                onChange={e => setNote(e.target.value)}
                placeholder="Start typing..."
                rows={6}
            />
            <div>
                {chars} characters · {words} words
            </div>
        </div>
    )
}

// Mount with the shared store
function App() {
    return (
        <Provider store={appStore}>
            <Notepad />
        </Provider>
    )
}

Vue

<script setup>
import { useAtom, useValue } from "valdres-vue"
import { noteAtom, charCountSelector, wordCountSelector } from "./state"

const note = useAtom(noteAtom)
const chars = useValue(charCountSelector)
const words = useValue(wordCountSelector)
</script>

<template>
  <div>
    <textarea
      v-model="note"
      placeholder="Start typing..."
      rows="6"
    />
    <div>
      {{ chars }} characters · {{ words }} words
    </div>
  </div>
</template>

Svelte

<script>
  import { watch, readable } from "valdres-svelte"
  import { noteAtom, charCountSelector, wordCountSelector } from "./state"

  const note = watch(noteAtom)
  const chars = readable(charCountSelector)
  const words = readable(wordCountSelector)
</script>

<div>
  <textarea
    value={note.value}
    oninput={e => note.set(e.target.value)}
    placeholder="Start typing..."
    rows="6"
  />
  <div>
    {chars.value} characters · {words.value} words
  </div>
</div>

Solid

import { createAtom, createValue } from "valdres-solid"
import { ValdresProvider } from "valdres-solid"
import { noteAtom, charCountSelector, wordCountSelector, appStore } from "./state"

function Notepad() {
    const [note, setNote] = createAtom(noteAtom)
    const chars = createValue(charCountSelector)
    const words = createValue(wordCountSelector)

    return (
        <div>
            <textarea
                value={note()}
                onInput={e => setNote(e.target.value)}
                placeholder="Start typing..."
                rows="6"
            />
            <div>
                {chars()} characters · {words()} words
            </div>
        </div>
    )
}

function App() {
    return (
        <ValdresProvider store={appStore}>
            <Notepad />
        </ValdresProvider>
    )
}

Angular

import { Component } from "@angular/core"
import { injectAtom, injectValue } from "valdres-angular"
import { noteAtom, charCountSelector, wordCountSelector } from "./state"

@Component({
    template: `
        <div>
            <textarea
                [value]="note()"
                (input)="note.set($event.target.value)"
                placeholder="Start typing..."
                rows="6"
            ></textarea>
            <div>
                {{ chars() }} characters · {{ words() }} words
            </div>
        </div>
    `,
})
export class Notepad {
    note = injectAtom(noteAtom)
    chars = injectValue(charCountSelector)
    words = injectValue(wordCountSelector)
}

The cross-framework demo

Here's how you'd mount all five in the same page, sharing a single store:

// main.ts
import { appStore, noteAtom } from "./state"

// React
import { createRoot } from "react-dom/client"
import { Provider } from "valdres-react"
createRoot(document.getElementById("react-notepad")).render(
    <Provider store={appStore}><ReactNotepad /></Provider>
)

// Vue
import { createApp } from "vue"
import { createValdres } from "valdres-vue"
const vueApp = createApp(VueNotepad)
vueApp.use(createValdres({ store: appStore }))
vueApp.mount("#vue-notepad")

// Svelte
import { mount } from "svelte"
import SvelteNotepad from "./SvelteNotepad.svelte"
mount(SvelteNotepad, { target: document.getElementById("svelte-notepad") })

// All three textareas now stay in sync through appStore

Type in any one — the others update instantly.

Key takeaways

  • Framework agnostic — the state layer doesn't know or care which framework reads it.
  • No bridges needed — React, Vue, and Svelte all subscribe to the same store directly. There's no event bus, postMessage, or custom synchronization.
  • Derived state works everywhere — the word and character counts are selectors that every framework can use.
  • Micro-frontend friendly — this pattern works for micro-frontends where different teams use different frameworks but share state.