WatermelonDB icon indicating copy to clipboard operation
WatermelonDB copied to clipboard

Simple usage in hooks

Open lud opened this issue 1 year ago • 8 comments

Hello,

I would like your advice on how to use WDB from hooks. I know queries hooks are not supported right now, but I have seen examples on the web that I can't find again. I guess it would be nice to add them as temporary solutions somewhere.

I would like to know if there is a way to simplify the following:

Subscribe to a query

// Component.tsx
const clause = useMemo(() => [Q.where('some_field', someValue)], [someValue])
const posts = useDbQuery('posts', clause)

// Hooks.ts
export function useDbQuery<T extends Model>(tableName: string, clause: Clause[]): T[] {
  const database = useDatabase()

  return useObservable<T[], [Clause[]]>((_, inputs$) =>
    inputs$.pipe(
      switchMap(([clause]) => database
        .get<T>(tableName)
        .query(...clause)
        .observe()),
    )
  , [], [clause])
}

Query from a "has many" relationship

// Component.tsx
const clause = useMemo(() => [Q.where('some_field', someValue)], [someValue])
const comments = useExtendQuery(post.comments, clause)

// Hooks.ts
export function useExtendQuery<T extends Model>(query: Query<T>, clause: Clause[]): T[] {
  return useObservable<T[], [Clause[]]>((_, inputs$) =>
    inputs$.pipe(
      switchMap(([clause]) => query
        .extend(...clause)
        .observe()),
    )
  , [], [clause])
}

I now have to add variants for observeWithColumns.

Thank you.

edit Version with columns from a relationship

export function useExtendQueryColumns<T extends Model>(query: Query<T>, clause: Clause[], observeColumns: string[]): T[] {
  const inputs: [Clause[], string[]] = [clause, observeColumns]
  return useObservable<T[], [Clause[], string[]]>((_, inputs$) =>
    inputs$.pipe(
      switchMap(([clause]) => query
        .extend(...clause)
        .observeWithColumns(observeColumns)),
    )

  , [], inputs)
}

lud avatar Oct 23 '24 00:10 lud

This might help.

heliocosta-dev avatar Oct 25 '24 22:10 heliocosta-dev

Hey thanks, this is kind of the same as what I am doing. I'll try if useObservableState can be better with what I do. The internal observable state seems to be cached forever when I change the code and the page reloads.

I hope official hooks can be made available soon.

lud avatar Oct 26 '24 14:10 lud

@isaachinman I'd be interested in your criticism for those hooks.

The drawback is that I have to freeze some data outside of the hook like this:

const commentsFilter = useMemo(
  () => [Q.where('step', stepIndex), Q.sortBy('name')],
  [stepIndex],
)
const observeColums = useRef(['step', 'name'])

But on the other side I guess joining column names and json-stringifying the query clauses is very fast anyway.

But I am not sure what are the pro/cons over useObsevableState or useObservable.

lud avatar Oct 26 '24 14:10 lud

But I am not sure what are the pro/cons over useObservableState or useObservable.

I am no rxjs expert. But my understanding is that useObservable returns a new observable, while useObservableState subscribes to an existing observable. The latter is preferable because Watermelon is already exposing observables. I found weird/buggy behaviour when trying to use useObservable, as you're essentially adding another layer of observable.

The drawback is that I have to freeze some data outside of the hook like this

Any useDbQuery type hook should take all dependencies/query data as arguments, and memoise within, in my opinion.

It's a shame we have not had a response from @radex, as I am sure we could easily put together a collaborative PR to introduce official hooks.

isaachinman avatar Oct 26 '24 14:10 isaachinman

Any useDbQuery type hook should take all dependencies/query data as arguments, and memoise within, in my opinion.

Yes that would make it far easier to use, no need for the user to think about memoization.

Ok I looked at the code and indeed the observable you create seems to be properly unsubscribed from when a new observable is given to useObservableState. I'll try to replace my queries with your code.

Yeah, a library of hooks could be very useful. In the mean time if you have other hooks to share I'd be pretty happy. If you look at my first hook above I have to call useDatabase inside. Where does the database variable compe in your useDatabaseRows ?

lud avatar Oct 26 '24 14:10 lud

The database variable is wherever you call new Database.

isaachinman avatar Oct 26 '24 14:10 isaachinman

Oh so you define the hooks there so no need to useDatabase afterwards. Got it!

lud avatar Oct 26 '24 15:10 lud

Another version to avoid JSON.stringify:

export function useExtendQueryColumns<T extends Model>(query: Query<T>, clause: Clause | Clause[], observeColumns: string[]): T[] {
  const [observableColumnsMemo, clauseMemo] = useDeepMemo(
    () => [observeColumns, arrayWrap(clause)],
    [observeColumns, clause],
  )

  const observable = useMemo(
    () => query
      .extend(...clauseMemo)
      .observeWithColumns(observableColumnsMemo)
    ,
    ([query, observableColumnsMemo, clauseMemo]),
  )

  return useObservableState(observable, [])
}

function arrayWrap<T>(itemOrList: T | T[]): T[] {
  if (Array.isArray(itemOrList)) return itemOrList
  return [itemOrList]
}

function useDeepMemo<T>(generator: () => T, dependencies: unknown[]): T {
  const ref = useRef({ value: null, deps: [] }) as MutableRefObject<{ value: null | T, deps: unknown[] }>

  if (!isEqual(ref.current.deps, dependencies)) {
    ref.current = { value: generator(), deps: dependencies }
  }

  return ref.current.value as T
}

Not sure how to benchmark that though.

lud avatar Oct 26 '24 16:10 lud