Open ericvicenti opened 5 years ago
We need hooks for watermelon db please
We need hooks asap pls
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.
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?
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.
@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
@832bb9 Could you give an example?
@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.
@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?
@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...
@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(),
[],
),
)
@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.
@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?
@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);
});
@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
.
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
}
https://github.com/Nozbe/withObservables/assets/6882794/28d0573a-abce-40bf-ad94-60ac295cda5d
@crimx Perfect, this works like a charm! Thanks a lot for sticking with me!
@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?
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?
@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.
@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.
@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)
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
}
@832bb9 Thanks again. This works :)!
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 mixrxjs
operators withramda
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 whyuseTable$
anddata$
is wrong. You don't need to calluseSubscription
,useObservableState
already passes data from observable to react component world, so you can useuseEffect
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 }
Thanks! This works like a charm. I made some changes to it so it allows one to fetch data from any table:
import { switchMap as switchMap$ } from 'rxjs';
import { useObservable, useObservableEagerState } from 'observable-hooks';
import { useDatabase } from '@nozbe/watermelondb/react';
import type { Model, Q } from '@nozbe/watermelondb';
import type { TableName } from '@models/schema';
const useDatabaseData = <T extends Model>(
tableName: TableName,
query: Q.Clause[] = [],
) => {
const database = useDatabase();
const data = useObservableEagerState(
useObservable(
inputs$ =>
inputs$.pipe(
switchMap$(([db]) => db.get(tableName).query(query).observe()),
),
[database],
),
);
return data as T[];
};
export default useDatabaseData;
Are there any performance issues or any other flaws/edge cases with the above?
Hello guys, I am having a issue and I want to make sure I understand it correctly.
I have a page structure as below and I get the data with useDatabaseData, there is no problem here.
TestPage.tsx
const data = useDatabaseData<CollectionEntity>(CollectionEntity.table, [
Q.take(100),
]);
I am using flatList in this page
const renderItem = ({item}: ListItemData<CollectionEntity>) => (
<TestItem entity={item} />
);
<FlatList data={data} renderItem={renderItem} />
--TestItem.tsx render content
<TouchableOpacity
onPress={handlePress}
style={{{padding: 10, marginBottom: 10, backgroundColor: 'red'}}>
<Text>{entity.title}</Text>
<Text>{collection?.actionType || 'None'}</Text>
</TouchableOpacity>
In TestItem.tsx, I am querying another table with the entityId information of the entity object I received as a parameter.
const data = useDatabaseData<Collection>(Collection.table, [
Q.where('entity_id', entity.entityId),
]);
const collection = data?..[0];
there is no problem here either, I have a function in the collection model within the handlePress event and it changes the actionType.
collection.changeActionType(ActionTypes.Wanna);
@writer async changeActionType(
actionType: ActionTypes | null,
): Promise<ActionTypes | null> {
if (this.actionType === actionType) {
await this.update(record => {
record.actionType = ActionTypes.None;
});
return null;
} else if (this.actionType !== actionType) {
await this.update(record => {
record.actionType = actionType;
});
return actionType;
}
await this.database.get<Collection>(this.table).create(record => {
record.entityId = this.entityId;
record.actionType = this.actionType;
record.collectionType = this.collectionType;
record.userId = this.userId;
});
return actionType;
}
However, even though I do this and the records are actually updated in the db, the collection data in TestItem.tsx is not updated.
My expectation is that where I pull observable records from the db, when the model is updated, the record will be updated everywhere I use the model.
Am I missing something, or does watermelonDb already work like this.
Hey all, I've found
withObservables
to be super handy, but now with react hooks I've been using a simple "useObservables" instead:Usage:
Note: it is best to use ObservableBehavior so that the
observable.value
can be accessed synchronously for the first render