Skip to content

Commit

Permalink
docs: Docs
Browse files Browse the repository at this point in the history
  • Loading branch information
mnasyrov committed Aug 3, 2021
1 parent d84a2e2 commit e306eba
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 30 deletions.
63 changes: 49 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ Reactive state and effect management with RxJS.

## Overview

The library provides a way to declare actions and effects, states and stores. The core package is framework-agnostic which can be used independently in libraries, backend and frontend apps, including micro-frontends architecture.
The library provides a way to describe business and application logic using MVC-like architecture. Core elements include actions and effects, states and stores. All of them are optionated and can be used separately. The core package is framework-agnostic and can be used in different cases: libraries, server apps, web, SPA and micro-frontends apps.

The library is inspired by [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller), [RxJS](https://github.com/ReactiveX/rxjs), [Akita](https://github.com/datorama/akita) and [Effector](https://github.com/effector/effector).
The library is inspired by [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller), [RxJS](https://github.com/ReactiveX/rxjs), [Akita](https://github.com/datorama/akita), [JetState](https://github.com/mnasyrov/jetstate) and [Effector](https://github.com/effector/effector).

## Features
### Features

- Framework-agnostic
- Functional API
Expand All @@ -37,7 +37,7 @@ The library is inspired by [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93vie
- Effect container
- Typescript typings

## Packages
### Packages

Please find the full documentation by the links below.

Expand All @@ -54,7 +54,20 @@ npm install rx-effects rx-effects-react --save

### Concepts

`// TODO`
The main idea is to use the classic MVC pattern with event-based models (state stores) and reactive controllers (actions and effects). The view subscribes to model changes (state queries) of the controller and requests the controller to do some actions.

![Diagram](docs/concept-diagram.svg)

Core elements:

- `State` – a data model.
- `StateQuery` – a getter and subscriber for data of the state.
- `StateMutation` – a pure function which changes the state.
- `Store` – a state storage, it provides methods to update and subscribe the state.
- `Action` – an event emitter.
- `Effect` – a piece of business logic which handles the action and makes state changes and side effects.
- `EffectScope` – a controller-like boundary for effects and business logic
- `Controller` – a controller type for effects and business logic

### Example

Expand All @@ -64,6 +77,7 @@ Below is an implementation of the pizza shop, which allows order pizza from the
// pizzaShop.ts

import {
Controller,
createAction,
createEffectScope,
declareState,
Expand All @@ -73,8 +87,10 @@ import {
} from 'rx-effects';
import { delay, filter, map, mapTo, of } from 'rxjs';

// The state
type CartState = { orders: Array<string> };

// State mutation can be exported and tested separately
const addPizzaToCart =
(name: string): StateMutation<CartState> =>
(state) => ({ ...state, orders: [...state.orders, name] });
Expand All @@ -86,57 +102,70 @@ const removePizzaFromCart =
orders: state.orders.filter((order) => order !== name),
});

// Declaring the state. `declareState()` returns a few factories for the store.
const CART_STATE = declareState<CartState>(() => ({ orders: [] }));

export type PizzaShopController = {
// Declaring the controller.
// It should provide methods for triggering the actions,
// and queries or observables for subscribing to data.
export type PizzaShopController = Controller<{
ordersQuery: StateQuery<Array<string>>;

addPizza: (name: string) => void;
removePizza: (name: string) => void;
submitCart: () => void;
submitState: EffectState<Array<string>>;

destroyController: () => void;
};
}>;

export function createPizzaShopController(): PizzaShopController {
// Creates the state store
const store = CART_STATE.createStore();

// Creates queries for the state data
const ordersQuery = store.query((state) => state.orders);

// Introduces actions
const addPizza = createAction<string>();
const removePizza = createAction<string>();
const submitCart = createAction();

// Creates the scope for effects to track internal subscriptions
const scope = createEffectScope();

// Handle simple actions
scope.handleAction(addPizza, (order) => store.update(addPizzaToCart(order)));

scope.handleAction(removePizza, (name) =>
store.update(removePizzaFromCart(name)),
);

// Create a effect in a general way
const submitEffect = scope.createEffect<Array<string>>((orders) => {
// Sending an async request to a server
return of(orders).pipe(delay(1000), mapTo(undefined));
});

// Effect can handle `Observable` and `Action`. It allows to filter action events
// and transform data which is passed to effect's handler.
submitEffect.handle(
submitCart.event$.pipe(
map(() => store.get().orders),
map(() => ordersQuery.get()),
filter((orders) => !submitEffect.pending.get() && orders.length > 0),
),
);

// Effect's results can be used as actions
scope.handleAction(submitEffect.done$, () =>
store.set(CART_STATE.initialState),
);

return {
ordersQuery: store.query((state) => state.orders),
ordersQuery,
addPizza,
removePizza,
submitCart,
submitState: submitEffect,
destroyController: () => scope.destroy(),
destroy: () => scope.destroy(),
};
}
```
Expand All @@ -148,13 +177,19 @@ import React, { FC, useEffect } from 'react';
import { useConst, useObservable, useStateQuery } from 'rx-effects-react';
import { createPizzaShopController } from './pizzaShop';

export const PizzaShop: FC = () => {
export const PizzaShopComponent: FC = () => {
// Creates the controller and destroy it on unmounting the component
const controller = useConst(() => createPizzaShopController());
useEffect(() => controller.destroyController, [controller]);
useEffect(() => controller.destroy, [controller]);

// The same creation can be achieved by using `useController()` helper:
// const controller = useController(createPizzaShopController);

// Using the controller
const { ordersQuery, addPizza, removePizza, submitCart, submitState } =
controller;

// Subscribing to state data and the effect stata
const orders = useStateQuery(ordersQuery);
const isPending = useStateQuery(submitState.pending);
const submitError = useObservable(submitState.error$, undefined);
Expand Down
29 changes: 29 additions & 0 deletions docs/concept-diagram.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@startuml

rectangle View {
[Component] as "UI Component"
}

rectangle Controller {
collections Effects
collections Queries
}

rectangle Model {
database Store as "State Store"
}

cloud Backend

Component --> Effects : Actions
Component <-- Queries : Rendering

Effects --> Store : Updates
Effects <--> Backend : Async API

Effects -> Queries : Results

Store --> Queries : Data


@enduml
53 changes: 53 additions & 0 deletions docs/concept-diagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import {
Action,
Controller,
createAction,
createEffectScope,
declareState,
Effect,
EffectScope,
StateDeclaration,
StateMutation,
Store,
} from 'rx-effects';
import { firstValueFrom } from 'rxjs';
import { take, toArray } from 'rxjs/operators';
import { Action, createAction } from './action';
import { Controller } from './controller';
import { Effect } from './effect';
import { createEffectScope, EffectScope } from './effectScope';
import { declareState, StateDeclaration } from './stateDeclaration';
import { StateMutation } from './stateMutation';
import { Store } from './store';

// Example usage of RxEffects: a calculator which has actions: increment,
// decrement, add, subtract and reset.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Controller,
createAction,
createEffectScope,
declareState,
Expand All @@ -8,8 +9,10 @@ import {
} from 'rx-effects';
import { delay, filter, map, mapTo, of } from 'rxjs';

// The state
type CartState = { orders: Array<string> };

// State mutation can be exported and tested separately
const addPizzaToCart =
(name: string): StateMutation<CartState> =>
(state) => ({ ...state, orders: [...state.orders, name] });
Expand All @@ -21,56 +24,69 @@ const removePizzaFromCart =
orders: state.orders.filter((order) => order !== name),
});

// Declaring the state. `declareState()` returns a few factories for the store.
const CART_STATE = declareState<CartState>(() => ({ orders: [] }));

export type PizzaShopController = {
// Declaring the controller.
// It should provide methods for triggering the actions,
// and queries or observables for subscribing to data.
export type PizzaShopController = Controller<{
ordersQuery: StateQuery<Array<string>>;

addPizza: (name: string) => void;
removePizza: (name: string) => void;
submitCart: () => void;
submitState: EffectState<Array<string>>;

destroyController: () => void;
};
}>;

export function createPizzaShopController(): PizzaShopController {
// Creates the state store
const store = CART_STATE.createStore();

// Creates queries for the state data
const ordersQuery = store.query((state) => state.orders);

// Introduces actions
const addPizza = createAction<string>();
const removePizza = createAction<string>();
const submitCart = createAction();

// Creates the scope for effects to track internal subscriptions
const scope = createEffectScope();

// Handle simple actions
scope.handleAction(addPizza, (order) => store.update(addPizzaToCart(order)));

scope.handleAction(removePizza, (name) =>
store.update(removePizzaFromCart(name)),
);

// Create a effect in a general way
const submitEffect = scope.createEffect<Array<string>>((orders) => {
// Sending an async request to a server
return of(orders).pipe(delay(1000), mapTo(undefined));
});

// Effect can handle `Observable` and `Action`. It allows to filter action events
// and transform data which is passed to effect's handler.
submitEffect.handle(
submitCart.event$.pipe(
map(() => store.get().orders),
map(() => ordersQuery.get()),
filter((orders) => !submitEffect.pending.get() && orders.length > 0),
),
);

// Effect's results can be used as actions
scope.handleAction(submitEffect.done$, () =>
store.set(CART_STATE.initialState),
);

return {
ordersQuery: store.query((state) => state.orders),
ordersQuery,
addPizza,
removePizza,
submitCart,
submitState: submitEffect,
destroyController: () => scope.destroy(),
destroy: () => scope.destroy(),
};
}
Loading

0 comments on commit e306eba

Please sign in to comment.