startupjs react-sharedb-classes
Run ShareDB
in React
using Class syntax
What it does
- Brings real-time collaboration to React using ShareDB;
- Uses Racer to add a
model
to your app to do any data manipulations; - The
model
acts as a global singleton state, so you can use it as a
replacement for other state-management systems like Redux
or MobX
; - Makes the
render
reactive similar to how it's done in MobX
--
rerendering happens whenever any model
data you used in render
changes.
Installation
You don't need to install anything if you are in a StartupJS project.
For instructions on standalone usage in a pure React project refer to react-sharedb
readme
Requirements
react: 16.9 - 17
Usage with Classes
@subscribe(cb)
HOC
@subscribe
decorator is used to specify what you want to subscribe to.
@subscribe
gives react component a personal local scope model, located at path $components.<random_id>
.
This model will be automatically cleared when the component is unmounted.
HOW TO: Subscribe to data and use it in render()
@subscribe
accepts a single argument -- cb
, which receives props
and must return the subscriptions object
.
Each key
in the subscriptions object
will fetch specified data from the MongoDB or a Local path and
will write it into the component's personal scope model under that key
.
The read-only data of the component's model is available as this.props.store
.
Use it to render the data you subscribed to, same way you would use this.state
.
IMPORTANT As with this.state
, the this.props.store
SHOULD NOT be modified directly! Read below to find out how to modify data.
Example:
import React from 'react'
import {subscribe, subLocal, subDoc, subQuery} from 'startupjs'
@subscribe(props => ({
myUserId: subLocal('_session.userId'),
room: subDoc('rooms', props.roomId)
}))
@subscribe(props => ({
myUser: subDoc('users', props.store.userId),
usersInRoom: subQuery('users', {_id: {
$in: props.store.room && props.store.room.userIds || []
}})
}))
export default class Room extends React.Component {
render () {
let {room, myUser, usersInRoom} = this.props.store
return <Fragment>
<h1>{room.name}</h1>
<h2>Me: {myUser.name}</h2>
<p>Other users in the room: {usersInRoom.map(user => user.name)}</p>
</Fragment>
}
}
As seen from the example, you can combine multiple @subscribe
one after another.
HOW TO: Modify the data you subscribed to
The actual scoped model of the component is available as this.props.$store
.
Use it to modify the data. For the API to modify stuff refer to the Racer documentation
In addition, all things from subscriptions object
you subscribed to are
available to you as additional scope models in this.props
under names this.props.$KEY
Example:
import React from 'react'
import {subscribe, subLocal, subDoc, subQuery} from 'startupjs'
@subscribe(props => ({
room: subDoc('rooms', props.roomId)
}))
export default class Room extends React.Component {
updateName = () => {
let {$store, $room} = this.props
$room.set('name', 'New Name')
$store.set('room.name', 'New Name')
}
render () {
let {room} = this.props.store
return <Fragment>
<h1>{room.name}</h1>
<button onClick={this.updateName}>Update Name</button>
</Fragment>
}
}
[Classes] sub*()
functions
Use sub*() functions to define a particular subscription.
subDoc(collection, docId)
Subscribe to a particular document.
You'll receive the document data as props.store.{key}
collection
[String] -- collection name. Required
docId
[String] -- document id. Required
Example:
@subscribe(props => ({
room: subDoc('rooms', props.roomId)
}))
subQuery(collection, query)
Subscribe to the Mongo query.
You'll receive the docuents as an array: props.store.{key}
You'll also receive an array of ids of those documents as props.store.{key}Ids
collection
[String] -- collection name. Required
query
[Object] -- query (regular, $count
, $aggregate
queries are supported). Required
Example:
@subscribe(props => ({
users: subQuery('users', { roomId: props.roomId, anonymous: false })
}))
IMPORTANT: The scope model ${key}
, which you receive from subscription, targets
an the array in the local model of the component, NOT the global collection
path.
So you won't be able to use it to efficiently update document's data. Instead you should manually
create a scope model which targets a particular document, using the id:
let {usersInRoom, $store} = this.props.store
for (let user of usersInRoom) {
$store.scope(`${collection}.${user.id}`).setEach({
joinedRoom: true,
updatedAt: Date.now()
})
}
subLocal(path)
Subscribe to the data you already have in your local model by path.
You'll receive the data on that path as props.store.{key}
You will usually use it to subscribe to private collections like _page
or _session
.
This is very useful when you want to share the state between multiple components.
It's also possible to subscribe to the path from a public collection, for example when you
want to work with some nested value of a particular document you have already subscribed to.
Example:
const TopBar = subscribe(props => ({
sidebarOpened: subLocal('_page.Sidebar.opened')
}))({
$sidebarOpened
}) =>
<button
onClick={() => $sidebarOpened.setDiff(true)}
>Open Sidebar</button>
const Sidebar = subscribe(props => ({
sidebarOpened: subLocal('_page.Sidebar.opened')
}))({
store: {sidebarOpened}
}) =>
sidebarOpened ? <p>Sidebar</p> : null
subValue(value)
A constant value to assign to the local scoped model of the component.
value
[String] -- value to assign (any type)
[Classes] Example
Below is an example of a simple app with 2 components:
Home
-- sets up my userId and renders Room
Room
-- shows my user name ands lets user update it,
shows all users which are currently in the room.
import React from 'react'
import Room from './Room.jsx'
import {model, subscribe, subLocal, subDoc, subQuery} from 'startupjs'
@subscribe(props => ({
userId: subLocal('_session.userId')
}))
export default class Home extends React.Component {
constructor (...props) {
super(...props)
let {$userId} = this.props
$userId.set(this.getUserId())
}
getUserId = () => model.add('users', {})
render = () => <Room roomId={'myCoolRoom1'} />
}
import React from 'react'
import {model, subscribe, subLocal, subDoc, subQuery} from 'startupjs'
@subscribe(props => ({
userId: subLocal('_session.userId'),
room: subDoc('rooms', props.roomId)
}))
@subscribe(props => ({
myUser: subDoc('users', props.store.userId),
users: subQuery('users', {_id: {
$in: props.store.room && props.store.room.userIds || []
}})
}))
export default class Room extends React.Component {
constructor (...props) {
super(...props)
let {$room, roomId} = this.props
let {userId, room, room: {userIds = []}} = this.props.store
if (!room) model.add('rooms', {id: roomId, title: `Room #${roomId}`})
if (!userIds.includes(userId)) $room.push('userIds', userId)
}
changeName = (event) => {
let {$myUser} = this.props
$myUser.setEach({name: event.target.value})
}
render () {
let { myUser, room, users, userId } = this.props.store
return (
<main>
<h1>{room.title}</h1>
<section>
My User Name:
<input type='text' value={myUser.name} onChange={this.changeName} />
</section>
<h3>Users in the room:</h3>
{
users
.filter(({id}) => id !== userId) // exclude my user
.map(user =>
<section key={user.id}>
{user.name || `User ${user.id}`}
</section>
)
}
</main>
)
}
}
License
MIT
(c) Decision Mapper - http://decisionmapper.com