Skip to content

Commit

Permalink
# 4.6.0 2022-06-27
Browse files Browse the repository at this point in the history
* [design] add [strict](/api?id=strict), [act](/api?id=act) API.
  • Loading branch information
wangyi committed Jun 27, 2022
1 parent e1fc959 commit fd950f0
Show file tree
Hide file tree
Showing 25 changed files with 3,321 additions and 1,948 deletions.
328 changes: 185 additions & 143 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,183 +10,225 @@

`agent-reducer` is a model container for Javascript apps.

It helps you write applications with a micro `mvvm` pattern and provides a great developer experience.

You can use `agent-reducer` together with [React](https://reactjs.org), [Redux](https://redux.js.org), or with any other view library.
It helps you write applications with a micro `mvvm` pattern and provides a great developer experience, you can see details [here](https://filefoxper.github.io/agent-reducer/#/).

## Other language

[中文](https://github.com/filefoxper/agent-reducer/blob/master/README_zh.md)

## Compare with reducer

`agent-reducer` is desgined for splitting reducer function to smaller parts for different action types. And we found that `class` is a appropriate pattern for action splitting. So, the model pattern for `agent-reducer` looks like a class with a `state` property, and some reducer like methods.
## Basic usage

With the comparison between `reducer usage` and `agent-reducer usage`, you will have a first impression about what the model looks like.
Let's have some examples to learn how to use it.

The comparison is built on [React hooks](https://reactjs.org/docs/hooks-intro.html) ecosystem.
The example below is a counter, we can increase or decrease the state.

```typescript
import {Model} from "agent-reducer";
import {useReducer} from 'react';
import {useAgentReducer} from 'use-agent-reducer';

interface Action {
type?: 'stepUp' | 'stepDown' | 'step' | 'sum',
payload?: number[] | boolean
}

/**
* reducer description
* @param state last state
* @param action object as params
*/
const countReducer = (state: number = 0, action: Action = {}): number => {
switch (action.type) {
case "stepDown":
return state - 1;
case "stepUp":
return state + 1;
case "step":
return state + (action.payload ? 1 : -1);
case "sum":
return state + (Array.isArray(action.payload) ?
action.payload : []).reduce((r, c): number => r + c, 0);
default:
return state;
import {
effect,
Flows,
create,
act,
strict,
flow,
Model
} from "agent-reducer";

describe("basic", () => {
// a class model template for managing a state
class Counter implements Model<number> {
// state of this model
state: number = 0;

// a method for generating a next state
increase() {
// keyword `this` represents model instance, like: new Counter()
return this.state + 1;
}
}

/**
* model description
*/
class Counter implements Model<number> {
// current state
state = 0;

stepUp = (): number => this.state + 1;

stepDown = (): number => this.state - 1;

step(isUp: boolean):number{
return isUp ? this.stepUp() : this.stepDown();
}
// free to set params
sum(...counts: number[]): number {
return this.state + counts.reduce((r, c): number => r + c, 0);
decrease() {
const nextState = this.state - 1;
if (nextState < 0) {
// use another method for help
return this.reset();
}
return nextState;
}

}

......

// reducer tool
const [ state, dispatch ] = useReducer(countReducer,0);
reset() {
return 0;
}
}

test("call method from agent can change state", () => {
// 'agent' is an avatar object from model class,
// call method from 'agent' can lead a state change
const { agent, connect, disconnect } = create(Counter);
connect();
// 'increase' method is from 'agent',
// and returns a new state for model.
agent.increase();
// model state is changed to 1
// We call these state change methods 'action methods'.
expect(agent.state).toBe(1);
disconnect();
});

test("only the method get from agent object directly, can change state", () => {
const actionTypes: string[] = [];
const { agent, connect, disconnect } = create(Counter);
connect(({ type }) => {
// record action type, when state is changed
actionTypes.push(type);
});
// 'decrease' method is from 'agent',
// and returns a new state for model.
agent.decrease();
// model state is changed to 0
expect(agent.state).toBe(0);
// the 'reset' method called in 'decrease' method,
// it is not from 'agent',
// so, it can not lead a state change itself,
// and it is not an action method in this case.
expect(actionTypes).toEqual(["decrease"]);
disconnect();
});
});

// define sum callback
const handleSum = (...additions:number[]) => {
dispatch({type:'sum',payload:additions});
};

// agent-reducer
const {
state:agentState,
// sum method reference
stepUp:handleAgentSum
} = useAgentReducer(Counter);
// do not worry about the keyword `this`
// in method `handleAgentSum` from an `agent object`,
// it is always bind to your model instance,
// which is created or enhanced by agent-reducer.

......

```

Like any other independent libraries, `agent-reducer` needs connectors for working with a view library. If you are working with React, we recommend [use-agent-reducer](https://www.npmjs.com/package/use-agent-reducer) as its connector.
The operation is simple:

## Basic usage
1. create `agent` object
2. connect
3. call method from `agent` object
4. the method called yet can use what it `returns` to change model state (this step is automatic)
5. disconnect

```typescript
import {
MiddleWarePresets,
create,
middleWare,
Model
} from 'agent-reducer';

describe('basic usage',()=>{
It works like a redux reducer, that is why it names `agent-reducer`.

// this is a counter model,
// we can increase or decrease its state
class Counter implements Model<number> {
Let's see a more complex example, and we will use it to manage a filterable list actions.

state = 0; // initial state

// consider what the method returns as a next state for model
stepUp = (): number => this.state + 1;

stepDown = (): number => this.state - 1;

step(isUp: boolean):number{
return isUp ? this.stepUp() : this.stepDown();
}

// if you want to take a promise resolved data as next state,
// you can add a middleWare.
@middleWare(MiddleWarePresets.takePromiseResolve())
async sumByAsync(): Promise<number> {
const counts = await Promise.resolve([1,2,3]);
return counts.reduce((r, c): number => r + c, 0);
}
```typescript
import {
effect,
Flows,
create,
act,
strict,
flow,
Model
} from "agent-reducer";

describe("use flow", () => {
type State = {
sourceList: string[];
viewList: string[];
keyword: string;
};

const remoteSourceList = ["1", "2", "3", "4", "5"];

class List implements Model<State> {
state: State = {
sourceList: [],
viewList: [],
keyword: "",
};

// for changing sourceList,
// which will be used for filtering viewList
private changeSourceList(sourceList: string[]): State {
return { ...this.state, sourceList};
}

// for changing viewList
private changeViewList(viewList: string[]): State {
return { ...this.state, viewList };
}

test('by default, a method result should be the next state',()=>{
// use create api, you can create an `Agent` object from its `Model`
const {agent,connect,disconnect} = create(Counter);
// before call the methods,
// you need to connect it first
connect();
// calling result which is returned by method `stepUp` will be next state
agent.stepUp();
// if there is no more work for `Agent`,
// you should disconnect it.
disconnect();
expect(agent.state).toBe(1);
});
// for changing keyword,
// which will be used for filtering viewList
changeKeyword(keyword: string): State {
return { ...this.state, keyword };
}

test('If you want to take a promise resolve data as next state, you should use MiddleWare',async ()=>{
// use create api, you can create an `Agent` object from its `Model`
const {agent,connect,disconnect} = create(Counter);
// before call the methods,
// you need to connect it first
connect();
// calling result which is returned by method `sumByAsync`
// will be reproduced by `MiddleWarePresets.takePromiseResolve()`,
// then this MiddleWare will take the promise resolved value as next state
await agent.sumByAsync();
// if there is no more work for `Agent`,
// you should disconnect it.
disconnect();
expect(agent.state).toBe(6);
});
// fetch remote sourceList
// `flow` decorator can make a flow method,
// in flow method, keyword `this` is an agent object,
// so, you can call action method to change state.
@flow()
async fetchSourceList() {
// fetch remote sourceList
const sourceList = await Promise.resolve(remoteSourceList);
// keyword `this` represents an agent object in flow method,
// `changeSourceList` is from this agent object,
// and it is marked as an action method,
// so, it can change state.
this.changeSourceList(sourceList);
}

// effect of action methods: changeSourceList, changeKeyword for filtering viewList.
// `effect` decorator makes an effect method,
// the effect method can be used for listening the state change from action methods.
// effect method is a special flow method, it can not be called manually.
// We can add a flow mode by using `flow` decorator with effect,
// now, we have told the effect method works in a debounce mode with 100 ms
@flow(Flows.debounce(100))
@effect(() => [
// listen to action method `changeSourceList`
List.prototype.changeSourceList,
// listen to action method `changeKeyword`
List.prototype.changeKeyword,
])
private effectForFilterViewList() {
const { sourceList, keyword } = this.state;
// filter out the viewList
const viewList = sourceList.filter((content) =>
content.includes(keyword.trim())
);
// use action method `changeViewList` to change state
this.changeViewList(viewList);
}
}

test("flow method is used for composing action methods together to resolve more complex works", async () => {
const { agent, connect, disconnect } = create(List);
connect();
// use flow to fetch remote sourceList
await agent.fetchSourceList();
expect(agent.state.sourceList).toEqual(remoteSourceList);
disconnect();
});

test('effect method can listen to the state change of action methods',async ()=>{
const { agent, connect, disconnect } = create(List);
connect();
// use flow to fetch remote sourceList
await agent.fetchSourceList();
// change sourceList, the effect method `effectForFilterViewList` will start after 100 ms
expect(agent.state.sourceList).toEqual(remoteSourceList);
// change keyword,
// the effect method `effectForFilterViewList` will cancel itself,
// then start after 100 ms
agent.changeKeyword('1');
await new Promise((r)=>setTimeout(r,110));
// effect `effectForFilterViewList` filter out the viewList
expect(agent.state.sourceList).toEqual(remoteSourceList);
expect(agent.state.viewList).toEqual(['1']);
disconnect();
})
});

```

`agent-reducer` provides a rich MiddleWare ecosystem, you can pick appropriate MiddleWares from MiddleWarePresets, and add them to your method by using api `middleWare, withMiddleWare, agentOf` or `create` directly. You can also write and use your own `MiddleWare` to our system too.
The example above uses decorators like `@flow` and `@effect` to make a list manage model, which can fetch list from remote service and filter by keywords.

## Share state change synchronously

`agent-reducer` stores state, caches, listeners in the model instance, so you can share state change synchronously between two or more agent objects by using the same model instance.
`agent-reducer` stores state, caches, listeners in the model instance, so you can share state change synchronously between two or more different agent objects from the same model instance.

```typescript
import {
create,
middleWare,
MiddleWarePresets,
Action,
Model
} from 'agent-reducer';
Expand Down Expand Up @@ -245,7 +287,7 @@ describe('update by observing another agent',()=>{

```

The previous example may not easy for understanding, but consider if we use this feature in a view library like React, we can update state synchronously between different components without `props` or `context`, and these components will rerender synchronously. You can use it easily with its React connnector [use-agent-reducer](https://www.npmjs.com/package/use-agent-reducer).
This example may not easy for understanding, but consider if we use this feature in a view library like React, we can update state synchronously between different components without `props` or `context`, and these components will rerender synchronously. You can use it easily with its React connnector [use-agent-reducer](https://www.npmjs.com/package/use-agent-reducer).

## Connector

Expand Down

0 comments on commit fd950f0

Please sign in to comment.