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

eveline

v0.0.10

Published

☘️ Full-featured 1KB reactive state management ☘️

Downloads

49

Readme

  • 🚀 Reactive observable and computed values - just like MobX, Solid.js or Preact Signals
  • 👁 Transparency - no data glitches guaranteed
  • 🔄 Transactional updates - no unexpected side-effects
  • 🙈 Lazyness - nothing happens until you need a value
  • 🤓 Value-checked computeds - easily optimize your re-renders
  • ✅ Optimality - nothing can be done significantly better with all the guarantees
  • ⚙️ Customizable reaction scheduler for async flows
  • 🥏 Composable transparent models for convenient development
  • 💾 IE11 support - just ES6 Set is required
  • 💯 100% tests coverage with complex cases
  • ⭐️ Full TypeScript support
  • 📦 ...and all in 1KB package

Installation

npm install --save eveline
yarn add eveline

Table of contents

Basics

import { observable, computed, reaction, tx } from 'eveline';

// reactive observable value
const counter = observable(0);

// reactive computed value
const double = computed(() => counter.value * 2);

// side-effect (reaction)
const logger = reaction(() => {
  console.log(`Double value of ${counter.value} is ${double.value}`);
});

// prints "Double value of 1 is 2" and subscribes to observable/computed changes
logger.run();

counter.value = 2;  // Prints "Double value of 2 is 4", syncronously by default

// run modifications in transaction - will react to the latest value only
tx(() => {
  counter.value = 3;
  counter.value = 4;
});

// destroy reaction - no more logs after that
logger.destroy();

Value-checked observables and computeds

const check = Object.is;

// second argument to observable is value-check function (like Object.is)
const a = observable(5, check);
const b = observable(10, check);

// second argument for computed is value-check function too
const sum = computed(() => a.value + b.value, check);

// react to sum changes
reaction(() => {
  console.log(`Sum is ${sum.value}`);
}).run();

a.value = 5;  // value is the same - no reaction
a.value = 10; // logs "Sum is 20"
b.value = 20; // logs "Sum is 30"

tx(() => {
  a.value = 20;
  b.value = 10; // both values are changed, but sum did not change - no logs here
});

Models

Easily make observable values transparent and collocate related computeds and actions in one place

import { makeModel } from "eveline";
import { observer } from "eveline/react";

export const makeCounter = (initial) => {
  const self = makeModel({
    data: {
      count: initial
    },
    computed: {
      double() { return self.count * 2; }
    },
    actions: {
      inc() { self.count += 1; },
      dec() { self.count -= 1; }
    }
  });

  return self;
};

export const Counter = observer(({ model }) => {
  return (
    <>
      <button onClick={model.dec}>-</button>
      <button onClick={model.inc}>+</button>
      Double of {model.count} is {model.double}
    </>
  );
});

const counter = makeCounter(0);

ReactDOM.render(<Counter model={counter} />, document.body);

Class models

Like classes and OOP? No problems!

import { observable, computed, action, makeObservable } from 'eveline';

class CounterModel {
  count = observable.prop(0)
  double = computed.prop(() => this.count * 2)
  
  constructor() {
    makeObservable(this);
  }
  
  inc = action(() => {
    this.count += 1;
  })
  
  dec = action(() => {
    this.count -= 1;
  })
}

const counter = new CounterModel();

Async actions and custom schedulers

By default, all effects in Eveline are syncronous, so for async actions you need to wrap every syncronous block into a transaction:

import { action, utx } from 'eveline';

class Model {
  isFetching = observable.prop(false)
  data = observable.prop(null)
  error = observable.prop(null)
  
  fetchData = action(async () => {
    this.isFetching = true;
    
    try {
      const response = await fetch('someurl');
      const data = await response.json();
      
      utx(() => {
        this.isFetching = false;
        this.data = data;
      })
    } catch (err) {
      utx(() => {
        this.isFetching = false;
        this.error = err;
      })
    }
  })
}

Keeping writing utx(() => {}) for every syncronous block is quite cumbersome, so there is a simpler way - to change a reaction runner to microtask:

import { configure } from 'eveline';

configure({
  reactionRunner: (runner) => Promise.resolve().then(runner),
})

After that, all your observable changes will be automatically batched till the current microtask end, and reactions will run only after that. This enables writing transparent async code without worrying about sync/async blocks:

class Model {
  isFetching = observable.prop(false)
  data = observable.prop(null)
  error = observable.prop(null)
  
  fetchData = action(async () => {
    this.isFetching = true;
    
    try {
      const response = await fetch('someurl');
      this.data = await response.json();
    } catch (err) {
      this.error = err;
    } finally {
      this.isFetching = false;
    }
  })
}

React bindings

Eveline includes React bindings, ready for concurrent and Strict mode.

After wrapping a component in observer, it re-renders when any values it reads change.

import { observer } from 'eveline/react';

const Component = observer(({ model }) => {
  return <>Count is: {model.count}</>;
)}

Or alternatively, use useObserver inside of component to read reactive values:

import { useObserver } from 'eveline/react';

const Component = ({ model }) => {
  const [count, double] = useObserver(() => [model.count, model.double]);
  
  return <>Double of {count} is {double}</>;
)

For class components use observerClass:

class CounterComponent extends React.PureComponent {
  render() {
    const { model } = this.props;
    
    return <>Count is: {model.count}</>
  }
}

const Counter = observerClass(CounterComponent);

API

observable(value[, checkFn])

const checkFn = (prev: number, next: number) => prev === next;
const count = observable<number>(0, checkFn);

count.value;      // read value
count.value = 10; // write value

count.notify();   // notify about change without changing value;

count.$$observable === true;

computed(fn[, checkFn])

const checkFn = (prev: number, next: number) => prev === next;
const double = computed(() => count.value * 2, checkFn);

double.value;     // read value
double.destroy(); // unsubscribe from dependencies and free cached value

double.$$computed === true;

reaction(fn[, manager])

const log = reaction(() => {
  console.log('double is', double.value);
});

log.run();  // run reaction and subscribe to dependencies
log.run();  // run reaction again

log.run(1, 2, 3);  // pass arguments to reaction and return fn result

log.destroy();  // destroy reaction, unsubscribe from subscriptions

log.unsubscribe();  // unsubscribe from subscriptions, but keep them for future
log.subscribe();    // subscribe to stored subscriptions after unsubscribe

// manager usage
const asyncLog = reaction(
  () => console.log(double.value),
  () => setTimeout(asyncLog.run, 1000),
);

asyncLog.run(); // prints immediately

count.value = 100;  // prints after 1 second

tx(thunk)

Batch observable changes and run reactions only after last transaction end

const a = observable(10);
const b = observable(20);

tx(() => {
  a.value = 100;
  b.value = 200;
});

utx(fn)

The same as tx(thunk), but do not track reads of observables inside of thunk and return fn result.

const r = reaction(() => {
  a.value;
  console.log(utx(() => b.value));
});

r.run();  // r depends on a, but not on b

action(fn)

Returns a function wrapped in utx, that passes its argument to fn and returns its result

const inc = action((value) => {
  count.value += value;
})

inc(100);

makeModel(model)

makeModel(target, model)

Construct an observable model from model descriptor. If target parameter is given, all field declarations are done on it.

const model = makeModel({
  // data section is converted to observable fields with getters and setters
  // if a field is an observable, it's passed as is
  // the section can be a function returning an object
  data: {
    counter: 0,
    greeting: 'hello',
    increment: observable(0, checkFn),
  },
  // computed section is converted to getters
  // if a field is a computed, it's passed as is
  computed: {
    double() {
      return model.count * 2;
    },
    doubleGreeting: computed(() => {
      return model.greeting + ' ' + model.greeting;
    }, checkFn),
  },
  // actions section wraps every function to action
  actions: {
    inc() {
      model.counter += model.increment;
    },
    setGreeting(greeting: string) {
      model.greeting = greeting;
    },
  },
  // extra section is assigned to result as is
  extra: {
    id: uuid(),
  }
}

model.counter;  // returns 0
model.greeting; // returns 'hello'

model.counter = 100;

model.double; // returns 200

model.inc();  // perform action
model.setGreeting('hola');

model.id; // extra fields are accessable as is

model.$counter; // returns observable instance
model.$double;  // returns computed instance

makeObservable(instance)

makeObservable(target, instance)

Convert all observable and computed fields on instance to getters/setters. If target parameter is given, all declarations are done on it.

class Model {
  // use .prop to make Typescript think it's already a number, not observable
  counter = observable.prop(0);
  
  // the same for computed.prop
  double = computed.prop(() => this.counter * 2)
  
  constructor() {
    makeObservable(this);
  }
  
  inc = action(() => {
    this.counter += 1;
  })
}

const model = new Model();

model.counter;  // returns 0
model.counter = 100;

model.double;   // returns 200

model.inc();    // perform action

configure(config)

const defaultConfig = {
  // default reaction runner, runs the `runner` fn immediately
  // see above for microtask runner for convenient async operations
  reactionRunner: (runner) => runner(),
  // cacheOnUntrackedRead allows to make computed values not to cache the result
  // when they are read in untracked context (utx, action) or without it
  // default value is true, but for real applications it's better to turn it off to prevent memory leaks
  cacheOnUntrackedRead: true,
}

configure(defaultConfig),