```typescript!
import { rxActions } from '@rx-angular/state/actions';
type ListUi = { searchInput: string };
@Component({
template: `
<input name="search" (input)="ui.searchInput($event)" />
<input name="page-size" (input)="ui.pageSizeInput($event)" type="number" />
`,
})
class ListComponent {
protected ui = rxActions<ListUi>(({transforms}) => transforms({
searchInput: eventValue,
pageSizeInput: eventValue
});
@Output()
queryUpdate = this.ui.searchInput$;
@Output()
sizeUpdate = this.ui.pageSizeInput$;
}
@Injectable()
class MovieListState {
actions = rxActions<{
paginate: number;
}>();
paginatedList$ = this.actions.paginate$.pipe(
exhaustMap(page => this.movieService.load(page))
)
}
```
```typescript
import { rxState } from '@rx-angular/state';
import { rxActions } from '@rx-angular/state/actions';
import { rxEffects } from '@rx-angular/state/effects';
@Component()
export class MovieListComponent {
private movieService = inject(MovieService);
private actions = rxActions<{ paginate: void; }>();
private state = rxState<{
list: Movie[];
page: number;
maxPages: number;
}>(({ set, get, connect }) => {
set({ list: [], page: 0, maxPages: 0 });
connect('list', this.movieService.fetch(0));
connect('maxPages', this.movieService.maxPages());
connect(
this.actions.paginate$.pipe(
filter(() => get('page') + 1 < get('maxPages')),
exhaustMap(() => this.movieService.fetch(get('page') + 1))
),
({ list, page }, listToAppend) => ({
list: list.concat(...listToAppend),
page: page + 1
})
);
});
readonly list$ = this.state.select('list');
private effects = rxEffects(({ register }) => {
register(this.state.select(['page', 'maxPages']), ({ page, maxPages }) => {
if (page === maxPages) {
alert('last page reached, no more to load');
}
});
});
}
```
```typescript
import { rxState } from '@rx-angular/state';
import { rxActions } from '@rx-angular/state/actions';
import { rxEffects } from '@rx-angular/state/effects';
@Component()
export class MovieSearchComponent {
private movieService = inject(MovieService);
actions = rxActions<{ search: string; }>();
private state = rxState<{
list: Movie[];
loading: boolean;
search: string;
}>(({ set, get, select, connect }) => {
set({ list: [], search: '', loading: false });
connect(
this.actions.filter$.pipe(
exhaustMap((query) => this.movieService.fetch(query).pipe(
startWith({ loading: true, search: query }),
map((list) => ({ list }))
endWith({ loading: false })
))
)
);
});
private effects = rxEffects(({ register }) => {
register(this.state.select(['search', 'list', 'loading']), ({ filter, list, loading }) => {
if (!loading && filter && list.length === 0) {
alert('nothing found with this filter');
}
})
});
}
```
```typescript
/* state should force users to set initial values for required fields */
interface State {
todos: Todo[];
loading: boolean;
filter?: string;
}
const state = rxState<State>(({ initialize }) => {
// we have to force users
initialize({})
});
// 1. insert a signal into state
state.connect(signal);
// 2. read signal from state
const listLength = state.computed(({ todos }) => todos().length);
const modelIsViewModel = state.get();
class Component {
private state = rxState<State>(({ initialValue, set, connect, setAccumulator /* ,select, get, */}) => {
initialValue({});
set({ todos: [], loading: true });
connect('todos', inject(TodoService).fetch());
});
const additionalTodods = state.computed(({ todos }) => todos(), pipe(
switchMap(todos => this.service.getPage(todos.length))
))
const additionalTodods = state.computedFrom(['todos'], pipe(
switchMap(todos => this.service.getPage(todos.length))
));
const filteredTodos = toSignal(
this.state.select(['todos', 'filter']).pipe(
switchMap(({ todos, filter }) => this.service.getPage(todos.length, filter))
)
);
const filteredTodos = this.state.select(['todos', 'filter']).pipe(
switchMap(({ todos, filter }) => this.service.getPage(todos.length, filter))
)
// Signal<boolean>
const loaded = state.computed(({ loading }) => !loading());
// Signal<boolean>
const loading = state.signal('loading');
// Signal<boolean>
const debouncedLoading = state.computedFrom(['loading', 'filter'], pipe(
map(({ loading, filter }) => loading),
debounceTime(200)
));
// Signal<boolean>
const debouncedLoading = state.computedFrom(
state => state.loading,
pipe(
debounceTime(200)
)
);
// Signal<boolean>
// Option 1: computedFrom baked into `rxState`, accepting a `pipe`,
// returning a `Signal<T>`
const debouncedLoading = state.computedFrom(pipe(
select('loading'),
debounceTime(200)
));
// Signal<boolean>
// Option 1: computedFrom baked into `rxState`, accepting a `pipe`,
// returning a `Signal<T>`
const debouncedLoading = state.computedFrom(select(
map(({ loading }) => loading),
debounceTime(200)
));
// Signal<boolean>
// Option 2: use `toSignal` and state `select` to achieve the same result
const debouncedLoading = toSignal(state.select(
select('loading'),
debounceTime(200)
));
// loading === loading2
// Signal<State> -> not really possible to implement actually
const loading = state.signal();
// Signal<State>
const signalState = state.computed(s => s);
// Observable<{ loading: boolean }>
const loading$ = state.select(['loading']);
// Observable<boolean>
const loading$ = state.select('loading');
}
function rxStore<T>() {
const state = rxState();
const effects = rxEffects();
const actions = rxActions();
return {
...state,
...effects,
...actions
}
}
```