Skip to main content

Base action with common logic

In Kiss, all actions must extend KissAction<State>. For example:

import { KissAction } from 'kiss-for-react';
import { State } from 'State';

class Increment extends KissAction<State> {
reduce() {
return this.state.increment();
}
}

In all the code I show in this documentation, you'll see I usually write extend Action instead of extend KissAction<State>.

This is because I'm assuming you have previously defined your own abstract base action class called simply Action, that itself extends KissAction<State>. Then, you may have all your actions extend this Action class instead.

This is how you would define the Action class:

import { KissAction } from 'kiss-for-react';
import { State } from 'State';

export abstract class Action extends KissAction<State> { }

Remember this is optional, but recommended. The reason to do this is twofold:

  • First, you'll avoid writing extends KissAction<State> in every action class. Now, you'll need to write extends Action instead, which is simpler.

  • And second, to have a common place to put any common logic that all your actions should have access to. More on that later.

Common logic

Suppose we have the following app state:

class State {
constructor(
public items: Item[] = [],
public selectedItem: Item | null = null
) {}
}

class Item {
constructor(public id: number, public text: string) {}
}

And then we have an action, which selects an item by id:

class SelectItem extends Action {
constructor(public id: number) { super(); }

reduce() {
let item = this.state.items.find(item => item.id === this.id);
if (item === undefined) throw new UserException('Item not found');
return this.state.copy({selectedItem: item});
}
}

You would use it like this:

var item = new Item(1, 'A'); 
dispatch(new SelectItem(item.id));

Now, suppose we have a lot of actions that need to access the items and selectedItem properties. We could add getters and selectors to the base Action class:

abstract class Action extends KissAction<State> {

// Getters shortcuts
get items(): Item[] { return this.state.items; }
get selectedItem(): Item | null { return this.state.selectedItem; }

// Selectors
findById(id: number): Item | undefined { return this.items.find(item => item.id === id); }
searchByText(text: string): Item | undefined { return this.items.find(item => item.text.includes(text)); }
get selectedIndex(): number { return this.selectedItem !== null ? this.items.indexOf(this.selectedItem) : -1; }
}

And then your actions have an easier time accessing the store's state:

class SelectItem extends Action {
constructor(public id: number) { super(); }

reduce() {
let item = this.findById(this.id); // Here!
if (item === undefined) throw new UserException('Item not found');
return this.state.copy({selectedItem: item});
}
}

The difference above is that, instead of writing:

let item = this.state.items.find(item => item.id === this.id); 

You can simply write:

let item = this.findById(this.id); 

It may seem a small reduction of boilerplate, but it adds up.

In practice, your base action class may end up containing a lot of elaborate "selector functions", which then can be used by all your actions.

The only requirement is that your actions now extend Action instead of KissAction<State>.

Typed getters for the dependencies​

The base action is also where you declare typed getters for the store's environment, dependencies and configuration, which are typed unknown in the store:

abstract class Action extends KissAction<State> {
get env(): Environment { return this.store.environment as Environment; }
get deps(): Dependencies { return this.store.dependencies as Dependencies; }
get config(): Config { return this.store.configuration as Config; }
}

Then, your actions can write this.deps.api.loadUser(id), without casts.