Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useObservables for react hooks #16

Open
ericvicenti opened this issue Dec 12, 2018 · 74 comments
Open

useObservables for react hooks #16

ericvicenti opened this issue Dec 12, 2018 · 74 comments

Comments

@ericvicenti
Copy link

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

@radex radex reopened this Dec 12, 2018
@kilbot
Copy link

kilbot commented Feb 10, 2019

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 😄

@kilbot
Copy link

kilbot commented Mar 2, 2019

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 😄

@radex
Copy link
Collaborator

radex commented Mar 4, 2019

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...

@radex
Copy link
Collaborator

radex commented Mar 14, 2019

@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?

@kilbot
Copy link

kilbot commented Mar 14, 2019

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.

@kilbot
Copy link

kilbot commented Mar 16, 2019

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!

@kilbot
Copy link

kilbot commented Mar 17, 2019

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 😅

@ericlewis
Copy link

This seems very similar to how react-apollo-hooks works!

@radex
Copy link
Collaborator

radex commented May 11, 2019

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: [Concurrent] Safely disposing uncommitted objects facebook/react#15317 (comment) _

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?

@kilbot
Copy link

kilbot commented May 12, 2019

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?

@radex
Copy link
Collaborator

radex commented May 12, 2019

@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.

@danielkcz
Copy link

Just want to point out we have mobx-react-lite@next which includes experimental support for Concurrent mode. Feel free to try that.

@radex
Copy link
Collaborator

radex commented Aug 19, 2019

@FredyC Thanks! I'll check it out!

@crimx
Copy link

crimx commented Feb 24, 2020

observable-hooks supports Observables and Suspense which I think is super handy to use in conjunction with withObservables.

@radex
Copy link
Collaborator

radex commented Feb 24, 2020

@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...

@crimx
Copy link

crimx commented Feb 24, 2020

That would be great! Much appreciated🍻.

@gliesche
Copy link

Hi there. Any news on this?

@radex
Copy link
Collaborator

radex commented Jul 16, 2020

@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
Copy link

gliesche commented Jul 16, 2020

@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.

@radex
Copy link
Collaborator

radex commented Jul 16, 2020

@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
Copy link

@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...

@crimx
Copy link

crimx commented Jul 16, 2020

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?

@radex
Copy link
Collaborator

radex commented Jul 17, 2020

(@crimx FWIW we're moving away from RxJS internally for performance reasons, but yes, the outside API is RxJS-compatible)

@gliesche
Copy link

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

@crimx
Copy link

crimx commented Jul 20, 2020

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.

@gliesche
Copy link

isObservable is included in RxJS. What RxJS version are you using?

The version of the metro bundler is being used: rxjs@5.5.12

@crimx
Copy link

crimx commented Jul 21, 2020

@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?

@mfbx9da4
Copy link

Yes, we'll upgrade to RxJS 7 soon.

Having said that, so far I haven't seen and tested a generic observable hook that I'd be willing to endorse. It's not too difficult to get something that "works", but I'll only recommend to switch away from withObservables HOC once a hook solution can be shown to be as performant or more, and actually correct (there's a lot of weird edge cases to take care of).

I know it's frustrating. I'd like to move to hook-only solution myself too. This year, I hope.

@radex Would you be able to detail

  • What are the key metrics you are evaluating for measuring performance of a hooks solution?
  • What are the weird edge cases you mention? Do you have a set of complementary HOC tests which could be used to verify a hooks implementation?

A HOC approach is quite outdated now and I believe will be a deal breaker for many users considering 🍉.

@bard
Copy link

bard commented Aug 4, 2022

Usually, lack of a hook API is a deal breaker to me, too, due (to answer @radex's question above) partly to the mindset switch required when working with HOCs in an otherwise full-hook codebase, and partly to the typing issues that often accompany HOCs (of which there seem to be some: #123, #102).

However, costs are to be evaluated against benefits, and WatermelonDB is, to the best of my knowledge, one of only two open source libraries that allow offline-first applications without requiring a specific backend (the other being RxDB), which is a hell of a benefit, so I'd still go with the HOC-based API if there were signs of a hook API coming at some point. I couldn't find anything other than this thread, though. I was wondering if it's still on the radar?

@832bb9
Copy link

832bb9 commented Dec 5, 2022

@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?

Just started to adopt WatermelonDB in my RN App. I understand that we need to host observables logic on components level (which unlocks conditional rendering), but what I don't like is that HOC enforces me to split some UI nodes into separate component just to allow it's wrapping. So instead i used withObservables to create ObservablesContainer with children function exposing observed props.

  <ObservablesContainer deps={[]} getObservables={() => ({ some: interval$(1000) })}>
    {(props) => <Text>{JSON.stringify(props)}</Text>}
  </ObservablesContainer>

So now getObservables and children function can accept any other data from scope including useDatabase result.

@radex based on your WatermelonDB API design experience do you have immediate concerns on such approach?

@17Amir17
Copy link

Any update?

@okalil
Copy link

okalil commented May 11, 2023

Hey! This issue has been here for a while... I am still interested on using react hooks wih watermelondb, so I just would like to share an other approach for local first data https://riffle.systems/essays/prelude/ . It's called Riffle, it is not publicly available yet, but it would make possible a super fast reactive synchronous data hook, that can maybe serve as inspiration for new ideas in this project.

@rohankm
Copy link

rohankm commented Jul 25, 2023

We need hooks for watermelon db please

@ftaibi
Copy link

ftaibi commented Apr 11, 2024

We need hooks asap pls

@okalil
Copy link

okalil commented Apr 11, 2024

Hey, just to share, my current "workaround" is using Watermelon queries with React Query, like that:

const { data: posts } = useQuery({
  queryKey: ['comments'],
  queryFn: () => database.get('comments').query().fetch(),
})

And for a write:

const { isPending } = useMutation({
  async mutationFn: () => {
    await database.write(async () => {
      const comment = await database.get('comments').find(commentId)
      await comment.update(() => {
        comment.isSpam = true
      })
    })
  },
  onSuccess: () => queryClient.invalidateQueries(['comments'])
})

Even though react query is very popular for network requests, it works for any async data source, so that's fine:

The con is that you need to invalidate the query manually after the mutation;
The pro is that is an agnostic solution, even if you exchange watermelon db for something else, will still work with any async data source;
Besides, I personally think it's easier and more flexible than wrapping components with a hoc or dealing with observables transformations in more complex cases.

@bensenescu
Copy link

Hey, just to share, my current "workaround" is using Watermelon queries with React Query, like that:

@okalil This is awesome! I was thinking this might work, but was scared away by the warnings in the docs.

Have you gotten reactivity to work? I think your linked implementation will do a fresh query everytime the component loads the first time. But, have you gotten it to update while the user is on a screen if you get new or updated records during sync? I'm thinking something like invalidating the queries in your pull might work?

@832bb9
Copy link

832bb9 commented Apr 11, 2024

After more then year of using watermelon with react native i would suggest everyone to use https://observable-hooks.js.org/api/#useobservableeagerstate. I did not discover any downsides. The main benefit is that it provides data synchronously and thus no need to handle async state like in react query approach.

@okalil
Copy link

okalil commented Apr 12, 2024

@bensenescu react query is basically a memory cache, and there's a staleTime option that can be set to reduce refetch frequency, even avoiding it at all.
Regarding sync, I only used custom implementations, but I think invalidating everything after pull should work like you said

@Stophface
Copy link

@832bb9 Could you give an example?

@832bb9
Copy link

832bb9 commented Apr 28, 2024

@Stophface Yes, sure.

For example you have some todo app. Then you can render data from WatermelonDB like:

import { useDatabase } from '@nozbe/watermelondb/hooks'
import { useObservable, useObservableEagerState } from 'observable-hooks'
import { Text } from 'react-native'
import { switchMap as switchMap$ } from 'rxjs/operators'

export const Task = ({ taskId }: { taskId: string }) => {
  const database = useDatabase()

  // - resolves synchronously (no loading state)
  // - reactive (whenever task changed in db either after sync or local update this component will re-render)
  const task = useObservableEagerState(
    useObservable(
      (inputs$) => switchMap$(([taskId]) => database.get('tasks').findAndObserve(taskId))(inputs$),
      [taskId],
    ),
  )

  return <Text>{task.subject}</Text>
}

Here is an alternative using react-query:

import { useDatabase } from '@nozbe/watermelondb/hooks'
import { useQuery } from '@tanstack/react-query'
import { Text } from 'react-native'

export const Task = ({ taskId }: { taskId: string }) => {
  const database = useDatabase()

  // - resolves asynchronously (needs to handle loading state)
  // - non reactive (you need to invalidate query key manually after sync or local update)
  const taskQuery = useQuery(['task', { taskId }], () => database.get('tasks').find(taskId))

  if (taskQuery.isLoading) {
    return <Text>Loading...</Text>
  }

  if (taskQuery.isError) {
    return <Text>Something went wrong</Text>
  }

  const task = taskQuery.data

  return <Text>{task.subject}</Text>
}

I would much prefer to use first approach for WatermelonDB, while continue using react-query for data which need to be fetched from server separately, like statistics, if there is no need for it to be updated from client or work offline.

@Stophface
Copy link

@832bb9 Thanks for the quick response. I am trying to modify your first example with useObservable to monitor a whole table and get the whole table when any of the rows is updated. I am trying it like this

import { useObservable, useObservableEagerState } from 'observable-hooks'
import { switchMap as switchMap$ } from 'rxjs/operators'

const data = useObservableEagerState(
    useObservable(
      (inputs$) => switchMap$(() => userDatabase.get('my_table').observe())(inputs$)
    )
  )

which gives me

TypeError: Unable to lift unknown Observable type

I also tried it like this

import { useObservable, useObservableEagerState } from 'observable-hooks'
import { switchMap as switchMap$ } from 'rxjs/operators'

  const data = useObservableEagerState(
    useObservable(
      () => switchMap$(() => userDatabase.get('my_table').observe())
    )
  )

which gives me

TypeError: state$.subscribe is not a function (it is undefined)

Is there a way to observe a complete table?

@Stophface
Copy link

@832bb9 Or is that only working if I observe specific rows for changes? It would be super awesome if you could come back to me, because I struggle getting watermelonDB to work with the observables...

@jessep
Copy link

jessep commented May 16, 2024

@Stophface The problem is, as you said, that was looking for specific rows. Here's an example from what I'm making that works for a query of a list.

  const foci = useObservableEagerState(
    useObservable(
      () => db.get<FocusModel>('foci').query().observe(),
      [],
    ),
  )

@Stophface
Copy link

Stophface commented May 22, 2024

@jessep Thanks for your answer. If I implement it like this

import { useDatabase } from '@nozbe/watermelondb/hooks'
import { useObservable, useObservableEagerState } from 'observable-hooks'
import { Text } from 'react-native'

export const Foo = () => {
  const database = useDatabase()

  const tableData = useObservableEagerState(
    useObservable(
      () => db.get('some_table').query().observe(),
      [],
    ),
  )

  return <Text>{tableData.name}Text>
}

I get

Error: Observable did not synchronously emit a value.

@Stophface
Copy link

@crimx

I have this hook

import { useObservable, useObservableState } from 'observable-hooks'


const tableUpdated = (table) => {
    const database = useDatabase();

    const data = useObservableState(
        useObservable(
            () => database.get(table).query().observe(),
            [],
        ),
    )
    return data;
};

export default tableUpdated;

Which I use the following

const Foo = () => {
    const posts = tableUpdated('posts')

    return <p>{posts.text}</p>
}

Now, what I actually need is something comparable to useEffect with a dependency on the table 'posts'. Whenever the table gets updated, I need the useEffect to run and perform some actions and finally set a local state.

According to the documentation you are supposed to use useSubscription. But I have no clue how to get the following (here: wrong with useEffect) running with useSubscription

const Foo = () => {
    const [foo, setFoo] = useState(null)
    const posts = tableUpdated('posts')

    useEffect(() => {
        if(posts) {
            const result = ... // do sth
            setFoo(result)
    }, [posts])

    return <p>{foo}</p>
}

You seem to know your way around observables?

@crimx
Copy link

crimx commented May 23, 2024

@Stophface like this?

import { useObservable, useObservableState, useSubscription } from 'observable-hooks'
import { switchMap } from 'rxjs';


const useTable$ = (table) => {
    const database = useDatabase();
    return useMemo(
      () => database.get(table).query().observe(),
      [database, table]
    );
};

const posts$ = useTable$("posts");

const posts = useObservableState(posts$);

useSubscription(posts$, (posts) => {
  console.log(posts);
});

@Stophface
Copy link

Stophface commented May 23, 2024

@crimx Thanks for coming back to me. Looks like what I am looking for, but when I test your code in my component like this

import { useObservableState, useSubscription } from 'observable-hooks'
import { switchMap } from 'rxjs';

const Foo = () => {
  const useTable$ = (table) => {
    const database = useDatabase();
    return useMemo(
      () => database.get(table).query().observe(),
      [database, table]
    );
  };

  const posts$ = useTable$("posts");
  const posts = useObservableState(posts$);
  useSubscription(posts$, (posts) => {
    console.log(posts);
  });

  return <p>...</p>
}

It does not console.log anything when the database gets updated.

I am halfway there, because this

const database = useDatabase();
const data = useObservableState(
    useObservable(
        () => database.get('posts').query().observe(),
        [],
    ),
)

console.log(data)

console.logs everytime the database is updated, if I put it directly into a functional component.

const Foo = () => {
    const database = useDatabase();
    const data = useObservableState(
        useObservable(
            () => database.get('posts').query().observe(),
            [],
        ),
    )

    console.log(data)

   return <p>{data.name}</p>
}

But its kinda "useless" if I am not able to perform some (async) calculations, setting local state with the result etc. on it, which must be done in useSubscription since I cannot do it where the result of useObservableState is currently written into data.

@crimx
Copy link

crimx commented May 23, 2024

I could not reproduce your issue. Here is the code I used on the watermelondb example.

const useTable$ = (database, table) => {
  return useMemo(
    () => database.collections.get(table).query().observe(),
    [database, table]
  )
}

function MyPost({ database }) {
  const posts$ = useTable$(database, 'posts')
  useSubscription(posts$, posts => {
    console.log('[posts] Post changed!', posts)
  })
  return null
}
Screen.Recording.2024-05-23.at.15.52.04.mov

@Stophface
Copy link

@crimx Perfect, this works like a charm! Thanks a lot for sticking with me!

@Stophface
Copy link

Stophface commented May 25, 2024

@crimx Do you know how to observe not a complete table, but only a record based on a condition? If I had this query

await database
        .get('posts')
        .query(
          Q.where('postId', postId),
          Q.and(Q.where('authorId', Q.eq(authorId))),
        )
        .fetch();

how would I write it with observables?

@crimx
Copy link

crimx commented May 27, 2024

Sorry I am not familiar with advanced watermelondb API. Base on the code you posted, are you looking for something like from which converts a promise to into an observable?

@832bb9
Copy link

832bb9 commented May 27, 2024

@Stophface you need to use observe method instead of fetch then, like:

  const post = useObservableEagerState(
    useObservable(
      (inputs$) => 
        inputs$.pipe(
          switchMap$(([authorId, postId]) =>
            database
              .get('posts')
              .query(Q.where('postId', postId), Q.and(Q.where('authorId', Q.eq(authorId))))
              .observe(),
          ),
          map$((posts) => posts[0])
        ),
      [authorId, postId],
    ),
  )

Also i am not fully sure what you are trying to achieve with that query. postId seems to be already enough to find exact record in db, you don't need to check author in query then. Also Q.and expecting more then one argument, it is useless otherwise. WatermelonDB expose findAndObserve method specifically for such case.

  const post = useObservableEagerState(
    useObservable(
      (inputs$) => inputs$.pipe(switchMap$(([postId]) => database.get('posts').findAndObserve(postId)),
      [postId],
    ),
  )

But it will work only with id field, which is required. I see that you are using postId in query and I am not sure how it differ from id in your schema.

@832bb9
Copy link

832bb9 commented May 27, 2024

@crimx from is great when you don't have observables, fortunately WatermelonDB is built on top of RxJS and expose all necessary APIs to work with them. This allows component to react to db changes, while fetch method (even wrapped with observable) don't call subscribe, it just returns current state from db.

@Stophface
Copy link

@832bb9

Also i am not fully sure what you are trying to achieve with that query. postId seems to be already enough to find exact record in db, you don't need to check author in query then. Also Q.and expecting more then one argument, it is useless otherwise.

I just came up with an example that contains a query :)
When I try your suggestion

import { useMemo } from 'react';
import { useObservableState, useSubscription, useObservable } from 'observable-hooks'
import { map$, switchMap$ } from "rxjs";


const useTable$ = (database, postId) => {
    return useObservableState(
      useObservable(
        (inputs$) =>
          inputs$.pipe(
            switchMap$(([postId]) =>
              database
                .get('posts')
                .query(Q.where('id', postId))
                .observe(),
            ),
            map$(records => records[0])
          ),
        [database, postId],
      ),
    )
}


const SomeFunctionalComponent () => {
  const data$ = useTable$(database, 1)
  useSubscription(data$, records => {
    console.log(records)
  })
  return null
}

I get

TypeError: 0, _$$_REQUIRE(_dependencyMap[13], "rxjs").switchMap$ is not a function (it is undefined)

@832bb9
Copy link

832bb9 commented May 27, 2024

This is because rxjs package don't have such members, I am renaming them while import to satisfy so called Finnish Notation (https://benlesh.medium.com/observables-and-finnish-notation-df8356ed1c9b). You are free not to use it, but I just don't want to mix rxjs operators with ramda operators I am using in same files. You shouldn't add $ sign to anything except variables which holds observables or functions which returns them, thats why useTable$ and data$ is wrong. You don't need to call useSubscription, useObservableState already passes data from observable to react component world, so you can use useEffect if you want to do anything with it.

import { useDatabase } from '@nozbe/watermelondb/hooks'
import { useObservable, useObservableState } from 'observable-hooks'
import { useEffect } from 'react'
import { switchMap as switchMap$ } from 'rxjs'

const useObservePost = (postId: string) => {
  const database = useDatabase()

  const post = useObservableEagerState(
    useObservable(
      (inputs$) => inputs$.pipe(switchMap$(([database, postId]) => database.get('posts').findAndObserve(postId))),
      [database, postId],
    ),
  )

  return post
}

const SomeFunctionalComponent = () => {
  const post = useObservePost('1')

  useEffect(() => {
    console.log(post)
  }, [post])

  return null
}

@Stophface
Copy link

@832bb9 Thanks again. This works :)!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests