redux-classic
Type-safe, DRY and OO redux. Implemented with typescript.
Change Log
This is a clone of redux-app and is planned to be the main repo
Installation
yarn add redux-classic
or
npm install --save redux-classic
Short Example
class App {
counter = new Counter();
}
class Counter {
value = 0;
@action
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 and React, can be found here redux-classic-examples.
How it works
For each 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).
To make it easier to debug, each generated component name follows the following pattern: OriginalClassName_ReduxAppComponent. If while debugging you don't see the _ReduxAppComponent suffix it means the class was not replaced by an underlying component and is probably lacking a decorator (@action or @sequence).
Reading the source tip #1: There are two main classes in redux-classic. The first is ReduxApp and the second is Component.
Documentation
Stay Pure
Although redux-classic 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.
Async Actions
Async actions (thunks, sagas, epics...) and side effects are handled in redux-classic by using the sequence
decorator.
What it does is to tell redux-classic that the decorated method acts (almost) as a plain old javascript method. We say almost since while the method body is executed regularly it still dispatches an action so it's still easy to track and log.
Remember:
- Don't change the state inside
sequence
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-classic-examples page
class MyComponent {
@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);
}
@action
public setStatus(newStatus: string) {
this.status = newStatus;
}
}
Multiple Components of the Same Type
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-classic-examples page
export class App {
@withId('SyncMe')
public counter1 = new Counter();
@withId('SyncMe')
public counter2 = new Counter();
@withId(123)
public counter3 = new Counter();
@withId()
public counter4 = new Counter();
}
Connect to a view
You can leverage the following ReduxApp static method to connect your state components to your view:
ReduxApp.getComponent(componentType, componentId?, appId?)
You can use IDs to retrieve a specific component or omit the ID to get the first instance that redux-classic finds.
React
working example can be found on the redux-classic-examples page
Use the snippet below to create an autoSync
function. You can then use it as you would normally use react-redux's connect
:
const MyReactCounter: React.SFC<Counter> = (props) => (
<div>
<span>Value: {props.value}</span>
<button onClick={props.increment}>Increment</button>
</div>
);
const synced = autoSync(Counter)(MyReactCounter);
export { synced as MyReactComponent };
The autoSync
snippet:
import { connect } from 'react-redux';
import { Constructor, getMethods, ReduxApp } from 'redux-classic';
export function autoSync<T>(stateType: Constructor<T>) {
return connect<T>(() => {
const comp = ReduxApp.getComponent(stateType);
const compMethods = getMethods(comp, true);
return Object.assign({}, comp, compMethods);
});
}
Angular and others
working example can be found on the redux-classic-examples page
With Angular and similar frameworks (like Aurelia) it's as easy as:
class MyCounterView {
public myCounterReference = ReduxApp.getComponent(Counter);
}
Computed Values
To calculate values from other parts of the components state instead of using a fancy selector function you can simply use a standard javascript getter.
Remember: As everything else, getters should be pure and should not mutate the state.
Example:
working example can be found on the redux-classic-examples page
class ComputedGreeter {
public name: string;
public get welcomeString(): string {
return 'Hello ' + this.name;
}
@action
public setName(newVal: string) {
this.name = newVal;
}
}
Ignoring Parts of the State
You can use the ignoreState
decorator to prevent particular properties of your components to be stored in the store.
Example:
class MyComponent {
public storeMe = 'hello';
@ignoreState
public ignoreMe = 'not stored';
@action
public changeState() {
this.storeMe = 'I am 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:
class MyComponent {
@action
public someAction() {
}
}
if (!(obj instanceof MyComponent))
throw new Error("Invalid argument. Expected instance of MyComponent");
Luckily redux-classic supplies a utility method called isInstanceOf
which you can use instead:
class MyComponent {
@action
public someAction() {
}
}
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));
App Options
export class AppOptions {
name?: string;
updateState?: boolean;
}
Usage:
const app = new ReduxApp(new App(), { updateState: false }, devToolsEnhancer(undefined));
Global Options
class GlobalOptions {
logLevel: LogLevel;
action: ActionOptions;
}
enum LogLevel {
None = 0,
Verbose = 1,
Debug = 2,
Warn = 5,
Silent = 10
}
class ActionOptions {
actionNamespace?: boolean;
actionNamespaceSeparator?: string;
uppercaseActions?: boolean;
}
Usage:
ReduxApp.options.logLevel = LogLevel.Debug;
ReduxApp.options.action.uppercaseActions = true;
Changelog
The change log can be found here.