redux-app
Type-safe, DRY and OO redux. Implemented with typescript.
Change Log
Installation
npm install --save redux-app
Short Example
@component
class App {
counter = new Counter();
}
@component
class Counter {
value = 0;
increment() {
this.value = this.value + 1;
}
}
const app = new ReduxApp(new App());
console.log(app.root.counter.value);
console.log(app.store.getState());
app.root.counter.increment();
console.log(app.root.counter.value);
console.log(app.store.getState());
Important Notice
You should not mutate the object properties but rather assign them with new values.
That's why we write this.value = this.value + 1
and not this.value++
.
More Examples
More examples, including usage with Angular or Aurelia, can be found here redux-app-examples.
How it works
For each component
decorated class the library generates an underlying Component
object that holds the same properties and methods.
The new Component object has it's prototype patched and all of it's methods replaced with dispatch() calls.
The generated Component also has a hidden 'reducer' property which is later on used by redux store. The 'reducer' property itself is
generated from the original object methods, replacing all 'this' values with the current state from the store on each call (using
Object.assign and Function.prototype.call).
Reading the source tip #1: There are two main classes in redux-app. The first is ReduxApp and the second is Component.
Documentation
Stay Pure
Although redux-app embraces a new syntax it still adheres to the three principals of redux:
- The store is still the single source of truth. An automatic process propagates it to the components, similarly to what happens in react-redux.
- The state is still read only. Don't mutate the component's state directly, only via actions (methods).
- Changes are made with pure functions so keep your actions pure.
Features
Async Actions
Async actions (thunks, sagas, epics...) and side effects are handled in redux-app by using either the sequence
decorator or the noDispatch
.
Both decorators does exactly the same and are actually aliases of the same underlying function. What they do is
to tell redux-app that the decorated method is a plain old javascript method and that it should not be patched (about
the patch process see How it works). So, to conclude, what these decorators actually do is to tell
redux-app to do nothing special with the method.
Remember:
- Don't change the state inside
noDispatch
methods. - If you need to dispatch a series of actions use the
sequence
decorator. Don't call actions from within other actions directly.
Usage:
working example can be found on the redux-app-examples page
@component
class MyComponent {
public setStatus(newStatus: string) {
this.status = newStatus;
}
@sequence
public async fetchImage() {
this.setStatus('Fetching...');
var response = await fetch('fetch something from somewhere');
var responseBody = await response.json();
this.setStatus('Adding unnecessary delay...');
setTimeout(() => {
this.setStatus('I am done.');
}, 2000);
}
@noDispatch
public doNothing() {
console.log('I am a plain old method. Nothing special here.');
}
}
withId
The role of the withId
decorator is double. From one hand, it enables the co-existence of two (or more) instances of the same component,
each with it's own separate state. From the other hand, it is used to keep two separate components in sync. Every component, when dispatching
an action attaches it's ID to the action payload. The reducer in it's turn reacts only to actions targeting it's component ID.
The 'id' argument of the decorator can be anything (string, number, object, etc.).
Example:
working example can be found on the redux-app-examples page
@component
export class App {
@withId('SyncMe')
public counter1 = new CounterComponent();
@withId('SyncMe')
public counter2 = new CounterComponent();
@withId(123)
public counter3 = new CounterComponent();
@withId()
public counter4 = new CounterComponent();
}
connect
One of the most useful features of redux-app is the ability to connect
components. Connected components are references to other components.
The connection is achieved using a "smart getter".
It is smart in that sense that it waits for the target component to be available and than replace itself
(i.e. the getter) with a simple reference to the target object, thus preventing further unnecessary invocations of the getter.
You can use IDs to connect to a specific component or omit the ID to connect to the first instance that redux-app finds.
You can connect a view to parts of the app tree, as shown in the next example. You can also connect two, or more, components to a single source inside your app tree. To see a working example of the latter checkout the examples repository.
Remember: When connecting components there should always be at least one non-connected instance of that component in your ReduxApp tree (a.k.a. the "source" component).
Example:
working example can be found on the redux-app-examples page
@component
class App {
public myComponent = new MyComponent();
}
@component
class MyComponent {
public message = 'hello!';
}
const app = new ReduxApp(new App());
class MyView {
@connect
public myComponentReference: MyComponent;
}
You can pass an optional 'options' argument to the connect
decorator:
export class ConnectOptions {
app?: string;
id?: any;
live?: boolean;
}
Computed Values
It is possible to automatically calculate values from other parts of the components state (similar in concept to redux selectors).
To do that just declare a getter and decorate it with the computed
decorator. Behind the scenes redux-app will replace the getter
with regular values and will take care of updating it after each change to the relevant state.
Note: As everything else, computed value getters should also be pure and should not mutate other parts of the state.
Example:
working example can be found on the redux-app-examples page
@component
class ComputedGreeter {
public name: string;
@computed
public get welcomeString(): string {
return 'hello ' + this.name;
}
public setName(newVal: string) {
this.name = newVal;
}
}
Utilities
ignoreState
You can use the ignoreState
decorator to prevent particular properties of your components to be stored in the store.
Example:
@component
class MyComponent {
public storeMe = 'hello';
@ignoreState
public ignoreMe = 'not stored';
}
const app = new ReduxApp(new MyComponent());
console.log(app.root);
console.log(app.store.getState());
isInstanceOf
We've already said that classes decorated with the component
decorator are being replaced at runtime
with a generated subclass of the base Component class. This means you lose the ability to have assertions
like this:
@component
class MyComponent {
}
if (!(obj instanceof MyComponent))
throw new Error("Invalid argument. Expected instance of MyComponent");
Luckily redux-app supplies a utility method called isInstanceOf
which you can use instead:
@component
class MyComponent {
}
if (!isInstanceOf(obj, MyComponent))
throw new Error("Invalid argument. Expected instance of MyComponent");
The updated code will throw either if obj
is instance of MyComponent
or if it is an instance of a Component that was generated from the MyComponent
class. In all other cases the call to isInstanceOf
will return false
and no exception will be thrown.
Applying Enhancers
The ReduxApp
class has few constructor overloads that lets you pass additional store arguments (for instance, the awesome devtool extension enhancer).
constructor(appCreator: T, enhancer?: StoreEnhancer<T>);
constructor(appCreator: T, options: AppOptions, enhancer?: StoreEnhancer<T>);
constructor(appCreator: T, options: AppOptions, preloadedState: T, enhancer?: StoreEnhancer<T>);
Example:
const app = new ReduxApp(new App(), devToolsEnhancer(undefined));
Options
Component Options
You can supply the following options to the component
decorator.
class SchemaOptions {
actionNamespace?: boolean;
uppercaseActions?: boolean;
}
Usage:
@component({ uppercaseActions: false })
class Counter {
value = 0;
increment() {
this.value = this.value + 1;
}
}
Computed Options
class ComputedOptions {
deepComparison: boolean;
}
App Options
export class AppOptions {
name?: string;
updateState?: boolean;
}
Usage:
const app = new ReduxApp(new App(), { updateState: false }, devToolsEnhancer(undefined));
Global Options
Available global options:
class GlobalOptions {
logLevel: LogLevel;
emitClassNames: boolean;
convertToPlainObject?: boolean;
schema: SchemaOptions;
}
enum LogLevel {
None = 0,
Verbose = 1,
Debug = 2,
Warn = 5,
Silent = 10
}
Usage:
ReduxApp.options.logLevel = LogLevel.Debug;
Changelog
The change log can be found here.