Connecting Components
After you define some Models, it's time to connect Watermelon to your app's interface. We're using React in this guide, however WatermelonDB can be used with any UI framework.
Note: If you're not familiar with higher-order components, read React documentation, check out recompose
… or just read the examples below to see it in practice!
Reactive components
Here's a very simple React component rendering a Comment
record:
const Comment = ({ comment }) => (
<div>
<p>{comment.body}</p>
</div>
)
Now we can fetch a comment: const comment = await commentsCollection.find(id)
and then render it: <Comment comment={comment} />
. The only problem is that this is not reactive. If the Comment is updated or deleted, the component will not re-render to reflect the changes. (Unless an update is forced manually or the parent component re-renders).
Let's enhance the component to make it observe the Comment
automatically:
import { withObservables } from '@nozbe/watermelondb/react'
const enhance = withObservables(['comment'], ({ comment }) => ({
comment // shortcut syntax for `comment: comment.observe()`
}))
const EnhancedComment = enhance(Comment)
export default EnhancedComment
Now, if we render <EnhancedComment comment={comment} />
, it will update every time the comment changes.
Reactive lists
Let's render the whole Post
with comments:
import { withObservables } from '@nozbe/watermelondb/react'
import EnhancedComment from 'components/Comment'
const Post = ({ post, comments }) => (
<article>
<h1>{post.name}</h1>
<p>{post.body}</p>
<h2>Comments</h2>
{comments.map(comment =>
<EnhancedComment key={comment.id} comment={comment} />
)}
</article>
)
const enhance = withObservables(['post'], ({ post }) => ({
post,
comments: post.comments, // Shortcut syntax for `post.comments.observe()`
}))
const EnhancedPost = enhance(Post)
export default EnhancedPost
Notice a couple of things:
We're starting with a simple non-reactive
Post
componentLike before, we enhance it by observing the
Post
. If the post name or body changes, it will re-render.To access comments, we fetch them from the database and observe using
post.comments.observe()
and inject a new propcomments
. (post.comments
is a Query created using@children
).Note that we can skip
.observe()
and just passpost.comments
for convenience —withObservables
will call observe for usBy observing the Query, the
<Post>
component will re-render if a comment is created or deletedHowever, observing the comments Query will not re-render
<Post>
if a comment is updated — we render the<EnhancedComment>
so that it observes the comment and re-renders if necessary.
Reactive relations
The <Comment>
component we made previously only renders the body of the comment but doesn't say who posted it.
Assume the Comment
model has a @relation('users', 'author_id') author
field. Let's render it:
const Comment = ({ comment, author }) => (
<div>
<p>{comment.body} — by {author.name}</p>
</div>
)
const enhance = withObservables(['comment'], ({ comment }) => ({
comment,
author: comment.author, // shortcut syntax for `comment.author.observe()`
}))
const EnhancedComment = enhance(Comment)
comment.author
is a Relation object, and we can call .observe()
on it to fetch the User
and then observe changes to it. If author's name changes, the component will re-render.
Note again that we can also pass Relation
objects directly for convenience, skipping .observe()
Reactive optional relations
Continuing the above example, if the comment has no author, the comment.author_id
must be null. If comment.author_id
has a value, the author record it refers to must be stored in the database, otherwise withObservables
will throw an error that the record was not found.
Reactive counters
Let's make a <PostExcerpt>
component to display on a list of Posts, with only a brief summary of the contents and only the number of comments it has:
const PostExcerpt = ({ post, commentCount }) => (
<div>
<h1>{post.name}</h1>
<p>{getExcerpt(post.body)}</p>
<span>{commentCount} comments</span>
</div>
)
const enhance = withObservables(['post'], ({ post }) => ({
post,
commentCount: post.comments.observeCount()
}))
const EnhancedPostExcerpt = enhance(PostExcerpt)
This is very similar to normal <Post>
. We take the Query
for post's comments, but instead of observing the list of comments, we call observeCount()
. This is far more efficient. And as always, if a new comment is posted, or one is deleted, the component will re-render with the updated count.
Hey, what about React Hooks?
We get it — HOCs are so 2017, and Hooks are the future! And we agree.
However, Hooks are not compatible with WatermelonDB's asynchronous API. You could use alternative open-source Hooks for Rx Observables, however we don't recommend that. They won't work correctly in all cases and won't be as optimized for performance with WatermelonDB as withObservables
. In the future, once Concurrent React is fully developed and published, WatermelonDB will have official hooks.
See discussion about official useObservables
Hook
Understanding withObservables
Let's unpack this:
withObservables(['post'], ({ post }) => ({
post: post.observe(),
commentCount: post.comments.observeCount()
}))
- Starting from the second argument,
({ post })
are the input props for the component. Here, we receivepost
prop with aPost
object. - These:are the enhanced props we inject. The keys are props' names, and values are
({
post: post.observe(),
commentCount: post.comments.observeCount()
})Observable
objects. Here, we override thepost
prop with an observable version, and create a newcommentCount
prop. - The first argument:
['post']
is a list of props that trigger observation restart. So if a differentpost
is passed, that new post will be observed. If you pass[]
, the rendered Post will not change. You can pass multiple prop names if any of them should cause observation to re-start. Think of it the same way as thedeps
argument you pass touseEffect
hook. - Rule of thumb: If you want to use a prop in the second arg function, pass its name in the first arg array
Advanced
- findAndObserve. If you have, say, a post ID from your Router (URL in the browser), you can use:
withObservables(['postId'], ({ postId, database }) => ({
post: database.get('posts').findAndObserve(postId)
})) - RxJS transformations. The values returned by
Model.observe()
,Query.observe()
,Relation.observe()
are RxJS Observables. You can use standard transforms like mapping, filtering, throttling, startWith to change when and how the component is re-rendered. - Custom Observables.
withObservables
is a general-purpose HOC for Observables, not just Watermelon. You can create new props from anyObservable
.
Advanced: observing sorted lists
If you have a list that's dynamically sorted (e.g. sort comments by number of likes), use Query.observeWithColumns
to ensure the list is re-rendered when its order changes:
// This is a function that sorts an array of comments according to its `likes` field
// I'm using `ramda` functions for this example, but you can do sorting however you like
const sortComments = sortWith([
descend(prop('likes'))
])
const CommentList = ({ comments }) => (
<div>
{sortComments(comments).map(comment =>
<EnhancedComment key={comment.id} comment={comment} />
)}
</div>
)
const enhance = withObservables(['post'], ({ post }) => ({
comments: post.comments.observeWithColumns(['likes'])
}))
const EnhancedCommentList = enhance(CommentList)
If you inject post.comments.observe()
into the component, the list will not re-render to change its order, only if comments are added or removed. Instead, use query.observeWithColumns()
with an array of column names you use for sorting to re-render whenever a record on the list has any of those fields changed.
Advanced: observing 2nd level relations
If you have 2nd level relations, like author's Contact
info, and want to connect it to a component as well, you cannot simply use post.author.contact.observe()
in withObservables
. Remember, post.author
is not a User
object, but a Relation
that has to be asynchronously fetched.
Before accessing and observing the Contact
relation, you need to resolve the author
itself. Here is the simplest way to do it:
import { compose } from '@nozbe/watermelondb/react'
const enhance = compose(
withObservables(['post'], ({ post }) => ({
post,
author: post.author,
})),
withObservables(['author'], ({ author }) => ({
contact: author.contact,
})),
)
const EnhancedPost = enhance(PostComponent);
If you're not familiar with function composition, read the enhance
function from top to bottom:
- first, the PostComponent is enhanced by changing the incoming
post
prop into its observable version, and by adding a newauthor
prop that will contain the fetched contents ofpost.author
- then, the enhanced component is enhanced once again, by adding a
contact
prop containing the fetched contents ofauthor.contact
.
Alternative method of observing 2nd level relations
If you are familiar with rxjs
, another way to achieve the same result is using switchMap
operator:
import { switchMap } from 'rxjs/operators'
const enhance = withObservables(['post'], ({post}) => ({
post: post,
author: post.author,
contact: post.author.observe().pipe(switchMap(author => author.contact.observe()))
}))
const EnhancedPost = enhance(PostComponent)
Now PostComponent
will have Post
, Author
and Contact
props.
2nd level optional relations
If you have an optional relation between Post
and Author
, the enhanced component might receive null
as author
prop. As you must always return an observable for the contact
prop, you can use rxjs
's of
function to create a default or empty Contact
prop:
import { of as of$ } from 'rxjs'
import { withObservables, compose } from '@nozbe/watermelondb/react'
const enhance = compose(
withObservables(['post'], ({ post }) => ({
post,
author: post.author,
})),
withObservables(['author'], ({ author }) => ({
contact: author ? author.contact.observe() : of$(null),
})),
)
With the switchMap
approach, you can do:
const enhance = withObservables(['post'], ({post}) => ({
post: post,
author: post.author,
contact: post.author.observe().pipe(
switchMap(author => author ? author.contact : of$(null))
)
}))
Database Provider
To prevent prop drilling you can use the Database Provider and the withDatabase
Higher-Order Component.
import { DatabaseProvider } from '@nozbe/watermelondb/react'
// ...
const database = new Database({
adapter,
modelClasses: [Blog, Post, Comment],
})
render(
<DatabaseProvider database={database}>
<Root />
</DatabaseProvider>, document.getElementById('application')
)
To consume the database in your components you just wrap your component like so:
import { withDatabase, compose } from '@nozbe/watermelondb/react'
// ...
export default compose(
withDatabase,
withObservables([], ({ database }) => ({
blogs: database.get('blogs').query(),
}),
)(BlogList)
The database prop in the withObservables
Higher-Order Component is provided by the database provider.
useDatabase
You can also consume Database
object using React Hooks syntax:
import { useDatabase } from '@nozbe/watermelondb/react'
const Component = () => {
const database = useDatabase()
}
Next steps
➡️ Next, learn more about custom Queries