Examples
Basic Toggle
Edit this snippetimport { 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
import { RxToggle } from './RxToggle';
import { useReactable } from '@reactables/react';
function App() {
const [toggleState, actions] = useReactable(RxToggle);
const { toggleOn, toggleOff, toggle } = actions;
return (
<>
<h5>Reactable Toggle</h5>
Toggle is: {toggleState ? 'On' : 'Off'}
<br />
<button onClick={toggleOn}>Toggle On</button>
<button onClick={toggleOff}>Toggle Off</button>
<button onClick={toggle}>Toggle</button>
</>
);
}
export default App;
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RxToggle } from './RxToggle';
// See Reactable Directive
// at https://reactables.github.io/angular/reactable-directive
import { ReactableDirective } from './reactable.directive';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ReactableDirective],
template: `
<div *reactable="rxToggle; let state = state; let actions = actions;">
<h1>Angular Reactable Toggle: {{ state ? 'on' : 'off' }}</h1>
<button (click)="actions.toggleOn()">Toggle On </button>
<button (click)="actions.toggleOff()">Toggle Off </button>
<button (click)="actions.toggle()">Toggle </button>
</div>
`,
})
export class App {
rxToggle = RxToggle();
}
import { RxToggle } from './RxToggle';
const [state$, actions] = RxToggle();
const { toggleOn, toggleOff, toggle } = actions;
state$.subscribe((toggleState) => {
// Update the view when state changes.
document.getElementById('toggle-state')
.innerHTML = toggleState ? 'On' : 'Off';
});
// Bind click handlers
document.getElementById('toggle-on')
.addEventListener('click', toggleOn);
document.getElementById('toggle-off')
.addEventListener('click', toggleOff);
document.getElementById('toggle')
.addEventListener('click', toggle);
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
.
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 snippetimport { 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
import { useReactable } from '@reactables/react';
import DataService from './data-service';
import { RxFetchData } from './RxFetchData';
import './App.css';
function App() {
const [state, actions] = useReactable(RxFetchData, {
dataService: new DataService(),
});
if (!state) return;
const { loading, data } = state;
return (
<>
<div>
{data && <span>{data}</span>}
<br />
<button onClick={actions.fetch}>Fetch Data!</button>
<br />
{loading && <span>Fetching...</span>}
</div>
</>
);
}
export default App;
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RxFetchData, FetchDataReactable } from './RxFetchData';
import DataService from './data-service';
// See Reactable Directive
// at https://reactables.github.io/angular/reactable-directive
import { ReactableDirective } from './reactable.directive';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ReactableDirective],
template: `
<div *reactable="rxFetchData; let state = state; let actions = actions">
<h1>Reactable fetching data</h1>
<span *ngIf="state.data">{{ state.data }}</span>
<br />
<button (click)="actions.fetch()">Fetch Data!</button>
<br />
<span *ngIf="state.loading">Fetching...</span>
</div>
`,
})
export class App implements OnInit {
rxFetchData!: FetchDataReactable;
constructor(private dataService: DataService) {}
ngOnInit() {
this.rxFetchData = RxFetchData({ dataService: this.dataService });
}
}
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.
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
import { useReactable } from '@reactables/react';
import HotelService from './hotel-service';
import { RxHotelSearch } from './Rx/RxHotelSearch';
import './App.css';
function App() {
const [state, actions] = useReactable(RxHotelSearch, {
hotelService: new HotelService(),
});
if (!state) return;
const {
controls: { smokingAllowed, petsAllowed },
searchResult: { loading, data },
} = state;
return (
<>
<div>
<br />
<button onClick={actions.toggleSmokingAllowed}>
Smoking Allowed : {smokingAllowed ? 'Yes' : 'No'}{' '}
</button>
<br />
<br />
<button onClick={actions.togglePetsAllowed}>
Pets Allowed : {petsAllowed ? 'Yes' : 'No'}{' '}
</button>
<br />
{loading && 'Searching...'}
<br />
{data && data}
</div>
</>
);
}
export default App;
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
RxHotelSearch,
HotelSearchState,
HotelSearchActions,
} from './Rx/RxHotelSearch';
import { Reactable } from '@reactables/core';
import HotelService from './hotel-service';
// See Reactable Directive
// at https://reactables.github.io/angular/reactable-directive
import { ReactableDirective } from './reactable.directive';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ReactableDirective],
template: `
<div *reactable="rxHotelSearch; let state = state; let actions = actions;">
<h1>Hotel Search</h1>
<button (click)="actions.toggleSmokingAllowed()">Smoking Allowed: {{ state.controls.smokingAllowed ? 'Yes' : 'No'}}</button>
<button (click)="actions.togglePetsAllowed()">Pets Allowed: {{ state.controls.petsAllowed ? 'Yes' : 'No'}}</button>
<br />
<span *ngIf="state.searchResult.loading">Searching...</span>
<br />
<span *ngIf="state.searchResult.data">{{ state.searchResult.data }}</span>
</div>
`,
})
export class App implements OnInit {
rxHotelSearch!: Reactable<HotelSearchState, HotelSearchActions>;
constructor(private hotelService: HotelService) {}
ngOnInit() {
this.rxHotelSearch = RxHotelSearch({ hotelService: this.hotelService });
}
}
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
.
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
import { RxToggleCounter } from './RxToggleCounter';
import { useReactable } from '@reactables/react';
function App() {
const [state, actions] = useReactable(RxToggleCounter);
if (!state) return;
const {
toggle: toggleState,
counter: { count },
} = state;
const {
toggle: { toggleOn, toggleOff, toggle },
resetCounter,
} = actions;
return (
<>
<h5>Reactable Toggle</h5>
Toggle is: {toggleState ? 'On' : 'Off'}
<br />
<button onClick={toggleOn}>Turn On</button>
<button onClick={toggleOff}>Turn Off</button>
<button onClick={toggle}>Toggle</button>
<br />
<br />
Toggle Button Count: {count}
<br />
<button onClick={resetCounter}>Reset Count</button>
</>
);
}
export default App;
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RxToggleCounter } from './RxToggleCounter';
// See Reactable Directive
// at https://reactables.github.io/angular/reactable-directive
import { ReactableDirective } from './reactable.directive';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ReactableDirective],
template: `
<div *reactable="rxToggleCounter; let state = state; let actions = actions;">
<h1>Angular Reactable Toggle</h1>
<h2>Toggle is {{ state.toggle ? 'on' : 'off' }}</h2>
<button (click)="actions.toggle.toggleOn()">Turn On </button>
<button (click)="actions.toggle.toggleOff()">Turn Off </button>
<button (click)="actions.toggle.toggle()">Toggle </button>
<br>
<br>
<h2>Toggle Button Count: {{ state.counter.count }}</h2>
<br>
<button (click)="actions.resetCounter()">Reset Counter </button>
</div>
`,
})
export class App {
rxToggleCounter = RxToggleCounter();
}
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());