Skip to content

1. Start with simple state ​

StateAdapt stores can be as simple as createSignal or RxJS BehaviorSubjects:

tsx
function SimpleState() {
  const nameStore = adapt('Bob');
  const name = fromAdapt(nameStore);
  return (
    <>
      <h2>Hello {name.state()}!</h2>
      <button onClick={() => nameStore.set('Bilbo')}>Change Name</button>
    </>
  );
}

StackBlitz

2. Add selectors for derived state ​

tsx
function DerivedState() {
  const nameStore = adapt('Bob', {
    selectors: {
      yelledName: name => name.toUpperCase(), // Will be memoized
    },
  });
  const name = fromAdapt(nameStore);
  return (
    <>
      <h2>Hello {name.state()}!</h2>
      <h2>Hello {name.yelledName()}!</h2>
      <button onClick={() => nameStore.set('Bilbo')}>Change Name</button>
    </>
  );
}

StackBlitz

3. Define state changes declaratively in stores ​

Maintain separation of concerns by keeping state logic together instead of scattered.

tsx
function StateChanges() {
  const nameStore = adapt('Bob', {
    reverseName: (name) => name.split('').reverse().join(''), 
    selectors: {
      yelledName: (name) => name.toUpperCase(), // Will be memoized
    },
  });
  const name = fromAdapt(nameStore);
  return (
    <>
      <h2>Hello {name.yelledName()}!</h2>
      <button onClick={() => nameStore.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore.reverseName()}>Reverse Name</button>
    </>
  );
}

StackBlitz

4. Reuse state patterns with state adapters ​

tsx
const nameAdapter = createAdapter<string>()({ 
  reverseName: (name) => name.split('').reverse().join(''),
  selectors: {
    yelledName: (name) => name.toUpperCase(), // Will be memoized
  },
});

function StateAdapters() {
  const nameStore1 = adapt('Bob', nameAdapter);
  const name1 = fromAdapt(nameStore1);

  const nameStore2 = adapt('Bob', nameAdapter);
  const name2 = fromAdapt(nameStore2);
  return (
    <>
      <h2>Hello {name1.yelledName()}!</h2>
      <button onClick={() => nameStore1.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore1.reverseName()}>Reverse Name</button>

      <h2>Hello {name2.yelledName()}!</h2>
      <button onClick={() => nameStore2.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore2.reverseName()}>Reverse Name</button>
    </>
  );
}

StackBlitz

5. React to observable data sources ​

Multiple stores might need to react to the same observable, so it needs independent annotation.

tsx
const nameAdapter = createAdapter<string>()({
  reverseName: (name) => name.split('').reverse().join(''),
  concatName: (name, anotherName: string) => `${name} ${anotherName}`, 
  selectors: {
    yelledName: (name) => name.toUpperCase(), // Will be memoized
  },
});

const onNameFromServer = timer(3000).pipe(map(() => 'Joel')); 

function ObservableSources() {
  const nameStore1 = adapt('Bob', nameAdapter); 
  const nameStore1 = adapt('Bob', {
    adapter: nameAdapter,
    sources: onNameFromServer, // Set state
  });
  const name1 = fromAdapt(nameStore1);

  const nameStore2 = adapt('Bob', nameAdapter); 
  const nameStore2 = adapt('Bob', {
    adapter: nameAdapter,
    sources: {
      concatName: onNameFromServer, // Trigger a specific state reaction
    },
  });
  const name2 = fromAdapt(nameStore2);
  return (
    <>
      <h2>Hello {name1.yelledName()}!</h2>
      <button onClick={() => nameStore1.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore1.reverseName()}>Reverse Name</button>

      <h2>Hello {name2.yelledName()}!</h2>
      <button onClick={() => nameStore2.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore2.reverseName()}>Reverse Name</button>
    </>
  );
}

StackBlitz

6. Share DOM event sources with multiple stores ​

Don't write callback functions to imperatively change state in multiple stores. Instead, declare the DOM event as an independent source that multiple stores can react to.

tsx
const nameAdapter = createAdapter<string>()({
  reverseName: (name) => name.split('').reverse().join(''),
  concatName: (name, anotherName: string) => `${name} ${anotherName}`,
  selectors: {
    yelledName: (name) => name.toUpperCase(), // Will be memoized
  },
});

const onResetBoth = source(); 

const onNameFromServer = timer(3000).pipe(map(() => 'Joel'));

function DomSources() {
  const nameStore1 = adapt('Bob', {
    adapter: nameAdapter,
    sources: onNameFromServer, // Set state
    sources: {
      set: onNameFromServer, // `set` is provided with all adapters
      reset: onResetBoth, // `reset` is provided with all adapters
    },
  });
  const name1 = fromAdapt(nameStore1);

  const nameStore2 = adapt('Bob', {
    adapter: nameAdapter,
    sources: {
      concatName: onNameFromServer, // Trigger a specific state reaction
      reset: onResetBoth, // `reset` is provided with all adapters
    },
  });
  const name2 = fromAdapt(nameStore2);
  return (
    <>
      <h2>Hello {name1.yelledName()}!</h2>
      <button onClick={() => nameStore1.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore1.reverseName()}>Reverse Name</button>

      <h2>Hello {name2.yelledName()}!</h2>
      <button onClick={() => nameStore2.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore2.reverseName()}>Reverse Name</button>

      <button onClick={onResetBoth}>Reset Both</button>
    </>
  );
}

StackBlitz

7. Select state from multiple stores ​

tsx
const nameAdapter = createAdapter<string>()({
  reverseName: (name) => name.split('').reverse().join(''),
  concatName: (name, anotherName: string) => `${name} ${anotherName}`,
  selectors: {
    yelledName: (name) => name.toUpperCase(), // Will be memoized
  },
});

const onResetBoth = source();

const onNameFromServer = timer(3000).pipe(map(() => 'Joel'));

function MultiStoreSelectors() {
  const nameStore1 = adapt('Bob', {
    adapter: nameAdapter,
    sources: {
      set: onNameFromServer, // `set` is provided with all adapters
      reset: onResetBoth, // `reset` is provided with all adapters
    },
  });
  const name1 = fromAdapt(nameStore1);

  const nameStore2 = adapt('Bob', {
    adapter: nameAdapter,
    sources: {
      concatName: onNameFromServer, // Trigger a specific state reaction
      reset: onResetBoth, // `reset` is provided with all adapters
    },
  });
  const name2 = fromAdapt(nameStore2);

  const bothBobs = () => name1.state() === 'Bob' && name2.state() === 'Bob'; 

  return (
    <>
      <h2>Hello {name1.yelledName()}!</h2>
      <button onClick={() => nameStore1.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore1.reverseName()}>Reverse Name</button>

      <h2>Hello {name2.yelledName()}!</h2>
      <button onClick={() => nameStore2.set('Bilbo')}>Change Name</button>
      <button onClick={() => nameStore2.reverseName()}>Reverse Name</button>

      <button onClick={onResetBoth}>Reset Both</button>

      {bothBobs() && <h2>Hello Bobs!</h2>}
    </>
  );
}

StackBlitz