Persist the count

Persist the count

The counter keeps its value between launches by using the native key/value store rather than localStorage. It is per app, it survives reinstalling the frontend, and it works identically on desktop and mobile.

Declare what the app uses in peko.toml, and give the window a size:

[ui]
framework = "static"
width = 420
height = 340

[capabilities]
uses = ["storage", "menu"]

Calling native code

Every handler the native side registers under namespace.method is reachable as peko.<namespace>.<method>(params). Name the methods you use once, and the calls read like ordinary async functions:

import { peko } from '@peko/client/react'

const storage = peko.storage as {
  get(params: { key: string }): Promise<unknown>
  set(params: { key: string; value: unknown }): Promise<unknown>
}

await storage.set({ key: 'count', value: 7 })
const stored = await storage.get({ key: 'count' })

Values are stored as JSON, so a number comes back a number. A key that was never set returns null. The handlers are registered for you by pekoui; there is no native code to write for this.

peko.invoke('storage.get', { key }) is the same call written out longhand, and is what you would reach for with a dynamic method name.

The component

import { useEffect, useState } from 'react'
import { peko } from '@peko/client/react'

const STORAGE_KEY = 'count'

const storage = peko.storage as {
  get(params: { key: string }): Promise<unknown>
  set(params: { key: string; value: unknown }): Promise<unknown>
}

export default function App() {
  // null until the stored value has been read. Without that distinction the
  // first render looks like a real 0 and the save effect below writes it over
  // the count that was persisted last run.
  const [count, setCount] = useState<number | null>(null)

  useEffect(() => {
    let active = true
    void (async () => {
      await peko.ready
      const stored = await storage.get({ key: STORAGE_KEY })
      if (active) {
        setCount(typeof stored === 'number' ? stored : 0)
      }
    })()
    return () => {
      active = false
    }
  }, [])

  useEffect(() => {
    if (count === null) return
    void storage.set({ key: STORAGE_KEY, value: count })
  }, [count])

  return (
    <main className="counter">
      <output id="count" className="count">{count ?? ''}</output>
      <div className="actions">
        <button id="decrement" onClick={() => setCount((v) => (v ?? 0) - 1)}>Decrement</button>
        <button id="reset" onClick={() => setCount(0)}>Reset</button>
        <button id="increment" onClick={() => setCount((v) => (v ?? 0) + 1)}>Increment</button>
      </div>
    </main>
  )
}

Two details are worth copying rather than rediscovering:

await peko.ready before the first call. The bridge authenticates on boot, and a call made before that resolves is a call made to nothing.

The null starting state. A useState(0) here would save 0 over the stored count on the very first render, every launch. The load-then-save pair needs a value that means the count has not been read yet.

The ids on the elements are not decoration. The store-asset scripts later in this guide target them, so they are part of the app's contract.

Run the app, press Increment a few times, quit, and reopen it. The count is still there.