Comparing version 0.2.1 to 0.3.0
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var defineAction_1 = require("./defineAction"); | ||
Object.defineProperty(exports, "defineAction", { enumerable: true, get: function () { return defineAction_1.defineAction; } }); | ||
var effects_1 = require("./effects"); | ||
Object.defineProperty(exports, "off", { enumerable: true, get: function () { return effects_1.off; } }); | ||
Object.defineProperty(exports, "on", { enumerable: true, get: function () { return effects_1.on; } }); | ||
Object.defineProperty(exports, "once", { enumerable: true, get: function () { return effects_1.once; } }); | ||
Object.defineProperty(exports, "put", { enumerable: true, get: function () { return effects_1.put; } }); | ||
Object.defineProperty(exports, "take", { enumerable: true, get: function () { return effects_1.take; } }); | ||
var Emitter_1 = require("./Emitter"); | ||
Object.defineProperty(exports, "Emitter", { enumerable: true, get: function () { return Emitter_1.Emitter; } }); | ||
var functions_1 = require("./functions"); | ||
Object.defineProperty(exports, "defineAction", { enumerable: true, get: function () { return functions_1.defineAction; } }); | ||
Object.defineProperty(exports, "off", { enumerable: true, get: function () { return functions_1.off; } }); | ||
Object.defineProperty(exports, "on", { enumerable: true, get: function () { return functions_1.on; } }); | ||
Object.defineProperty(exports, "once", { enumerable: true, get: function () { return functions_1.once; } }); | ||
Object.defineProperty(exports, "take", { enumerable: true, get: function () { return functions_1.take; } }); | ||
var ViewStore_1 = require("./ViewStore"); | ||
Object.defineProperty(exports, "ViewStore", { enumerable: true, get: function () { return ViewStore_1.ViewStore; } }); | ||
var Store_1 = require("./Store"); | ||
Object.defineProperty(exports, "Store", { enumerable: true, get: function () { return Store_1.Store; } }); |
{ | ||
"name": "tinysaga", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"main": "lib/index.js", | ||
@@ -5,0 +5,0 @@ "module": "esm/index.js", |
[](https://app.circleci.com/pipelines/github/crazytoucan/tinysaga?branch=master) | ||
[](https://www.npmjs.com/package/tinysaga) | ||
# tinysaga | ||
Simple state update infrastructure for modern React apps. | ||
Allows both copy-on-write stores as used by React-Redux and mutable stores for computationally intensive desktop applications. | ||
Simple state update pattern for modern React apps. | ||
Allows for updating both immutable state as used by React-Redux and mutable state for computationally intensive desktop applications. | ||
No generators. | ||
More documentation coming soon. | ||
## Installation | ||
```sh | ||
$ yarn add tinysaga | ||
``` | ||
Comes with TypeScript typings out of the box, as well as ESM support and tree shaking. | ||
## Motivation | ||
Redux-Saga is a great library which allows applications to manage the complex state updates and side effects that happen in many React-Redux applications. It is a crucial part of the current frontend software stack. | ||
However, the library itself is quite complex, which can cause problems in the following ways: | ||
1. Developers onboarding into a Saga-based codebase may need to understand the Saga fork and threading model pretty deeply in order to contribute to the code. This creates cognitive overhead for even the best developers. | ||
2. The effect model can be surprising to unravel. For example, sending a callback to external code becomes a lot more complex as developers need to reason about how to invoke effects from within the callback. | ||
3. The syntax itself can be challenging and unfamiliar, such as `yield` and `yield*`, or why you would want to `yield take` inside of a `while (true)` loop. | ||
4. The code can be very hard to debug, especially in modern compile-to-ES6 toolchains where the generators become obfuscated. Stepping through code can be a chore, and profiling code through many layers of effect realization can be a nightmare. | ||
Tinysaga supports most of the common `put()`, `takeEvery()`, and `take()` workflows from Redux-Saga, leaving only the most complex uses (like debouncing, throttling, and cancellation) to other sophisticated libraries using plain old JavaScript. | ||
## Design | ||
At its core, Tinysaga is really just an event bus that integrates into React-Redux. | ||
- Dispatched Actions from React-Redux (or from other external sources, like a WebSocket or ticking timer) are sent to the Tinysaga `Emitter`. | ||
- The `Emitter` is wired up with handlers for each Action `type` in your application. Those handlers are free to do whatever they want, such as reducing a Store's state, changing any mutable state you have, or dispatching other actions. | ||
There are a set of helpful Effects that Tinysaga exports, such as `take()` and `once()` which allow you to compose the low-level Emitter primitive into more powerful constructs. No generators involved. | ||
## Examples | ||
### Making a network call | ||
```ts | ||
const FetchUser = defineAction<{ userId: string }>("FetchUser"); | ||
const FetchUserSuccess = defineAction<{ data: IUserData }>("FetchUserSuccess"); | ||
const FetchUserFailed = defineAction<{ message: string }>("FetchUserFailed"); | ||
on(emitter, FetchUser.TYPE, async ({ userId }) => { | ||
// Note: `userId` and all of these calls are type-aware! No saga ReturnType shenanigans | ||
try { | ||
const data = await Api.fetchUser(userId); | ||
put(emitter, FetchUserSuccess({ data })); | ||
} catch (e) { | ||
put(emitter, FetchUserFailed({ message: e.message })); | ||
} | ||
}); | ||
``` | ||
### Closing a popover unless the user mouses into it or its anchor | ||
```ts | ||
const DismissPopover = defineAction("DismissPopover"); | ||
const PopoverAnchorEnter = defineAction("PopoverAnchorEnter"); | ||
const PopoverEnter = defineAction("PopoverEnter"); | ||
function popoverHandler(emitter: IEmitter) { | ||
const debouncedHide = lodash.debounce(() => { | ||
store.setState({ ...store.state, popover: undefined }); | ||
}, 500); | ||
on(emitter, DismissPopover.TYPE, () => { | ||
debouncedHide(); | ||
}); | ||
on(emitter, PopoverAnchorEnter.TYPE, () => { | ||
debouncedHide.cancel(); | ||
}); | ||
on(emitter, PopoverEnter.TYPE, () => { | ||
debouncedHide.cancel(); | ||
}); | ||
} | ||
``` |
@@ -14,3 +14,3 @@ import { IAction, IActionType, IEmitter } from "./types"; | ||
public emit({ type, payload }: IAction) { | ||
public put({ type, payload }: IAction) { | ||
const next: IDispatchNode = { type, payload, next: null }; | ||
@@ -17,0 +17,0 @@ if (this.end !== null) { |
@@ -0,4 +1,5 @@ | ||
export { defineAction } from "./defineAction"; | ||
export { off, on, once, put, take } from "./effects"; | ||
export { Emitter } from "./Emitter"; | ||
export { defineAction, off, on, once, take } from "./functions"; | ||
export { IAction, IActionDefinition, IActionType, IEmitter, IViewStore } from "./types"; | ||
export { ViewStore } from "./ViewStore"; | ||
export { Store } from "./Store"; | ||
export { IAction, IActionDefinition, IActionType, IEmitter, IStore } from "./types"; |
@@ -21,3 +21,3 @@ export interface IAction { | ||
export interface IEmitter { | ||
emit(action: IAction): void; | ||
put(action: IAction): void; | ||
on<T>(type: IActionType<T>, handler: (payload: T) => void): void; | ||
@@ -27,3 +27,3 @@ off<T>(type: IActionType<T>, handler: (payload: T) => void): void; | ||
export interface IViewStore<S> { | ||
export interface IStore<S> { | ||
readonly state: S; | ||
@@ -30,0 +30,0 @@ |
@@ -28,3 +28,7 @@ interface IHandler { | ||
emit(payload: any) { | ||
public emit(payload: any) { | ||
if (this.dispatching) { | ||
throw new Error("emit() when already emitting"); | ||
} | ||
this.dispatching = true; | ||
@@ -43,3 +47,3 @@ const chain = this.chain; | ||
add(handler: IHandler) { | ||
public add(handler: IHandler) { | ||
if (this.dispatching) { | ||
@@ -56,3 +60,3 @@ if (this.nextChain === null) { | ||
remove(handler: IHandler) { | ||
public remove(handler: IHandler) { | ||
if (this.dispatching) { | ||
@@ -59,0 +63,0 @@ if (this.nextChain === null) { |
@@ -8,4 +8,5 @@ { | ||
"callable-types": false, | ||
"prefer-for-of": false | ||
"prefer-for-of": false, | ||
"member-access": true | ||
} | ||
} |
14197
16
289
85