Giter VIP home page Giter VIP logo

micro-observables's Introduction

Micro-observables

A simple Observable library that can be used for easy state-management in React applications.

Features

  • ๐Ÿ’†โ€โ™‚๏ธ Easy to learn: No boilerplate required, write code as you would naturally. Just wrap values that you want to expose to your UI into observables. Micro-observables only exposes a few methods to create and transform observables
  • โš›๏ธ React support: Out-of-the-box React support based on React Hooks and higher-order components
  • ๐Ÿฅ Lightweight: The whole source code is made of less than 400 lines of code, resulting in a 6kb production bundle
  • ๐Ÿ”ฅ Performant: Observables are evaluated only when needed. Micro-observables also supports React and React Native batching, minimizing the amount of re-renders
  • ๐Ÿ”ฎ Debuggable: Micro-observables does not rely on ES6 proxies, making it easy to identify lines of code that trigger renders. Code execution is easy to follow, making debugging straightforward
  • ๐Ÿ›  TypeScript support: Being written entirely in TypeScript, types are first-class citizen

Introduction

In micro-observables, observables are objects that store a single value. They are used to store a piece of state of your app. An observable notifies listeners each time its value changes, triggering a re-render of all components that are using that observable for example.

Observables can be easily derived into new observables by applying functions on them, such as select(), onlyIf() or default().

Micro-observables works great in combination with React thanks to the use of the useObservable() hook or the withObservables higher-order component. It can be used as a simple yet powerful alternative to Redux or MobX.

Micro-observables has been inspired by the simplicity of micro-signals. We recommend checking out this library for event-driven programming.

Note: If you are used to RxJS, you can think of micro-observables as a React-friendly subset of RxJS exposing only the BehaviorSubject class.

Basic usage

import assert from "assert";
import { observable } from "micro-observables";

const favoriteBook = observable({ title: "The Jungle Book", author: "Kipling" });
const favoriteAuthor = favoriteBook.select(book => book.author);

assert.deepEqual(favoriteBook.get(), { title: "The Jungle Book", author: "Kipling" });
assert.equal(favoriteAuthor.get(), "Kipling");

const receivedAuthors: string[] = [];
favoriteAuthor.subscribe(author => receivedAuthors.push(author));

favoriteBook.set({ title: "Pride and Prejudice", author: "Austen" });
assert.deepEqual(receivedAuthors, ["Austen"]);

favoriteBook.set({ title: "Hamlet", author: "Shakespeare" });
assert.deepEqual(receivedAuthors, ["Austen", "Shakespeare"]);

Using micro-observables with React

Micro-observables works great with React and can be used to replace state-management libraries such as Redux or MobX. It allows to easily keep components in sync with shared state by storing pieces of state into observables. The useObservable() hook or withObservables higher-order component can be used to access these values from a component.

Obligatory TodoList example

type Todo = { text: string; done: boolean };

class TodoService {
  private _todos = observable<readonly Todo[]>([]);

  readonly todos = this._todos.readOnly();
  readonly pendingTodos = this._todos.select(todos => todos.filter(it => !it.done));

  addTodo(text: string) {
    this._todos.update(todos => [...todos, { text, done: false }]);
  }

  toggleTodo(index: number) {
    this._todos.update(todos => todos.map((todo, i) => (i === index ? { ...todo, done: !todo.done } : todo)));
  }
}

const todoService = new TodoService();
todoService.addTodo("Eat my brocolli");
todoService.addTodo("Plan trip to Bordeaux");

export const TodoList: React.FC = () => {
  const todos = useObservable(todoService.todos);
  return (
    <div>
      <TodoListHeader />
      <ul>
        {todos.map((todo, index) => (
          <TodoItem key={index} todo={todo} index={index} />
        ))}
      </ul>
      <AddTodo />
    </div>
  );
};

const TodoListHeader: React.FC = () => {
  const pendingCount = useObservable(todoService.pendingTodos.select(it => it.length));
  return <h3>{pendingCount} pending todos</h3>;
};

const TodoItem: React.FC<{ todo: Todo; index: number }> = ({ todo, index }) => {
  return (
    <li style={{ textDecoration: todo.done ? "line-through" : "none" }} onClick={() => todoService.toggleTodo(index)}>
      {todo.text}
    </li>
  );
};

const AddTodo: React.FC = () => {
  const input = useRef<HTMLInputElement>(null);

  const addTodo = (event: React.FormEvent) => {
    event.preventDefault();
    todoService.addTodo(input.current!.value);
    input.current!.value = "";
  };

  return (
    <form onSubmit={addTodo}>
      <input ref={input} />
      <button>Add</button>
    </form>
  );
};

This example can be run on CodeSandbox.

React Batching

Micro-observables supports React batched updates: when modifying an observable, all re-renders caused by the changes from the observable and its derived observables are batched, minimizing the total amount of re-renders.

Another important benefit of React Batching is that it ensures consistency in renders: you can learn more about this on MobX Github.

By default, batching is disabled as it depends on the platform your app is targeting. To enable it, import one of these files before using micro-observables (typically in your index.js file):

For React DOM: import "micro-observables/batchingForReactDom"

For React Native: import "micro-observables/batchingForReactNative"

For other platforms: You can use the custom batching function provided by the platform by calling the setBatchedUpdater() function from micro-observables.

API

In micro-observables, there are two types of observables: WritableObservable and Observable. A WritableObservable allows to modify its value with the set() or update() methods. An Observable is read-only and can be created from a WritableObservable with readOnly(), select(), onlyIf() and other methods.

Functions

observable(initialValue): WritableObservable

observable(initialValue) is a convenient function to create a WritableObservable. It is equivalent to new WritableObservable(initialValue).

Wrapping a value with the observable() function is all is needed to observe changes of a given value.

Note: initialValue can be another observable. In this case, the new observable will be automatically updated when initialValue changes.

const book = observable("The Jungle Book");

Instance Methods

Observable.get()

Return the value contained by the observable without having to subscribe to it.

const book = observable("The Jungle Book");
assert.equal(book.get(), "The Jungle Book");

WritableObservable.set(newValue)

Set the new value contained by the observable. If the new value is not equal to the current one, listeners will be called with the new value.

const book = observable("The Jungle Book");
book.set("Pride and Prejudice");
assert.equal(book.get(), "Pride and Prejudice");

Note: newValue can be another observable. In this case, the observable will be automatically updated when newValue changes.

WritableObservable.update(updater: (value) => newValue)

Convenient method to modify the value contained by the observable, using its current value. It is equivalent to observable.set(updater(observable.get())). This is especially useful to work with collections or to increment values for example.

const books = observable(["The Jungle Book"]);
books.update(it => [...it, "Pride and Prejudice"]);
assert.deepEqual(books.get(), ["The Jungle Book", "Pride and Prejudice"]);

Observable.subscribe(listener: (value, prevValue) => void)

Add a listener that will be called when the observable's value changes. It returns a function to call to unsubscribe from the observable. Each time the value changes, all the listeners are called with the new value and the previous value. Note: Unlike other observable libraries, the listener is not called immediately with the current value when subscribe() is called.

const book = observable("The Jungle Book");

const received: string[] = [];
const prevReceived: string[] = [];
const unsubscribe = book.subscribe((newBook, prevBook) => {
  received.push(newBook);
  prevReceived.push(prevBook);
});
assert.deepEqual(received, []);
assert.deepEqual(prevReceived, []);

book.set("Pride and Prejudice");
assert.deepEqual(received, ["Pride and Prejudice"]);
assert.deepEqual(prevReceived, ["The Jungle Book"]);

unsubscribe();
book.set("Hamlet");
assert.deepEqual(received, ["Pride and Prejudice"]);
assert.deepEqual(prevReceived, ["The Jungle Book"]);

WritableObservable.readOnly()

Cast the observable into a read-only observable without the set() and update() methods. This is used for better encapsulation, preventing outside modifications when an observable is exposed.

class BookService {
  private _book = observable("The Jungle Book");

  readonly book = this._book.readOnly();
}

Note: This method only makes sense with TypeScript as the returned observable is the same unchanged observable.

Observable.select(selector: (value) => selectedValue)

Create a new observable with the result of the given selector applied on the input value. Each time the input observable changes, the returned observable will reflect this changes.

const book = observable({ title: "The Jungle Book", author: "Kipling" });
const author = book.select(it => it.author);
assert.equal(author.get(), "Kipling");
book.set({ title: "Hamlet", author: "Shakespeare" });
assert.equal(author.get(), "Shakespeare");

Note: The provided selector function can return another observable. In this case, the created observable will get its value from the returned observable and will be automatically updated when the value from the returned observable changes.

Observable.onlyIf(predicate: (value) => boolean)

Create a new observable that is only updated when the value of the input observable passes the given predicate. When onlyIf() is called, if the current value of the input observable does not pass the predicate, the new observable is initialized with undefined

const counter = observable(0);
const even = counter.onlyIf(it => it % 2 === 0);
const odd = counter.onlyIf(it => it % 2 === 1);
assert.equal(even.get(), 0);
assert.equal(odd.get(), undefined);

counter.update(it => it + 1);
assert.equal(even.get(), 0);
assert.equal(odd.get(), 1);

counter.update(it => it + 1);
assert.equal(even.get(), 2);
assert.equal(odd.get(), 1);

Observable.default(defaultValue)

Transform the observable into a new observable that contains the value of the input observable if it is not undefined or null, or defaultValue otherwise. It is equivalent to observable.select(val => val ?? defaultValue). This is especially useful in combination with onlyIf() to provide a default value if current value does not initially pass the predicate.

const userLocation = observable<string | null>(null);
const lastSeenLocation = userLocation.onlyIf(it => !!it).default("Unknown");
assert.equal(lastSeenLocation.get(), "Unknown");

userLocation.set("Paris");
assert.equal(lastSeenLocation.get(), "Paris");

userLocation.set(null);
assert.equal(lastSeenLocation.get(), "Paris");

userLocation.set("Bordeaux");
assert.equal(lastSeenLocation.get(), "Bordeaux");

Observable.toPromise()

Convert the observable into a promise. The promise will be resolved the next time the observable changes. This is especially useful in order to await a change from an observable.

const age = observable(34);
(async () => {
  await age.toPromise();
  console.log("Happy Birthday!");
})();
age.set(35);

Static Methods

Observable.select([observable1, observable2, ...], selector: (val1, val2...) => selectedValue)

Take several observables and transform them into a single observable with the result of the given selector applied on the input values. Each time one of the input observables changes, the returned observable will reflect this changes. This is a more generic version of the observable.select() instance method, that can takes several observables.

const author = observable("Shakespeare");
const book = observable("Hamlet");
const bookWithAuthor = Observable.select([author, book], (a, b) => ({
  title: b,
  author: a,
}));
assert.deepEqual(bookWithAuthor.get(), { title: "Hamlet", author: "Shakespeare" });

book.set("Romeo and Juliet");
assert.deepEqual(bookWithAuthor.get(), { title: "Romeo and Juliet", author: "Shakespeare" });

author.set("Kipling");
book.set("The Jungle Book");
assert.deepEqual(bookWithAuthor.get(), { title: "The Jungle Book", author: "Kipling" });

Observable.merge(observables)

Transform an array of observables into a single observable containing an array with the values from each observable.

const booksWithId = [
  { id: 1, book: observable("The Jungle Book") },
  { id: 2, book: observable("Pride and Prejudice") },
  { id: 3, book: observable("Hamlet") },
];
const books = Observable.merge(booksWithId.map(it => it.book));
assert.deepEqual(books.get(), ["The Jungle Book", "Pride and Prejudice", "Hamlet"]);

Observable.latest(observable1, observable2, ...)

Take several observables and transform them into a single observable containing the value from the last-modified observable. The returned observable is initialized with the value from the first given observable.

const lastMovie = observable("Minority Report");
const lastTvShow = observable("The Big Bang Theory");
const lastWatched = Observable.latest(lastMovie, lastTvShow);
assert.equal(lastWatched.get(), "Minority Report");

lastTvShow.set("Game of Thrones");
assert.equal(lastWatched.get(), "Game of Thrones");

lastMovie.set("Forrest Gump");
assert.equal(lastWatched.get(), "Forrest Gump");

Observable.compute(compute: () => value)

Observable.compute() is your silver bullet when it is too difficult to create a new observable with the usual select(), onlyIf() or latest() methods. It is especially useful when dealing with complex data structures. It takes a function that computes a new value by directly accessing values from other observables and it returns a new observable containing the result of this computation.

How it works: Each time the observable is evaluated, it calls the provided compute function and automatically tracks the observables that are used during the computation (i.e. those on which get() is getting called). It then registers these observables as input, ensuring that the new observable is updated each time one of them changes. If you are familiar with MobX, it works the same way as the @computed observables.

Note: There is a slight performance impact of using Observable.compute() as it has to track and update the inputs dynamically. But unless you're dealing with thousands of computed observables, it should not be noticeable.

const authors = new Map([
  [0, observable("Kipling")],
  [1, observable("Shakespeare")],
  [2, observable("Austen")],
]);
const books = observable([
  { title: "The Jungle Book", authorId: 0 },
  { title: "Pride and Prejudice", authorId: 2 },
  { title: "Persuasion", authorId: 2 },
]);
const booksWithAuthors = Observable.compute(() =>
  books.get().map(book => ({ title: book.title, author: authors.get(book.authorId).get() }))
);
assert.deepEqual(booksWithAuthors.get(), [
  { title: "The Jungle Book", author: "Kipling" },
  { title: "Pride and Prejudice", author: "Austen" },
  { title: "Persuasion", author: "Austen" },
]);

Observable.fromPromise(promise, onError?: (error) => value)

Convert the promise into an observable. The observable is initialized with undefined and will be updated with the value of the promise when it is resolved. If the promise is rejected, the optional onError function is called with the error and should return the value to assign to the observable. If no onError function is provided, the observable keeps its undefined value.

async function fetchBook(title: string): Promise<Book> {
  // ...
}

const book = Observable.fromPromise(fetchBook("The Jungle Book"));
assert.equal(book.get(), undefined);
book.subscribe(book => console.log(`Retrieved book: ${book}));

Observable.batch(block: () => void)

Group together several observable modifications. It ensures that listeners from any derived observable are only called once which might be useful for data consistency or for performance.

Additionally, if React batching is enabled, it batches re-renders together. You can learn more about React batching and how to enable it here.

const numbers = [...Array(10)].map((_, index) => observable(index));
const total = Observable.merge(numbers).select(num => num.reduce((a, b) => a + b));
expect(total.get()).toStrictEqual(45);

// Listeners of "total" will only be called once, with the final result.
// Without batching(), it would have been called 10 times
total.subscribe(val => assert.equal(val, 65));
Observable.batch(() => numbers.forEach(num => num.update(it => it + 1)));

React Integration

Hooks

useObservable(observable)

Return the value of the observable and trigger a re-render when the value changes.

const TodoList: React.FC = () => {
  const todos = useObservable(todoService.todos);
  return (
    <div>
      {todos.map((todo, index) => (
        <TodoItem key={index} todo={todo} />
      ))}
    </div>
  );
};

useMemoizedObservable(factory: () => Observable, deps: any[])

Shortcut for useObservable(useMemo(factory, deps)). Return the value of the observable created by the factory parameter and automatically trigger a re-render when its value changes.

The factory function is evaluated each time one of the values in deps changes. If unspecified, deps defaults to [], resulting in the factory function being called only once.

Note: useMemoizedObservable() is an optimized version of useObservable() that avoids recreating a new observable and reevaluating it at each render. Most of the time, you actually don't even need it, creating an observable is a fast operation and if your observable evaluation does not require heavy computation, you can use useObservable() directly instead.

type User = { id: string; displayName: string };
type Todo = { text: string; completed: boolean; assigneeId: string };

class TodoService {
  private _todos = observable<readonly Todo[]>([]);

  readonly todos = this._todos.readOnly();

  getTodosAssignedTo(assigneeId: string): Observable<Todo[]> {
    return this._todos.select(todos => todos.filter(it => it.assigneeId === assigneeId));
  }
}

const TodoList: React.FC<{ assigneeId: string }> = ({ assigneeId }) => {
  const todos = useMemoizedObservable(() => todoService.getTodosAssignedTo(assigneeId), [assigneeId]);
  return (
    <div>
      <ul>
        {todos.map((todo, index) => (
          <TodoItem key={index} todo={todo} index={index} />
        ))}
      </ul>
    </div>
  );
};

useComputedObservable(compute: () => value, deps?: any[])

Shortcut for useMemoizedObservable(() => Observable.compute(compute), deps)). Create a new observable with Observable.compute() and automatically trigger a re-render when the result of the compute function changes.

The observable is recreated each time one of the values in deps changes. If unspecified, deps defaults to [], resulting in the observable being created only once.

Higher Order Component

withObservables(Component, mapping): InjectedComponent

Hooks cannot be used in class components. In this case, you can use the withObservables HOC in order to inject values from observables into props of a component. It works the same as Redux's connect() function as it takes a component and a props-to-observables mapping.

mapping can either be a plain mapping object of the form { props1: observable1, props2: observable2 }, or it can be a function taking the ownProps of the component and returning a plain mapping object.

interface Props {
  assigneeId: string;
}

interface InjectedProps {
  readonly todos: Todo[];
}

class TodoList extends React.Component<Props & InjectedProps> {
  render() {
    return (
      <div>
        <ul>
          {todos.map((todo, index) => (
            <TodoItem key={index} todo={todo} index={index} />
          ))}
        </ul>
      </div>
    );
  }
}

const mapping = (ownProps: Props) => ({
  todos: todoService.getTodosAssignedTo(ownProps.assigneeId),
});

export default withObservables(TodoList, mapping);

micro-observables's People

Contributors

abreu-dev avatar dependabot[bot] avatar jesse-holden avatar krishemenway avatar simontreny 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

micro-observables's Issues

useObservable forceRender race condition

I've encountered a very obscure race condition when using multiple useObservable hooks across a variety of components.

The gist of it is when two different React components are observing multiple observables in the same store, a change in one observable will trigger a forceRender, which will cause the useEffect() effect function to be called, and when it returns it will call the unsubscribe method of the onChange method, and prevents the change notification to be delivered (as its no longer listening in between the change), https://github.com/BeTomorrow/micro-observables/blob/master/src/hooks.ts#L8. There is a timing issue where the onChange unsubscribers will be called by the effect's return function, removing the listener of an observable that in-turn will also be updated a set call from the store.

The result is one component updates, and the other does not as it never receives the onChange listener call and never calls its own forceRender. This happens especially with multiple shared observables across multiple components. I've experienced this in my own application that uses stores declaratively (like a spreadsheet).

One solution which works is to defer the unsubscribe calls:

export function useObservable<T>(observable: Observable<T>): T {
	const [, forceRender] = useState({});

	useEffect(() => {
		const unsubscribe = observable.onChange(() => forceRender({}));
		return () => {
			setTimeout(() => unsubscribe(), 150);
		}
	}, [observable]);

	return observable.get();
}

Using a timeout is certainly not ideal here but it does solve the issue. Its possible there is a circular dependency between observables and components, but this case should be considered. Please lmk if you have other ideas.

Add react-dom as an optional dependency

Currenty batchingForReactDom.js references react-dom even through react-dom is not referenced in the project package.json file.

It seems to cause an issue with recent versions of Yarn.

Would it be possible to react-dom to the optional dependencies of the package.json file? And I guess it should be the same with react-native.

Here is the full error from Yarn:

errors: [
  {
    detail: undefined,
    id: '',
    location: {
      column: 25,
      file: '../.yarn/__virtual__/micro-observables-virtual-7722fe8c8c/0/cache/micro-observables-npm-1.7.2-8e88bfdb52-15dd9eadc5.zip/node_modules/micro-observables/batchingForRea
ctDom.js',
      length: 11,
      line: 1,
      lineText: 'const ReactDOM = require("react-dom");',
      namespace: '',
      suggestion: ''
    },
    notes: [
      {
        location: {
          column: 33,
          file: '../.pnp.cjs',
          length: 343,
          line: 9047,
          lineText: '          "packageDependencies": [\\',
          namespace: '',
          suggestion: ''
        },
        text: `The Yarn Plug'n'Play manifest forbids importing "react-dom" here because it's not listed as a dependency of this package:`
      },
      {
        location: null,
        text: 'You can mark the path "react-dom" as external to exclude it from the bundle, which will remove this error. You can also surround this "require" call with a try/cat
ch block to handle this failure at run-time instead of bundle-time.'
      }
    ],
    pluginName: '',
    text: 'Could not resolve "react-dom"'
  }
]

Handling nested observable that is possibly undefined

Hello,
This is not strictly a bug it's more of an edge case that i'm not sure how to handle or the best pattern to make it work.

Say I have an item that has a nested observable for quantitySelected. Just doing:

const selectedItem = useObservable(itemStore.selectedItem)
const selectedItemValue = useObservable(selectedItem.quantitySelected)

Will cause it to break when selectedItem is undefined.

I can fix this by having a conditional hook but, this will cause other issues due to this being an anti-pattern.

const selectedItem = useObservable(itemStore.selectedItem)
const selectedItemValue = selectedItem ? useObservable(selectedItem.quantitySelected) : 0

Would appreciate any help on this.

Thank you

Observable.proxy idea

I have a scenario where I have an object that I'd like to turn into a micro-observable so I can use with the rest of observable state in the project.

I initially thought I could use Observable.compute(), but of course this is to compute only other structures already represented as an Observable value from micro-observables.

How about the idea of adding a Observable.proxy or Observable.wrap method, which could use the Proxy API to listen for changes in these situations?

avoid updates when value did not change (with custom equality function)

I have a graph of observables, that originate from a rapidly changing one (a redux store that we want to migrate away from).

I want derived observables to only propagate updates if their value actually changed.
I want to use a custom equality function to determine if something changed (e.g. structual equality over reference equality).

I know that I can compare current and last value inside subscribe, but most of my observables are not directly subscribed, but are used as inputs to other observables, which are unnessecarily recomputed.

I propose an additional, optional parameter isEqual(next: T, current: T): boolean, to .select() and .compute(), after the lambda function, where you can pass in an equality function that is evaluated on every update after the initialitation of the observable. If it returns true, the observable is not updated with the computed value.

import { isEqual } from "lodash"

const signalSpeed: Observable<Speed> = reduxStore.select(store => ({
  pxPerMm: store.config?.getSettings("pxPerMm") ?? 0,
  mmPerSec: store.config?.getSettings("signalSpeed") ?? 0,
}), isEqual)

react batching broken

from app code:

import "micro-observables/batchingForReactDom"

message:

ERROR in ./node_modules/micro-observables/batchingForReactDom.js
Module not found: Error: Can't resolve './lib' in '/home/peter/Dev/.../node_modules/micro-observables'
@ ./node_modules/micro-observables/batchingForReactDom.js 2:0-16


perhaps this feature is incomplete?

Including a dev tool

Thank you for this great library.
It helps me easily separating my business logic from my React code.

I use a logger to display the state of my micro observables and it is a bit of a mess.

I read in a previous issue that your team is working on version 2.x for releasing a dev tool (similar to the Redux dev tool interface I guess)

Is the work about it still in progress? It would be great to have some updates

Cheers :)

Individual observables vs object

Hi there,

I'm really enjoying the simplicity of this library and pattern. It reminds me of mobx but much simpler/lighter.

I'm wondering if you have an example in the wild using this library however as I'm trying to decide between the right patterns that offer nice syntax sugar, readability, succinctness and performance.

One question is, in a TodoService (which I prefer to call TodoStore), I would usually have a number of fields inside of a store, and I'm trying to decide if I should make a single observable state object, or break it into multiple observable values.

class SomeStore {
  state = observable({
    enabled: false,
    name: '',
    age: 30
  })
}

or.. separate..

class SomeStore {
  enabled = observable<boolean>(false)
  name = observable<string>('')
  age = observable<number>(30)
}

I will admit, I prefer the second approach with individual observables, but, I can't manage to come up with a new useObservable or a new useStore hook pattern to make it easy to consume the entirety of the store without having to relist each individual observable.

the other part to this pattern is considering serialization which is useful for cases when an app needs SSR.

thank you

React hook useObservable and SSR warnings

When useObservable is used in a Server Side rendering context, it raises a React warning:

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-
hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for
common fixes.

To avoid this, the solution described here could be easily applied: https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a

So for example, the code might look like this:

const useSsrCompatibleLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;

export function useObservable<T>(observable: Observable<T>): T {
  const [, forceRender] = useState({});
  const val = observable.get();

  useSsrCompatibleLayoutEffect(() => {
    return observable.subscribe(() => forceRender({}));
  }, [observable]);

  return val;
}

Add ES module distribution

For use with modern bundling tools like Vite and Snowpack. At the moment, Vite tries to optimize the library as a CommonJS module with a single default export, then errors with this at runtime:

Uncaught SyntaxError: The requested module '/@modules/micro-observables.js' does not provide an export named 'observable'

Adding an ES module distribution would fix this. I'm also open to adding this myself

using micro-observables library without react

I'm becoming quite fond of micro-observables and I'd like to use it in a library I'm writing that is not React based. I see 'react' is a peerDependency which will work fine but of course npm/yarn will complain of an unmet peerDependency.

The only react requirement if the hooks within https://github.com/BeTomorrow/micro-observables/blob/master/src/hooks.ts#L1, a thought would be to package this as a separate library just for the hook? or I believe making 'react' an optionalDependency might work as well.

middleware

I think it'd be great to have a middleware adapter interface for getters/setters and maybe other stuff. It would be useful to attach things like a logger during debugging situations to find out the code path of code being set. I feel it would be pretty easy to add too

btw (as an aside point), mobx6 came out today and its certainly cleaner then mobx4/5, but I still feel the micro footprint of micro-observables and smaller surface api makes for a better experience. However mobx is definitely more battle tested and has things like middleware for logging. My main thing with micro-observables is ensuring im not re-rendering more often than I want, and also to ensure consistency of the data as its being updated, perhaps this is why mobx encourages to make updates within an action() call. Maybe micro-observables should also have some explicit call to make a batch'd update in a single tick instead of having multiple state updates go across render ticks and make for unnecessary re-renders.

another interesting fact.. micro-observables/src/* is 328 lines of code, and mobx/src/* is 5312 lines of code.. 93% smaller which is a pretty awesome achievement, but I still think more can be done to micro-observables to make it better while still keeping the surface api as small as possible and keeping spirit of explicitness

(UPDATE: just found in the docs Observable.batch(block: () => void) for batch updating, nice)

"Cannot find module './lib'" ReactDOM Batching

Hello,

Thanks for creating and maintaining this package! I've come across an issue when trying to implement ReactDOM Batching in version 1.5.0-rc4.

Uncaught Error: Cannot find module './lib'
    at webpackMissingModule (batchingForReactDom.js:2)
    at Object../node_modules/micro-observables/batchingForReactDom.js (batchingForReactDom.js:2)
    at __webpack_require__ (bootstrap:832)
    at fn (bootstrap:129)
    at Module../src/index.tsx (index.tsx:2)
    at __webpack_require__ (bootstrap:832)
    at fn (bootstrap:129)
    at Object.0 (debug-relayer.ts:13)
    at __webpack_require__ (bootstrap:832)
    at bootstrap:970

Sure enough when I look at the installed module there is no such ./lib folder in the directory. Please advise on what I may be missing here.

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.