summaryrefslogtreecommitdiff
path: root/README.md
blob: 45563cea2b8e48e603600f7cc780baff9ac53b03 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# ShapeX

Create scalable event-driven applications with ShapeX, inspired by [re-frame](https://github.com/day8/re-frame/). ShapeX uses zero dependencies and is runtime agnostic, meaning that you can use it in Node, Deno, Bun, browsers, or really anywhere where JavaScript runs.

## Example application

This is an example application that demonstrates how to use the ShapeX library. It has a single starting point event called `request`, which returns an updated state, which changes the `counter`. When that state changes, the subscriber for the `counter` state fires.

```typescript
import ShapeX from "@shapex/shapex";

type AppState = {
  counter: number;
};

const app = ShapeX<AppState>({
  counter: 1,
});

app.subscribe("$.counter", (state) => {
  console.log("counter changed", state);

  return {
    state,
  };
});

app.subscribe("request", (state) => {
  return {
    state: {
      ...state,
      counter: state.counter + 1;
    }
  }
});

// Dispatch an event somewhere.
app.dispatch("request");
```

## Installation

[ShapeX is available via JSR](https://jsr.io/@shapex/shapex), so check that out for the installation instructions for any given runtime.

## Documentation

### State

At the core of your application is state. You start by initiating ShapeX with some initial state, like so:

```typescript
import ShapeX from "@shapex/shapex";

type AppState = {
  counter: number;
};

const app = ShapeX<AppState>({
  counter: 1,
});
```

You can model your `AppState` however you like. It does not have to be called `AppState`.

### Events

Events set things in motion. You can dispatch events like so:

```typescript
app.dispatch("some-event-name");
```

And, if there's a subscription for that event name, that subscription will then fire. The above example is a data-less event, but you can also dispatch events with data, like so:

```typescript
app.dispatch("some-event-name", arg1, arg2, arg3);
```

### Subscriptions

Subscriptions listen to events or changes to state. Each subscription must return a `SubscriptionResponse` object, which looks like this:

```typescript
{
  state: T, // state is required
  dispatch: {
    eventName: "event-to-dispatch",
    args: [arg1, arg2] // args are optional
  } // dispatch is optional
}
```

#### Event subscriptions

You can listen to events like so:

```typescript
app.subscribe(
  "some-event-name",
  (state, arg1: string, arg2: string, arg3: string) => {
    return {
      state,
    };
  }
);
```

Each subscription has a callback function which gets passed to it the app state and whatever data was passed
when the event was dispatched. Subscription callbacks must return an `Response` which consists of updated state and/or further event dispatches. If you don't want to update state, just return the same state that the callback got in the first place.

#### State change subscriptions

You can also listen to state changes with subscriptions, which will fire when the listened state changes. You can listen to state changes like so:

```typescript
app.subscribe("$.counter", (state) => {
  return {
    state,
  };
});
```

Notable difference here is the `$.` prefix in the subscription listener name, which tells ShapeX what state to look for. Here `$.counter` will look for the root-level `counter` key in state. To look for nested state, simply add a dot (`.`) followed by the key name, i.e: `$.counter.nestedKey`. Additionally, state change subscriptions do not get any additional data passed to them, only state.

#### Subscribe only once

If you want to subscribe to an event or state change only once, you can use the `subscribeOnce` method. This method works similarly to `subscribe`, but it will automatically unsubscribe after the first event or state change.

```typescript
app.subscribeOnce("$.counter", (state) => {
  return {
    state,
  };
});
```

#### Unsubscribe

If you want to unsubscribe from an event or state change, you can use the `unsubscribe` method. This method takes the event or state change name as its argument and removes the subscription.

```typescript
app.unsubscribe("counter++");
```

#### Change state

You can change state by returning a new state object, like so:

```typescript
app.subscribe("counter++", (state) => {
  return {
    state: {
      ...state,
      counter: state.counter + 1,
    },
  };
});
```

#### Dispatch events from subscriptions

You can also dispatch events from within subscriptions, like so:

```typescript
app.subscribe("counter++", (state) => {
  return {
    state: {
      ...state,
      counter: state.counter + 1,
    },
  };
});

app.subscribe("some-event-name", (state) => {
  return {
    state,
    dispatch: {
      event: "counter++",
    },
  };
});
```

Now if `some-event-name` is dispatched, it also dispatches `counter++`. You can also pass data along, like so:

```typescript
app.subscribe("counter-increase", (state, increase: number) => {
  return {
    state: {
      ...state,
      counter: state.counter + increase,
    },
  };
});

app.subscribe("some-event-name", (state) => {
  return {
    state,
    dispatch: {
      event: "counter-increase",
      args: [5],
    },
  };
});
```

So now if `some-event-name` is dispatched, it also dispatches `counter-increase` with an increase of 5.

#### Get the subscription count

If you want to get the number of subscriptions for a specific event or state change, you can use the `subscriptionCount` method. This method takes the event or state change name as its argument and returns the number of subscriptions.

```typescript
// State change subscriptions
app.subscriptionCount("$.counter");

// Event subscriptions
app.subscriptionCount("some-event-name");
```

#### Get all subscriptions

If you want to get all subscriptions, you can use the `subscriptions` method. This method returns an array of all the subscription names.

```typescript
app.subscriptions();
```

#### Get current app state

If you want to get the current state of the app, you can use the `state` method. This method returns the current state of the app.

```typescript
app.state();
```