Awesome
JetState 🚀
Reactive state management with RxJS.
The project is archived. Please visit the successor – RxEffects.
Introduction
JetState is a library for reactive state management and built on top of RxJS. It takes the idea of multiple data stores from Flux, immutable updates from Redux and leverage data streaming by RxJS. In result, it provides observable data store model.
JetState has influenced by Akita. This library is a kind of reimplementation (not fork) of Akita's core API and its pattern, albeit some docs are reused. JetState provides only core features for observable data store pattern and tends to be clean and simple tool.
JetState is framework agnostic, it is more like "M" in your MVVM, MVP and other M?? architecture. Its opinionated structure provides a pattern for managing app's state which can be used in many cases.
Updates Data streams
+-------------> Store --------------+
| |
| |
| v
Backend API <---> Service Query <--- Other queries
^ |
| |
| |
+---------- UI Component <----------+
Actions Rendering
(methods)
Installation
Install from the NPM repository using npm or yarn:
-
@jetstate/core
Core functionality, framework agnostic. Can be used with Angular as is.
npm install @jetstate/core
yarn add @jetstate/core
-
@jetstate/react
Helpers for React.js to use query's observables in components with hooks.
npm install @jetstate/react
yarn add @jetstate/react
Concepts and API
Store
Store is a single object which contains the store state and serves as the "single source of truth."
To create a store, you need to extend Store
class, passing the type as well as its initial state.
import {Store} from '@jetstate/core';
export interface SessionState {
token: string;
name: string;
}
export function createInitialState(): SessionState {
return {
token: '',
name: '',
};
}
export class SessionStore extends Store<SessionState> {
constructor() {
super(createInitialState());
}
}
With this setup you get a Store
object with the following interface:
import {Observable} from 'rxjs';
interface Store<State extends object> {
/** Returns a current value of the state */
readonly state: Readonly<State>;
/** Returns an observable of state value which pushes a current value first */
readonly state$: Observable<Readonly<State>>;
/** Returns an observable of state changes */
readonly changes$: Observable<Readonly<State>>;
/** Updates the store by a specified patch object */
update(patch: Partial<Readonly<State>>): void;
/** Updates the store by a patch which is produced by calling the updater with a current state */
update(updater: (state: State) => Partial<Readonly<State>>): void;
}
Query
Query is a class offering functionality responsible for querying the store.
You can think of the query as being similar to database queries. Its constructor function receives as parameters its own store and possibly other query classes.
Queries can talk to other queries, join entities from different stores, etc.
To create a Query, you need to extend the Query
class from JetState.
import {Query} from '@jetstate/core';
export class SessionQuery extends Query<SessionState> {
name$ = this.select(state => state.name);
constructor(store: SessionStore) {
super(store);
}
}
With this setup you get a Query
object with the following interface:
import {Observable} from 'rxjs';
import {Selector, Projection} from '@jetstate/core';
export interface Query<State extends object> {
/** Returns a current value of the state */
readonly state: Readonly<Readonly<State>>;
/** Returns an observable which pushes the current value first. */
select<V>(selector: Selector<State, V>): Observable<V>;
/** Returns a subset of a state. */
project<V>(selector: Selector<State, V>): Projection<V>;
}
Where Selector
is a function which returns a value from a state. Its type is the following:
type Selector<State extends object, V> = (state: Readonly<State>) => V;
Projection
allows to slice a streaming subset of the state:
import {Observable} from 'rxjs';
export interface Projection<V> {
/** A current value */
readonly value: V;
/** An observable which pushes the current value first. */
readonly value$: Observable<V>;
/** An observable for value changes. */
readonly changes$: Observable<V>;
}
Service
It is recommended to use a service rather than call the store update methods directly by a component.
import {SessionStore} from './sessionStore';
import {tap} from 'rxjs/operators';
export class SessionService {
constructor(private sessionStore: SessionStore, private http: HttpClient) {}
login(credentials) {
return this.http.login(credentials).pipe(
tap(({name, token}) => {
this.sessionStore.update({name, token});
}),
);
}
}
Functional utilities
There are a few functions which help to create and use stores and queries in functional way.
import {createStore, createQuery, select, project} from '@jetstate/core';
export interface SessionState {
token: string;
name: string;
}
const sessionStore = createStore<SessionState>({
token: '',
name: '',
});
const sessionQuery = createQuery<SessionState>(sessionStore);
const name$ = sessionQuery.select(state => state.name);
const token$ = select(sessionStore, state => state.token);
const tokenChanges$ = project(sessionStore, state => state.token).changes$;
Best Practices
JetState does not restrict how you structure your code. Instead, it enforces a set of high-level principles:
- The Store is a single object that contains the store state and serves as the "single source of truth."
- The only way to change the state is by calling its
update()
method. - A UI component should NOT get data from the store directly but instead use a Query.
- Asynchronous logic and update calls should be encapsulated in services and data services.
When possible, try to avoid injecting the Query in the service. Instead, use the fact that it's already injected in the component and pass the required data into the service's method by arguments.
Examples
Angular project
Install dependencies:
npm install --save @jetstate/core
Usage example:
import {Component, Injectable, NgModule} from '@angular/core';
import {Query, Store} from '@jetstate/core';
import {map} from 'rxjs/operators';
// Declare a state:
export interface UserState {
userName: string;
}
// Define the store
export class UserStore extends Store<UserState> {
constructor() {
super({
userName: 'World',
});
}
}
// Use Query to read data from the store.
@Injectable()
export class UserQuery extends Query<UserState> {
username$ = this.select(state => state.username);
}
// Separate store updating from components.
@Injectable()
export class UserService {
constructor(private store: UserStore) {}
setUserName(username: string) {
this.store.update({username});
}
}
// Provide the store, query and service to the app:
@NgModule({
providers: [UserStore, UserQuery, UserService],
})
export class AppModule {}
// Use the state:
@Component({
selector: 'User',
template: `
{{ message$ | async }}
`,
})
export class UserComponent {
constructor(private query: UserQuery, private service: UserService) {}
message$ = this.query.username$.pipe(map(username => `Hello ${username}!`));
changeUserName(value: string) {
this.service.setUserName(value);
}
}
React project
Install dependencies:
npm install --save @jetstate/core @jetstate/react
Usage example:
import {Query, Store} from '@jetstate/core';
import {useObservable, useProjection} from '@jetstate/react';
// Declare a state:
export interface UserState {
userName: string;
}
// Define the store
export class UserStore extends Store<UserState> {
constructor() {
super({
userName: 'World',
});
}
}
// Use Query to read data from the store.
export class UserQuery extends Query<UserState> {
username$ = this.select(state => state.username);
usernameUpperCased = this.project(state => state.username.toUpperCase());
}
// Separate store updating from components.
export class UserService {
constructor(private store: UserStore) {}
setUserName(username: string) {
this.store.update({username});
}
}
// Use the state:
export function UserComponent(props: {query: UserQuery; service: UserService}) {
const {query, service} = props;
const username = useObservable(query.username$);
const usernameUpperCased = useProjection(query.usernameUpperCased);
return (
<div>
<h1>
Hello {username}! {usernameUpperCased}!!
</h1>
<input
type="text"
value={username}
onChange={event => service.setUserName(event.target.value)}
/>
</div>
);
}