npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

event-reduce

v0.7.2

Published

State management based on reducing observable events into state

Downloads

140

Readme

⚡ event-reduce

A small, opinionated, state management library based on reducing observable events into state.

Why?

  • I like static typing.
  • I like how flux separates state and views.
  • I like how mobx makes derived state almost completely frictionless.
  • I particularly like being able to define a piece of state by how it changes.

How?

Start by installing the NPM package:

yarn add event-reduce

Create a model:

import { event, reduce } from 'event-reduce';

const incremented = event();
const decremented = event();

const counter = reduce(0)
    .on(incremented, count => count + 1)
    .on(decremented, count => count - 1);

Trigger events to update the model:

console.log(counter.value); // 0

incremented();
incremented();

console.log(counter.value); // 2

decremented();

console.log(counter.value); // 1

What if my application is more complicated than a single counter?

I'd recommend putting your events in their own class like this:

import { event, events } from 'event-reduce';

@events // Makes sure events are named well for debugging
class CounterEvents {
    counterAdded = event();
    incremented = event<{ counterIndex: string, amount: number }>();
}

and make a model class like this:

import { derived, reduce, reduced } from 'event-reduce';

@model  // Marks class as a model, enabling all the other decorated properties
class CounterModel {
    constructor (public events: CounterEvents) { }

    @reduced    // Marks property as reduced, so its value will update automatically
    counters = reduce([] as number[])
        .on(this.events.counterAdded, (counters) => counters.concat([0]))
        .on(this.events.incremented, (counters, { counterIndex, amount }) => counters
            .map((c, i) => i == counterIndex ? c + amount : c))
        .value; // Property will just be the value, rather than a Reduction object

    @derived   // Marks property as derived, which will cache its result until counters are changed
    get counterCount() {
        return this.counters.length;
    }
}

Note: event-reduce currently relies on "experimentalDecorators": true and "useDefineForClassFields": false being set in your tsconfig.json file. Standard ECMAScript field behaviour and decorator support is coming soon.

Then the model can be used like so:

let events = new CounterEvents();
let model = new CounterModel(events);

console.log(model.counters); // []
console.log(model.counterCount); // 0

events.counterAdded();
events.counterAdded();

console.log(model.counters); // [0, 0]
console.log(model.counterCount); // 2

events.incremented({ counterIndex: 0, amount: 1 });
events.incremented({ counterIndex: 1, amount: 2 });

console.log(model.counters); // [1, 2]

Chances are, your application is going to be even more complicated than even a list of counters 😁. You're going to need more model classes, and you're going to want to scope events to particular child models. I've provided a more complete example in the aptly-name example folder.

How do I connect this to React?

React integration is provided by a separate package, so start by installing that:

yarn add event-reduce-react

Hooks are provided for creating events, reductions, and derivations that persist between renders. Components that use reduced or derived properties should be wrapped in the reactive HOC, so that the component will re-render when the model changes.

import { reactive, useEvent, useReduced } from "event-reduce-react";

export const MyComponent = reactive(function MyComponent() {
    let increment = useEvent<number>();
    let count = useReduced(0)
        .on(increment, c => c + 1);

    return <div>
        Count: {count.value}
        <button onClick={() => increment()}>Increment</button>
    </div>
});

State can come from model objects instead of using hooks inside your components. The example application is implemented this way.

Can I persist the state of my models?

Yes! If you've got the appropriate decorators on your model properties, they can easily be saved and restored later.

import { state, reduced, reduce, derived, getState, setState } from "event-reduce";

@model
class MyModel {
    constructor(public events: CounterEvents, id: string) {
        this.id = id;
    }

    @state // Unobservable state that you want to save
    id: string;

    @reduced // Will be saved
    count = reduce(0)
        .on(this.events.incremented, c => c + 1)
        .value;

    @derived // Won't be saved, since it can be re-computed
    get countTimesTwo() {
        return this.count * 2;
    }
}

let model = new MyModel(new CounterEvents(), 'mine');

let state = getState(model); // { id: "mine", count: 0 }
setState(model, state); // Restore the saved state to the model

Often, reduced properties will contain more models. Their state will be saved and restored as part of the parent model's state. If you have a reduced collection of models though, you'll need to create new models from the restored state using the onRestore function. See the CounterListModel class in example/CounterList.tsx for an example.

How can I debug state changes?

The simplest way to get started is to enable logging:

import { enableLogging } from "event-reduce";

enableLogging(true);

Events and the reductions, derivations, and renders they trigger will be logged to the console, along with useful information such as the parameters passed in to the event, the previous and current values of reductions and derivations, and the sources of reductions, derivations, and renders. example console log

event-reduce also has support for time-travelling debugging with the Redux DevTools. Make sure you've set up your model for saving and restoring state, then simply register your top-level model to enable the integration.

import { enableDevTools } from "event-reduce";
import { myModel } from "./my-model"

enableDevTools(myModel, 'My model'); // name is optional

If you've got more than one top-level model, you'll need to register each of them separately.