Observable Store

createStore()

createStore produces a minimal observable store. It needs no external library.

import { createStore } from '@objectifthunes/limestone-sdk';

const store = createStore({ count: 0, user: null as string | null });

The generic type is inferred from initialState. The state shape is fixed at creation time.

Reading and writing state

store.get()

Returns the current snapshot synchronously.

const state = store.get();
console.log(state.count); // 0

store.set(newState)

Merges partial state. Listeners are notified only when the reference changes.

store.set({ count: 1 });
// state is now { count: 1, user: null }

store.set(updater)

Accepts a function for updates that depend on the previous state.

store.set((prev) => ({ ...prev, count: prev.count + 1 }));

Subscribing to changes

store.subscribe registers a listener that fires after every state change. It returns an unsubscribe function.

const unsubscribe = store.subscribe(() => {
  console.log('State changed:', store.get());
});

store.set({ count: 2 }); // listener fires

unsubscribe(); // stop listening
store.set({ count: 3 }); // listener does NOT fire

The listener receives no arguments. Call store.get() inside it to read the new state.

Selector-based subscriptions

Subscribe with a derived value to avoid re-renders from unrelated changes.

function subscribeToSelector<T, S>(
  store: Store<T>,
  selector: (state: T) => S,
  listener: (value: S) => void,
): () => void {
  let prev = selector(store.get());
  return store.subscribe(() => {
    const next = selector(store.get());
    if (next !== prev) {
      prev = next;
      listener(next);
    }
  });
}

// Only fires when `count` changes, not when `user` changes
const unsub = subscribeToSelector(store, (s) => s.count, (count) => {
  console.log('count is now', count);
});

useStore() hook

useStore binds a component to the store. It requires LimestoneProvider in the tree.

Full snapshot

import { useStore } from '@objectifthunes/limestone-sdk';

function Counter() {
  const state = useStore();
  return <Text>{state.count}</Text>;
}

The component re-renders on every state change.

Selector — granular re-renders

Pass a selector to subscribe only to a slice of state.

function Counter() {
  const count = useStore((s) => s.count);
  return <Text>{count}</Text>;
}

The component re-renders only when the selector result changes by strict equality (===). For objects or arrays, return a stable reference or use a primitive.

// Re-renders whenever any auth field changes
const authStatus = useStore((s) => s.auth.status);

// Re-renders only when the access token string changes
const token = useStore((s) => s.auth.tokens?.accessToken ?? null);

AppState shape

LimestoneProvider creates a Store<AppState>. The shape is:

interface AppState {
  auth: AuthState;
  network: NetworkState;
  mutations: QueuedMutation[];
}

interface AuthState {
  status: 'idle' | 'authenticating' | 'authenticated' | 'refreshing' | 'error';
  user: Record<string, unknown> | null;
  tokens: { accessToken: string; refreshToken: string } | null;
}

interface NetworkState {
  connected: boolean;
  type: 'wifi' | 'cellular' | 'none' | 'unknown';
}

Persistence with SecureStorageProvider

The store itself has no built-in persistence. Wire it manually using a SecureStorageProvider:

import { createStore } from '@objectifthunes/limestone-sdk';
import { createExpoSecureStore } from '@objectifthunes/limestone-sdk/expo-secure-store';

const storage = createExpoSecureStore();
const store = createStore({ theme: 'light', onboardingComplete: false });

// Rehydrate on boot
const saved = await storage.get('app-state');
if (saved) {
  store.set(JSON.parse(saved));
}

// Persist on every change
store.subscribe(() => {
  storage.set('app-state', JSON.stringify(store.get()));
});

Direct store access outside React

The store is available from useLimestone().store inside the component tree, or you can hold a reference to the store you created.

import { useLimestone } from '@objectifthunes/limestone-sdk';

function DebugPanel() {
  const { store } = useLimestone();
  const snapshot = store.get();
  // ...
}

Outside React — for example in a background task — hold the store reference directly:

// store created during bootstrap
export const appStore = createStore({ /* ... */ });

// in a background task
const { auth } = appStore.get();
if (!auth.tokens) {
  // redirect to login
}