Examples

Basic Toggle

Edit this snippet
import { RxBuilder, Reactable } from '@reactables/core';

type ToggleState = boolean;

type ToggleActions = {
  toggleOn: () => void;
  toggleOff: () => void;
  toggle: () => void;
};

export const RxToggle = (
  initialState = false
): Reactable<ToggleState, ToggleActions> =>
  RxBuilder({
    initialState,
    reducers: {
      toggleOn: () => true,
      toggleOff: () => false,
      toggle: (state: ToggleState) => !state,
    },
  });

Bind RxToggle to View

Debugging

When creating a reactable primitive with RxBuilder, a debug option is available to console.log all the actions and state updates occuring within that primitive.

Debug Example:




Extending Functionality

We can extend the functionality of reactables by passing in extra reducers as an option. To illustrate, we can make a slight modification to RxToggle and create a RxExtendedToggle that can also toggle based on 1 or 0 .

Edit this snippet
import { RxBuilder } from '@reactables/core';

export const RxToggle = ({initialState = false, reducers }) =>
  RxBuilder({
    initialState,
    reducers: {
      toggleOn: () => true,
      toggleOff: () => false,
      toggle: (state) => !state,
      ...reducers,
    },
  });

export const RxExtendedToggle = ({ initialState }) =>
  RxToggle({
    initialState,
    reducers: {
      numberToggle: (state, { payload }) => {
        switch (payload) {
          case 0:
            return false;
          case 1:
            return true;
          default:
            return state;
        }
      },
    },
  });


Fetching Data with an Effect

Edit this snippet
import { RxBuilder, Reactable } from '@reactables/core';
import DataService from './data-service';
import { from, of } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';

export type FetchDataState = {
  loading: boolean;
  success: boolean;
  data: string | null;
  error: unknown;
};

const initialState: FetchDataState = {
  loading: false,
  success: false,
  data: null,
  error: null,
};

export type FetchDataActions = {
  fetch: () => void;
};

export type FetchDataReactable = Reactable<FetchDataState, FetchDataActions>;

export const RxFetchData = ({
  dataService,
}: {
  dataService: DataService;
}): FetchDataReactable =>
  RxBuilder({
    initialState,
    reducers: {
      fetch: {
        reducer: (state) => ({ ...state, loading: true }),
        effects: [
          (action$) =>
            action$.pipe(switchMap(() => from(dataService.fetchData()))).pipe(
              map((response) => ({ type: 'fetchSuccess', payload: response })),
              catchError((err: unknown) =>
                of({ type: 'fetchFailure', payload: true })
              )
            ),
        ],
      },
      fetchSuccess: (state, action) => ({
        ...state,
        success: true,
        loading: false,
        data: action.payload as string,
        error: null,
      }),
      fetchFailure: (state, action) => ({
        ...state,
        loading: false,
        error: action.payload,
        success: false,
      }),
    },
  });

Bind RxFetchData to View

Actions On Component Mount

There are cases where we want actions to occur when a reactable is initialized during the UI component’s mount.

In our above fetching data example, the data is only fetched when the user clicks the button. We can make an update so the page fetches data on load.

We can add a source observable that emits only one action and completes with rxjs of function. This action then occurs when the reactable is initialized during component mount.

Edit this snippet
// ... ///
export const RxFetchData = ({
  dataService,
}: {
  dataService: DataService;
}): FetchDataReactable =>
  RxBuilder({
    initialState,
    sources: [of({type: 'fetch'})] // Add source observable
    reducers: {
      // ... //
    },
  });


Composition with Reactables

Aside from creating Reactable primitives with the RxBuilder factory function, you can also combine any number of Reactables together to form a new one.

Two primary use cases for this approach (not mutually exclusive):

  • You wish to create a Reactable that reuses functionality from other Reactables.

  • One part of your state needs to react to changes of another part.

Using an example for illustration. Consider a naive search that filter’s hotels based on smokingAllowed and petsAllowed. Using RxToggle and a slightly modified RxFetchData from previous examples, we will combine them and implement the search.

We can start with the toggle filter controls for smokingAllowed and petsAllowed. We will want a reactable with the following state and actions.

Edit this snippet
export type SearchControlsState = {
  smokingAllowed: ToggleState; // boolean
  petsAllowed: ToggleState; // boolean
};

export type SearchControlsActions = {
  toggleSmokingAllowed: () => void;
  togglePetsAllowed: () => void;
};

We can initialize an RxToggle for each filter control and use RxJS’s combineLatest function to combine the state observables together to create RxSearchControls.

import { combineLatest } from 'rxjs';

...

export const RxSearchControls = (): Reactable<
  SearchControlsState,
  SearchControlsActions
> => {
  const [smokingAllowed$, { toggle: smokingToggle }] = RxToggle();
  const [petsAllowed$, { toggle: petsToggle }] = RxToggle();

  const state$ = combineLatest({
    smokingAllowed: smokingAllowed$,
    petsAllowed: petsAllowed$,
  });

  const actions = {
    toggleSmokingAllowed: smokingToggle,
    togglePetsAllowed: petsToggle,
  };

  return [state$, actions];
};

Next, we create an RxHotelSearch reactable that includes RxSearchControls and RxFetchData.

We know when there is a state change in RxSearchControls, RxFetchData will have to react and fetch data to perform the search.

We will pipe the state observable from RxSearchControls and map it to a fetch action. Then provide this piped observable, fetchOnSearchChange$, as a source for RxFetchData during initialization.

import { Reactable } from '@reactables/core';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import {
  RxSearchControls,
  SearchControlsState,
  SearchControlsActions,
} from './RxSearchControls';
import { RxFetchData, FetchDataState } from './RxFetchData';
import HotelService from '../hotel-service';

type HotelSearchState = {
  controls: SearchControlsState;
  searchResult: FetchDataState;
};

type HotelSearchActions = SearchControlsActions;

export const RxHotelSearch = ({
  hotelService,
}: {
  hotelService: HotelService;
}): Reactable<HotelSearchState, HotelSearchActions> => {
  const [searchControls$, searchControlActions] = RxSearchControls();

  const fetchOnSearchChange$ = searchControls$.pipe(
    map((search) => ({ type: 'fetch', payload: search }))
  );

  const [searchResult$] = RxFetchData({
    dataService: hotelService,
    sources: [fetchOnSearchChange$],
  });

  const state$ = combineLatest({
    controls: searchControls$,
    searchResult: searchResult$,
  });

  const actions = searchControlActions;

  return [state$, actions];
};

We then use combineLatest function again to to give us our combined state observable.

Bind RxHotelSearch to View

Communication between Reactables

The reactable composition example above is a case where one reactable reacts to the state changes of another.

Reactables can also emit their actions for other reactables to receive. The reactable interface has a third optional item which is an observable emitting the reactable’s actions.

All reactable primitives created with RxBuilder provides the actions observable.

When composing reactables the developer can decide what actions to expose (if any) by merging any number of action observables together with RxJS.

Below is an example where a counter reactable, RxCounter, is extended to react to toggle actions emitted by RxToggle.

Edit this snippet
import { ofTypes, Action, Reactable } from '@reactables/core';
import { Observable, combineLatest } from 'rxjs';

import { RxToggle, ToggleActions, ToggleState } from './RxToggle';
import { RxCounter, CounterState } from './RxCounter';

interface ToggleCounter {
  toggle: ToggleState;
  counter: CounterState;
}

interface ToggleCounterActions {
  toggle: ToggleActions;
  resetCounter: () => void;
}

export const RxToggleCounter = (): Reactable<
  ToggleCounter,
  ToggleCounterActions
> => {
  const [toggleState$, toggleActions, toggleActions$] = RxToggle();

  const toggled$ = (toggleActions$ as Observable<Action<unknown>>)
    .pipe(ofTypes(['toggle']));

  const [counter$, { reset }] = RxCounter({
    sources: [toggled$],
    reducers: {
      toggle: (state) => ({ count: state.count + 1 }),
    },
  });

  const state$ = combineLatest({
    toggle: toggleState$,
    counter: counter$,
  });

  const actions = {
    toggle: toggleActions,
    resetCounter: reset,
  };

  return [state$, actions];
};

Bind RxToggleCounter to View

Global State with Reactables

Your global state can be managed by one Reactable. This Reactable can be created with RxBuilder or via composition.

Reactables are unopinionated on how they are stored and accessed for global state management.

In React you can use a Context or prop drilling. @reactables/react package has a StoreProvider component if you want to use a context to store your reactable. The state can then be accessed with the useAppStore hook.

In Angular, initializing your Reactable in a service provided in root is an easy choice.

You can use the APIs available in your framework for storing Reactable(s) in the global scope.

Decorate Reactable with storeValue

By default, the state observable from a Reactable is just an Observable. It does not hold a value and only emits a new state object when an action is invoked.

When using a Reactable for managing global state, it needs to be decorated with the storeValue decorator which extends the Reactable to return a ReplaySubject instead of the default state Observable. This ensures subsequent subscriptions from UI components will always receive the latest value.

Example:

const [
  state$, // state$ is now a ReplaySubject
  actions
] = storeValue(RxToggle());