useObservables for react hooks
Hey all, I've found withObservables to be super handy, but now with react hooks I've been using a simple "useObservables" instead:
import { useState, useEffect } from 'react';
export default function useObservable(observable) {
const [value, setValue] = useState(observable && observable.value);
useEffect(
() => {
if (!observable || !observable.subscribe) {
return;
}
const subscription = observable.subscribe(setValue);
return () => subscription.unsubscribe();
},
[observable]
);
return value;
}
Usage:
function MyView({ observable }) {
const currentValue = useObservable(observable);
return <Text>{currentValue}</Text>
}
Note: it is best to use ObservableBehavior so that the observable.value can be accessed synchronously for the first render
I'm just getting started with WatermelonDB, Observables and React hooks. I had some problems with the above code causing endless re-renders .. I suspect due to the ObservableBehavior as mentioned, but I don't fully understand what that means 😞
However, I did have success with this useObservable library, so I thought I'd just leave it here in case it helps someone else 😄
The useObservable hook I am currently using is:
import { Subject, Subscription } from 'rxjs';
import { useEffect, useMemo, useState } from 'react';
export default function useObservable(observable, initial, inputs = []) {
const [state, setState] = useState(initial);
const subject = useMemo(() => new Subject(), inputs);
useEffect(() => {
const subscription = new Subscription();
subscription.add(subject);
subscription.add(subject.pipe(() => observable).subscribe(value => setState(value)));
return () => subscription.unsubscribe();
}, [subject]);
return state;
}
Usage:
function MyView({ observable }) {
const currentValue = useObservable(observable, 'initial', [observable, triggers]);
return <Text>{currentValue}</Text>
}
I was going to post a PR to this repository ... but import { useObservable } from '@nozbe/with-observables' feels a little weird. Perhaps it would be better to have some sort of monorepo for hooks? Especially if @brunolemos has ideas for more hooks :smile:
I was going to post a PR to this repository ... but import { useObservable } from '@nozbe/with-observables' feels a little weird.
I know! I think it's best to post it anyway, and then figure out what's the best name… I imagine withObservables will be obsolete/deprecated in a year when everyone switches to Hooks anyway...
@kilbot as for your hook, I think it would be cleaner and more performant to avoid extra Subject, and just set state based on observable subscription. Another issue is the need for initial prop. If you have it - it's great, but it would be best to use Suspense to prevent further render and just wait until we can get our hands on the value subscribed to. WDYT?
I was having a problem with endless loops, ie: the first subscription was setting the state which triggered a rerender which started the process again. The extra Subject was an effort to get around that. However, I only started learning about observables when I wanted to use WatermelonDB .. so I'm a bit out of my depth 😓
I started rewriting the example app for WatermelonDB with typescript and hooks - mostly as a learning exercise for myself - I'll take another look at it this weekend to see if I reproduce the rerender issue I was having in my app.
Hi @radex, I've created some example code to illustrate the infinite loop problem I am having. Please compare these two Netlify builds: useObservable with extra Subject and useObservable without. You'll see the subscribe/unsubscribe loop in the console.
Click here to ~~compare the code~~(updated below) and here is the initial subscription which causes the problems.
It's a bit tricky to share code because I can't load everything into CodeSandbox .. but let me know if you spot anything!
I've just had a look at this again with fresh eyes and realise it is the removal of the observable as a dependency not the addition of the Subject which stopped the infinite loop, eg:
export default function useObservable(observable: Observable, initial?: any, deps: any[] = []) {
const [state, setState] = useState(initial);
useEffect(() => {
const subscription = observable.subscribe(setState);
return () => subscription.unsubscribe();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return state;
}
This seems to work okay.
It goes against the advice from the React team to remove the observable dependency, so there still may be some issues that I'm unaware of ... I'll have to do some more reading 😅
This seems very similar to how react-apollo-hooks works!
FYI! I'm working on a fully supported, tested & high-performance solution for hooks :)
This is not easy, because:
- asynchronicity means we might not have the data on first call to
useObservable, but caching means we might have - hooks can't cause the component to bail out early and return null (the way HOCs can)
- Suspense is not fully supported yet
- there's no support for disposing resources for uncommitted renders, which means we have to do some hacking to get the right performance & DX (See: https://github.com/facebook/react/issues/15317#issuecomment-491269433 _
here's some snippets of the work I'm doing.
Basic useEffect-based implementation:
export const neverEmitted = Symbol('observable-never-emitted')
export function useObservableSymbol<T>(observable: Observable<T>): T | typeof neverEmitted {
const [value, setValue] = useState(neverEmitted)
useEffect(() => {
const subscription = observable.subscribe(newValue => {
setValue(newValue)
})
return () => subscription.unsubscribe()
}, [observable])
return value
}
WIP highly-optimized implementation, but one that only works on BehaviorSubjects, and cached Observables (essentially, observables that can emit a value synchronously):
export function useObservableSync<T>(observable: Observable<T>): T {
const forceUpdate = useForceUpdate()
const value = useRef(neverEmitted)
const previousObservable = useRef(observable)
const subscription = useRef(undefined)
if (observable !== previousObservable.current) {
throw new Error('Passing different Observable to useObservable hook is not supported (yet)')
}
if (subscription.current === undefined) {
const newSubscription = observable.subscribe(newValue => {
value.current = newValue
if (subscription.current !== undefined) {
forceUpdate()
}
})
subscription.current = newSubscription
}
// TODO: GC subscription in case component never gets mounted
if (value.current === neverEmitted) {
throw new Error('Observable did not emit an initial value synchronously')
}
useEffect(() => {
return () => subscription.current.unsubscribe()
}, [])
return value.current
}
The plan is to take advantage of Suspense (or simulated suspense using error boundaries), so that you can just call const value = useObservable(query), without having to worry about initial value, and it does the right thing.
But you'd still have to wrap it somewhere with a <Supense> or a custom <ObservableSuspense> or something like that. I don't think there's a way around it.
I'd also like to publish an official prefetched component, so that you can do:
<Prefetch observables=[query, otherQuery, etc]>
<SomeComponentTreeThatUsesUseObservableHook />
</Prefetch>
this would be much faster than relying on suspense, since you can start db operations early, and by the time you get to rendering components, you have a synchronous data source, instead of constantly rolling back partial renders.
Would someone be willing to help out with writing tests for this?
This is great @radex! I would be keen to help out in any way I can.
I'll leave a link to mobx-react-lite here, just in case it is useful. There is a discussion on getting MobX to work with react hooks in concurrent mode, I guess there may be some parallels for WatermelonDB?
@kilbot Thanks! I've seen that thread and mobx-react-lite, and I was planning to spend some time next Friday digging deeper into it to understand how they did it ;) Although MobX doesn't really have asynchronicity issues AFAICT.
Just want to point out we have mobx-react-lite@next which includes experimental support for Concurrent mode. Feel free to try that.
@FredyC Thanks! I'll check it out!
observable-hooks supports Observables and Suspense which I think is super handy to use in conjunction with withObservables.
@crimx Amazing work! Looks like roughly what I was going for, but never had time to implement (yet) :) … although I do see some potential perf issues. I will check it out in the coming months...
That would be great! Much appreciated🍻.
Hi there. Any news on this?
@gliesche Not much! I recently got back to looking at it, and implemented an useModel() hook (not ready to be open sourced yet), but as you can see from the whole discussion, making a generic useObservables without a HOC, without compromises, and without forcing synchronous Watermelon is… well, very complicated :)
@gliesche Not much! I recently got back to looking at it, and implemented an
useModel()hook (not ready to be open sourced yet), but as you can see from the whole discussion, making a generic useObservables without a HOC, without compromises, and without forcing synchronous Watermelon is… well, very complicated :)
Thanks for the update. I tried observable-hooks, but that didn't really work. I'd really be interested in any working solution that uses hooks and observes WatermelonDB collections / models.
@gliesche For context: what problems are you looking to solve with a hook-based WatermelonDB observation? Do you just care about nice, consistent API? Or are there composition/performance/other problems you're running into because of this?
@gliesche For context: what problems are you looking to solve with a hook-based WatermelonDB observation? Do you just care about nice, consistent API? Or are there composition/performance/other problems you're running into because of this?
Consistent API is one main point, yes. Though I am not sure, if hook based observation is needed for custom hooks...
FYI since the last conversation observable-hooks had been refactored a little bit and had been made concurrent mode safe.
@gliesche what problems did you encounter? AFAIK watermelon observables are just plain RxJS observables so they should just work out of the box right?
(@crimx FWIW we're moving away from RxJS internally for performance reasons, but yes, the outside API is RxJS-compatible)
FYI since the last conversation observable-hooks had been refactored a little bit and had been made concurrent mode safe.
@gliesche what problems did you encounter? AFAIK watermelon observables are just plain RxJS observables so they should just work out of the box right?
@crimx I guess I don't get the API right, I tried something like:
const database = useDatabase();
const input$ = database.collections.get('posts').query().observe();
const [posts, onPosts] = useObservableState(input$, null);
Which results in a TypeError: rxjs_1.isObservable is not a function
isObservable is included in RxJS. What RxJS version are you using?
This should work
const database = useDatabase();
// reduce recomputation
const input$ = useObservable(() => database.collections.get('posts').query().observe());
// if the first parameter is an Observable `useObservableState` returns emitted values directly.
const posts = useObservableState(input$, null);
@radex is the observable from watermelon hot and with inital value? I am trying to implement a hook that subscribe synchronously. In concurrent mode it is only safe if the observable is hot.
isObservableis included in RxJS. What RxJS version are you using?
The version of the metro bundler is being used: [email protected]
@gliesche observable-hooks is meant to work with RxJS 6. Is there a reason that you still use RxJS 5? Is it a legacy project?
@gliesche observable-hooks is meant to work with RxJS 6. Is there a reason that you still use RxJS 5? Is it a legacy project?
It is not a legacy project. It's a react native project v0.62.2. Do I need to install the peer deps manually? I did not explicitly add a version of rjxs.
Do I need to install the peer deps manually?
Yes. Read this article if you are interested.
What we need is a way of expressing these "dependencies" between plugins and their host package. Some way of saying, "I only work when plugged in to version 1.2.x of my host package, so if you install me, be sure that it's alongside a compatible host." We call this relationship a peer dependency.
@radex is the observable from watermelon hot and with inital value? I am trying to implement a hook that subscribe synchronously. In concurrent mode it is only safe if the observable is hot.
@crimx It's not guaranteed to be hot — that's a problem. TL;DR: I'm working towards it.
I'm developing Nozbe Teams (the project that birthed WatermelonDB) with the intention on making all DB work synchronous so that React components can render in one shot… This works 100% of the time on web, but not yet on native (where synchronous native modules can't be guaranteed to work all the time - YET).
The longer-term plan is to go back to more multi-threading, but such that you can synchronously get values you need for first render, but can prefetch and cache queries beforehand on separate thread, and have them hot when you render.