Code Monkey home page Code Monkey logo

wana's Introduction

wana

npm Build status codecov Bundle size Code style: Prettier Donate

Observable state with ease. ⚡️

Bring your React components to the next level. ⚛️

  • Transparent proxies (no special classes)
  • Implicit observation (use your objects like normal)
  • Observable objects, arrays, sets, and maps (even custom classes)
  • Automatic reactions to observable changes (see the auto/useAuto/withAuto functions)
  • Support for deep observation (see the watch function)
  • Memoized derivations (see the o/useDerived functions)
  • Prevent unnecessary renders
  • 80% less SLOC than MobX

 

Why build this? The goal of this library is to explore the MobX approach of writing React components by designing a new API from the ground up with React in mind from the get-go. Another goal is to keep a lean core by writing an observability engine from scratch.

Who built this? Alec Larson, the co-author of react-spring and immer. You can support his work by becoming a patron.

 

Exports

  • o() for making observable objects
  • auto() for reactive effects
  • when() for reactive promises
  • no() for unobserved objects
  • noto() for unobserved scopes
  • watch() for listening to deep changes
  • shallowChanges() for listening to shallow changes
  • withAuto() for reactive components
  • useAuto() for easy auto calls in components
  • useO() for observable component state
  • useDerived() for observable getters
  • useChanges() for change listeners
  • useEffects() for reactive mounting/unmounting of effects
  • useBinding() for situations where withAuto is too invasive

The API reference can be found here:
https://github.com/alloc/wana/wiki/API-Reference

Many of wana's exports are tree-shakeable. 🌲

 

Babel Plugins

  • @wana/babel-plugin-with-auto
    For development only. It ensures that withAuto components appear in the "component stack" printed by React when an error is thrown while rendering. This makes debugging a lot easier, but also inflates the size of your application. This plugin produces broken code when used on a production bundle, because it relies on an API that exists only in development.

  • @wana/babel-plugin-add-react-displayname
    A fork of babel-plugin-add-react-displayname that works with Babel 7 and up. It also provides a callees option, which means HOCs like withAuto are supported. Basically, this plugin sets the displayName of your components for you, which makes React Devtools a better experience. It's recommended to use this plugin in both development and production.

wana's People

Contributors

aleclarson avatar ryanking1809 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

wana's Issues

Safari - TypeError: Map operation called on non-Map object

Hey Alec, have you hit this error in Safari (13.1) before?
TypeError: Map operation called on non-Map object

I'm still trying to find a replicable example. It works flawlessly in Chrome and Firefox, and every time I think I pinpoint the issue, it pops up somewhere else with a very different stack trace.

I'll keep trying to figure out what's going on, but thought I'd touch base to see if you've come across it? I have a feeling it's those Observable / AutoObservable classes we set up before - doesn't help that Safari's dev tools are a nightmare to use.

Delay when reading observable getters

Is there are way to prevent the delay which occurs when firing observable getters? It seems to only occur when creating a getter on a getter.

const test = o({num: 1});
const testGet1 = o(() => "Hello".repeat(test.num));
const testGet2 = o(() => testGet1().repeat(2));
auto(() => {
	console.log("testGet2 changed", testGet2());
});

console.log("tGet2", testGet2());
test.num = 2;
console.log("tGet2", testGet2());

// -> returns
// testGet2 changed HelloHello ---> I'm not sure this should fire?
// tGet2 HelloHello
// tGet2 HelloHello ---> expecting -> HelloHelloHelloHello
// ...
// testGet2 changed HelloHelloHelloHello ---> is happening well after change occured

This however, works at the expected speed:

console.log("tGet2", testGet1(), testGet2());
test.num = 2;
console.log("tGet2", testGet1(), testGet2());

// -> returns expected values
// testGet2 changed HelloHello
// tGet2 Hello HelloHello
// tGet2 HelloHello HelloHelloHelloHello
// testGet2 changed HelloHelloHelloHello

Is there something I'm missing?

Prepare for concurrent mode

In concurrent mode, renders can be discarded, which means they'll never be committed to the DOM (or whatever native host). There are potentially other issues as well (see here).

Currently, the withAuto and useAuto functions cause side effects during render, which means memory leaks will occur in concurrent mode.

Event history

Hi Alec, does wana have a central event system I can spy on like with mobx? https://mobx.js.org/refguide/spy.html

I need to build an undo system and it would be nice to just filter the existing observable events if possible. It's not a huge deal putting in the extra labour otherwise.

ps. I tried switching to mobx to test this out and all my animations became slow and choppy. Maybe it's my bad code but wana is significantly faster, good work!

Watch a single property without "auto"

Is there a way to apply watch just to specific object key rather than the whole object?
I have a large observable state, and need to trigger some side effect when certain properties change. The side effect doesn't use the object I'm watching so I can't use auto, and watch on the whole state will cause the side effect to fire unnecessarily.

A simple example below, is it possible to log "side effect" to the console when count changes but not when number changes?
https://codesandbox.io/s/serene-frog-5uige

const state = o({
  count: 0,
  number: 0
});
watch(state.count, () => console.log("unrelated side effect"));

function App() {
  return (
    <div className="App">
      <button onClick={() => state.count++}>Count ++</button>
      <button onClick={() => state.number--}>Number --</button>
    </div>
  );
}

EDIT: Perhaps the best way is to just to reference the value in auto and never use it?

auto(() => {
  const watch = state.count;
  console.log("unrelated side effect")
})

Shorthand syntax for "Auto" reactions

"Reactions" are unobserved side effects in response to an observed computation.

The current way to do this:

auto(() => {
  // Observed computation here
  noto(() => {
    // Unobserved side effects here
  })
})

The proposed way to do this:

auto(
  () => {
    // Observed computation here
  },
  result => {
    // Unobserved side effects here
  }
)

The bonus (of course) being that you don't need to import noto.

Could not find module in path: 'is-dev'

Hey Alec, nice work! I've been playing around with this and first impressions it seems significantly more performant than mobx. However, I've been using version 0.6 because higher versions get the following error Could not find module in path: 'is-dev' relative to '/node_modules/wana/dist/wana.cjs.js'. Would be possible to take a look when you get a moment?

It also looks like it's possible to used derived outside of react, in the following way. Is that correct?

import { o, derived, auto } from 'wana'
let count = o(1);
const multiplied = derived(() => count * 10, [count])
  auto(() => {
    console.log('multiplied:', multiplied())
  })

prevent auto from updating its observers

Hey, sorry to keep finding the most obscure use cases.
It looks like auto updates its observers on each run. When it comes to making these deferred updates, it forces me to do some unnecessary computation, and I wonder if there's any easy way around it.

useEffect(() => {
    const a = auto(() => {
      // would be nice to fire only once but necessary to maintain observation
      // might be a complex getter which would defeat the purpose of deferring the update
      readObservableValue();
      // defer update until browser is idle
      requestIdleCallback(() => updateReactState());
    });
    return () => {
      console.log("disposed");
      a.dispose();
    };
  }, []);

Is there a way to set the auto function observables in the initial run and then never refer to them again them again?
A simplified example would be this.

const store = o({ test: 1 });
auto(function () {
  // this just exists to set up the initial observation
  // and should never be used again
  if (!this.fired) {
    const val = store.test; // read observable object
    console.log("fire once");
    this.fired = true;
  }
  // this is what I would like to happen every time
  console.log("fire multiple");
});
store.test++; // fire once, fire multiple
store.test++; // fire multiple
store.test++; // stops observing -> would like "fire multiple" again
store.test++; // stops observing -> would like "fire multiple" again
store.test++; // stops observing -> would like "fire multiple" again

External Observables

Is it possible to trigger the auto function with external observables?

Say I had an object with onRead and onUpdate callbacks. Is there some way to do something like the following?

externalObservable.onRead(() => tellWanaObjectRead())
externalObservable.onUpdate(() => tellWanaObjectUpdated())

auto(() => {
    console.log(externalObservable.value)
})

externalObservable.value = 'newVal'
// -> console logs 'newVal'

globals.beforeChange

Hey Alec, trying to implement undo / redo. I think I can get something hacked together with global beforeChange event. Would that be possible?

The rough idea is to create a localized history by listening to observables using auto. But I need to somehow get the event object into the localized history when something changes. Using beforeChange, I think I can record all change events, then after, the auto function can just grab the latest event off the stack.

Sync auto fails with add / remove changes

I'm trying to get a getter to update when adding / removing object properties.

The example below acts a little strange. The getter updates when I add the first property to the object, but never afterwards.

https://codesandbox.io/s/wana-object-getters-u1q8b

const lookup = o({ 1: {}, 2: {}, 3: {} });
const list = o(() => Object.values(lookup));
auto(() => console.log("auto", { ...lookup }, [...list()]), { sync: true });

// auto fires here
lookup["4"] = {};

// auto doesn't fire
lookup["5"] = {};
// auto doesn't fire
lookup["6"] = {};

// auto doesn't fire
delete lookup["6"];

EDIT: Can confirm this is only an issue when sync: true, the following works as expected.

const lookup = o({ 1: {}, 2: {}, 3: {} });
const list = o(() => Object.values(lookup));
auto(() => console.log("auto", { ...lookup }, [...list()]));

lookup["4"] = {};
setTimeout(() => (lookup["5"] = {}), 500);
setTimeout(() => (lookup["6"] = {}), 500);
setTimeout(() => delete lookup["6"], 500);

Maintaining Caret Position with Input Fields

Hey Alec, when using withAuto with input fields react seems to loose the caret position, when typing in a new value.

I can fix it be replicating the state locally, I need to be able to set the input value from both inside and outside the input component (represented by the 'generate number' button).

And again adding useState to a shared parent would fix it in this example, but I need to use external state due to things being update outside of react. Is it possible to retain the caret position when updating from global state?

How the input should behave (but using local state):
https://codesandbox.io/s/friendly-hill-5ebqw

Wana example using external state, but the caret jumps to the end on input:
https://codesandbox.io/s/eager-black-l51dn

Parcel 2 - Uncaught ReferenceError: nonce is not defined

Hey, just trying out parcel 2 and am getting an error when using withAuto. I'm not 100% whether it's a valid error or something I need to raise with parcel? It's true that nonce isn't defined when the function is created but I'm not sure if being inside a function should change things.

Uncaught ReferenceError: nonce is not defined
    at withAuto.tsx:44
// Subscribe to observables as early as possible, because
    // we don't want effects to trigger the previous observer.
    useLayoutEffect(() => commit(observer, nonce))

Get and set an observable value inside an "auto" callback

An idea worth considering: If an auto callback both gets and sets the same observable value, that value should not be observed. Otherwise, an infinite loop will occur.

const state = o({ enabled: true, count: 0 })
auto(() => {
  if (state.count % 2) {
    // Flip "enabled" on every odd number.
    state.enabled = !state.enabled
  }
})

Currently, you can work around this by only accessing the observable value inside an untracked callback, but it's a little clunky and (maybe) easy enough to forget about this quirk.

const state = o({ enabled: true, count: 0 })
auto(() => {
  if (state.count % 2) {
    // Flip "enabled" on every odd number.
    state.enabled = !untracked(() => state.enabled)
  }
})

Make "withAuto" check for staleness in "useLayoutEffect"

When a withAuto component has an observable change between the render and commit phases, the user will see stale data for at least one frame. By adding a useLayoutEffect call to withAuto, it can check for stale data in time to force a sync re-render.

Note: This behavior would partially negate the benefits of React's concurrent mode, which means frame skips could occur because of it (even in concurrent mode).

Object.keys observes value changes

Ideally, the Object.keys shim should only observe key changes (eg: when a key is added or removed). Currently, it observes value changes too (eg: when a key is set to a new value).

Note: This goes for Map.prototype.keys as well.

"withAuto" components should never render more than once per batch of observed changes

Components wrapped by withAuto should never render more than once in response to an observable value being changed.

Unfortunately, when a parent component and its child both observe the same value, the child is rendered up to 2 times (depending on whether or not the child is memoized).

Solution

  1. Make withAuto provide a number to descendants that tracks the depth of withAuto calls. For example, when a parent and its child are both wrapped by withAuto, the child has a depth of 1 and the parent has a depth of 0.

  2. When an observed value is changed, add the withAuto component to a sorted queue, where components closer to the root come first.

  3. Before flushing the sorted queue, keep track of a nonce for each withAuto component in the queue. This cached nonce is compared to the current nonce before each withAuto component is updated. Only update a component whose current nonce is equal to its cached nonce.

In conclusion, withAuto will always update parent components first. Child components will avoid re-rendering more than once per batch of observed changes.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.