UNPKG

16.7 kBMarkdownView Raw
1# react-sharedb
2
3> Run `ShareDB` in `React`
4
5## What it does
6
71. Brings real-time collaboration to React using [ShareDB](https://github.com/share/sharedb);
82. Uses [Racer](https://derbyjs.com/docs/derby-0.10/models) to add a `model`
9 to your app to do any data manipulations;
103. The `model` acts as a global singleton state, so you can use it as a
11 replacement for other state-management systems like `Redux` or `MobX`;
124. Makes the `render` reactive similar to how it's done in `MobX` --
13 rerendering happens whenever any `model` data you used in `render`
14 changes.
15
16## *\[Hooks\]* `use*()`
17
18### `observer(FunctionalComponent)` HOF
19
20Higher Order Function which makes your functional component rendering reactive.
21**You have to** wrap your functional components in it to be able to use `react-sharedb` hooks.
22
23```js
24import {observer, useDoc} from 'react-sharedb'
25export default observer(function User ({userId}) {
26 let [user, $user] = useDoc('users', userId)
27 if (!user) return null // <Loading />
28 return (
29 <input value={user.name} onChange={e => $user.set('name', e.target.value)} />
30 )
31})
32```
33
34### `useDoc(collection, docId)`
35
36Refer to the documentation of [`subDoc()`](#subDoc) below
37
38### `useLocalDoc(collection, docId)`
39
40A convenience method to get the document you are already subscribed to
41using the same API as `useDoc()`
42
43```js
44let [game, $game] = useLocalDoc('games', gameId)
45// It's the same as doing:
46let [game, $game] = useLocal('games.' + gameId)
47```
48
49### `useQuery(collection, query)`
50
51Refer to the documentation of [`subQuery()`](#subQuery) below
52
53### `useQueryIds(collection, ids, options)`
54
55Subscribe to documents in collection by their ids
56
57`collection` \[String\] -- collection name. Required
58`ids` \[Array\] -- array of strings which should be document ids.
59`options` \[Object\] --
60 ```js
61 {
62 reverse: false // reverse the order of resulting array
63 }
64 ```
65
66Example:
67
68```js
69observer(function Players ({ gameId }) {
70 let [game] = useDoc('games', gameId)
71 if (!game) return null // <Loading />
72 let [players, $players] = useQueryIds('players', game.playerIds)
73 if (!players) return null // <Loading />
74
75 return (
76 <div>{players.map(i => i.name).join(' ,')}</div>
77 )
78})
79```
80
81### `useQueryDoc(collection, query)`
82
83Subscribe to a document using a query. It's the same as `useDoc()`, but
84with `query` parameter instead of the particular `docId`.
85`$limit: 1` and `$sort: { createdAt: -1 }` are added to the query automatically (if they don't already exist).
86
87`collection` \[String\] -- collection name. Required
88`query` \[Object\] -- query object, same as in `useQuery()`.
89
90Example:
91
92```js
93observer(function NewPlayer ({ gameId }) {
94 // { $sort: { createdAt: -1 }, $limit: 1 }
95 // is added automatically to the query, so the newest player will be returned.
96 // It's also reactive, so whenever a new player joins, you'll receive the new data and model.
97 let [newPlayer, $newPlayer] = useQueryDoc('players', { gameId })
98 if (!newPlayer) return null // <Loading />
99
100 return (
101 <div>New player joined: {newPlayer.name}</div>
102 )
103})
104```
105
106### `useLocal(path)`
107
108Refer to the documentation of [`subLocal()`](#subLocal) below
109
110### `useSession(path)`
111
112A convenience method to access the `_session` local collection.
113
114```js
115let [userId, $userId] = useSession('userId')
116// It's the same as doing:
117let [userId, $userId] = useLocal('_session.userId')
118```
119
120### `usePage(path)`
121
122A convenience method to access the `_page` local collection.
123
124```js
125let [game, $game] = usePage('game')
126// It's the same as doing:
127let [game, $game] = useLocal('_page.game')
128```
129
130### `useValue(value)`
131
132Refer to the documentation of [`subValue()`](#subValue) below
133
134### `useModel(path)`
135
136Return a model scoped to `path` (memoized by the `path` argument).
137If `path` is not provided, returns the model scoped to the root path.
138
139```js
140import React from 'react'
141import {render} from 'react-dom'
142import {observer, useModel, useLocal} from 'react-sharedb'
143
144const Main = observer(() => {
145 return (
146 <div style={{display: 'flex'}}>
147 <Content />
148 <Sidebar />
149 </div>
150 )
151})
152
153const Content = observer(() => {
154 let $showSidebar = useModel('_page.Sidebar.show')
155
156 // sidebar will be opened without triggering rerendering of the <Content /> component (this component)
157 return (
158 <div>
159 <p>I am Content</p>
160 <button onClick={() => $showSidebar.setDiff(true)}>Open Sidebar</button>
161 </div>
162 )
163})
164
165const Sidebar = observer(() => {
166 let [show, $show] = useLocal('_page.Sidebar.show')
167 if (!show) return null
168 return (
169 <div>
170 <p>I am Sidebar</p>
171 <button onClick={() => $show.del()}>Close</button>
172 </div>
173 )
174})
175
176render(<Main />, document.body.appendChild(document.createElement('div')))
177```
178
179### *\[Hooks\]* Example
180
181```js
182import React from 'react'
183import {observer, useDoc, useQuery, useLocal, useValue} from 'react-sharedb'
184
185export default observer(function Game ({gameId}) {
186 let [secret, $secret] = useValue('Game Secret Password')
187 let [userId, $userId] = useLocal('_session.userId')
188 let [user, $user] = useDoc('users', userId)
189 let [game, $game] = useDoc('games', gameId)
190 if (!(game && user)) return null // <Loading />
191
192 let [players, $players] = useQuery('players', {_id: {$in: game.playerIds}})
193 if (!players) return null // <Loading />
194
195 let [users, $users] = useQuery('users', {_id: {$in: players.map(i => i.userId)}})
196 if (!users) return null // <Loading />
197
198 function updateSecret (event) {
199 $secret.set(event.target.value)
200 }
201
202 function updateName (event) {
203 $user.set('name', event.target.value)
204 }
205
206 return (
207 <div>
208 <label>
209 Secret:
210 <input type='text' value={code} onChange={updateSecret} />
211 </label>
212
213 <label>
214 My User Name:
215 <input type='text' value={user.name} onChange={updateName} />
216 </label>
217
218 <h1>Game {game.title}</h1>
219
220 <h2>Users in game:</h2>
221 <p>{users.map(i => i.name).join(', ')}</p>
222 </div>
223 )
224})
225```
226
227### TODO:
228
2291. `useSubscribe(fns)`
2302. `<Suspense />` support
231
232## *\[Classes\]* HOC `@subscribe(cb)`
233
234`@subscribe` decorator is used to specify what you want to subscribe to.
235
236`@subscribe` gives react component a personal local scope model, located at path `$components.<random_id>`.
237This model will be automatically cleared when the component is unmounted.
238
239### HOW TO: Subscribe to data and use it in render()
240
241`@subscribe` accepts a single argument -- `cb`, which receives `props` and must return the `subscriptions object`.
242
243Each `key` in the `subscriptions object` will fetch specified data from the MongoDB or a Local path and
244will write it into the component's personal scope model under that `key`.
245
246The read-only data of the component's model is available as `this.props.store`.
247Use it to render the data you subscribed to, same way you would use `this.state`.
248
249**IMPORTANT** As with `this.state`, the `this.props.store` **SHOULD NOT** be modified directly! Read below to find out how to modify data.
250
251Example:
252
253```js
254import React from 'react'
255import {subscribe, subLocal, subDoc, subQuery} from 'react-sharedb'
256
257@subscribe(props => ({
258 myUserId: subLocal('_session.userId'),
259 room: subDoc('rooms', props.roomId)
260}))
261@subscribe(props => ({
262 myUser: subDoc('users', props.store.userId),
263 usersInRoom: subQuery('users', {_id: {
264 $in: props.store.room && props.store.room.userIds || []
265 }})
266}))
267export default class Room extends React.Component {
268 render () {
269 let {room, myUser, usersInRoom} = this.props.store
270 return <Fragment>
271 <h1>{room.name}</h1>
272 <h2>Me: {myUser.name}</h2>
273 <p>Other users in the room: {usersInRoom.map(user => user.name)}</p>
274 </Fragment>
275 }
276}
277```
278
279As seen from the example, you can combine multiple `@subscribe` one after another.
280
281### HOW TO: Modify the data you subscribed to
282
283The actual scoped model of the component is available as `this.props.$store`.
284Use it to modify the data. For the API to modify stuff refer to the [Racer documentation](https://derbyjs.com/docs/derby-0.10/models)
285
286In addition, all things from `subscriptions object` you subscribed to are
287available to you as additional scope models in `this.props` under names `this.props.$KEY`
288
289Example:
290
291```js
292import React from 'react'
293import {subscribe, subLocal, subDoc, subQuery} from 'react-sharedb'
294
295@subscribe(props => ({
296 room: subDoc('rooms', props.roomId)
297}))
298export default class Room extends React.Component {
299 updateName = () => {
300 let {$store, $room} = this.props
301 $room.set('name', 'New Name')
302 // You can also use $store to do the same:
303 $store.set('room.name', 'New Name')
304 }
305 render () {
306 let {room} = this.props.store
307 return <Fragment>
308 <h1>{room.name}</h1>
309 <button onClick={this.updateName}>Update Name</button>
310 </Fragment>
311 }
312}
313```
314
315## *\[Classes\]* `sub*()` functions
316
317Use sub*() functions to define a particular subscription.
318
319<a name="subDoc"></a>
320### `subDoc(collection, docId)`
321
322Subscribe to a particular document.
323You'll receive the document data as `props.store.{key}`
324
325`collection` \[String\] -- collection name. Required
326`docId` \[String\] -- document id. Required
327
328Example:
329
330```js
331@subscribe(props => ({
332 room: subDoc('rooms', props.roomId)
333}))
334```
335
336<a name="subQuery"></a>
337### `subQuery(collection, query)`
338
339Subscribe to the Mongo query.
340You'll receive the docuents as an array: `props.store.{key}`
341You'll also receive an array of ids of those documents as `props.store.{key}Ids`
342
343`collection` \[String\] -- collection name. Required
344`query` \[Object\] -- query (regular, `$count`, `$aggregate` queries are supported). Required
345
346Example:
347
348```js
349@subscribe(props => ({
350 users: subQuery('users', { roomId: props.roomId, anonymous: false })
351}))
352```
353
354**IMPORTANT:** The scope model `${key}`, which you receive from subscription, targets
355an the array in the local model of the component, NOT the global `collection` path.
356So you won't be able to use it to efficiently update document's data. Instead you should manually
357create a scope model which targets a particular document, using the id:
358
359```js
360let {usersInRoom, $store} = this.props.store
361for (let user of usersInRoom) {
362 $store.scope(`${collection}.${user.id}`).setEach({
363 joinedRoom: true,
364 updatedAt: Date.now()
365 })
366}
367```
368
369<a name="subLocal"></a>
370### `subLocal(path)`
371
372Subscribe to the data you already have in your local model by path.
373You'll receive the data on that path as `props.store.{key}`
374
375You will usually use it to subscribe to private collections like `_page` or `_session`.
376This is very useful when you want to share the state between multiple components.
377
378It's also possible to subscribe to the path from a public collection, for example when you
379want to work with some nested value of a particular document you have already subscribed to.
380
381Example:
382
383```js
384const TopBar = subscribe(props => ({
385 sidebarOpened: subLocal('_page.Sidebar.opened')
386}))({
387 $sidebarOpened
388}) =>
389 <button
390 onClick={() => $sidebarOpened.setDiff(true)}
391 >Open Sidebar</button>
392
393const Sidebar = subscribe(props => ({
394 sidebarOpened: subLocal('_page.Sidebar.opened')
395}))({
396 store: {sidebarOpened}
397}) =>
398 sidebarOpened ? <p>Sidebar</p> : null
399```
400
401<a name="subValue"></a>
402### `subValue(value)`
403
404A constant value to assign to the local scoped model of the component.
405
406`value` \[String\] -- value to assign (any type)
407
408### *\[Classes\]* Example
409
410Below is an example of a simple app with 2 components:
4111. `Home` -- sets up my userId and renders `Room`
4122. `Room` -- shows my user name ands lets user update it,
413 shows all users which are currently in the room.
414
415```js
416// Home.jsx
417import React from 'react'
418import Room from './Room.jsx'
419import {model, subscribe, subLocal, subDoc, subQuery} from 'react-sharedb'
420
421// `subscribe` means that the data is reactive and gets dynamically updated
422// whenever the data in MongoDB or in private collections gets updated.
423
424@subscribe(props => ({
425 // Subscribe to the path in a private collection `_session`.
426 //
427 // `private` collection means that the data does NOT get synced with MongoDB.
428 // Data in these collections live only on the client-side.
429 //
430 // Collection is considered `private` when its name starts from `_` or `$`.
431 //
432 // Private collections like `_session` are used as a singleton client-only storage,
433 // an alternative to `Redux` or `MobX`.
434 //
435 // You can have as many private collections as you like, but the common
436 // guideline is to use just one collection to store all private data -- `_session`
437 userId: subLocal('_session.userId')
438}))
439
440export default class Home extends React.Component {
441 constructor (...props) {
442 super(...props)
443
444 // For each thing you subscribe to, you receive a scoped `model`
445 // with the same name prefixed with `$` in `props`. Use it
446 // to update the data with the `model` operations available in Racer:
447 // https://derbyjs.com/docs/derby-0.10/models
448 let {$userId} = this.props
449
450 // Update the private path `_session.userId` with the new value
451 //
452 // We'll use this `_session.userId` in all other children
453 // components to easily get the userId without doing the potentially
454 // heavy/long process of fetching the userId over and over again.
455 $userId.set(this.getUserId())
456 }
457
458 // Get my userId somehow (for example from the server-side response).
459 // For simplicity, we'll just generate a random guid in this example
460 // by creating an empty user document each time, so whenever
461 // you open a new page, you'll be a new user.
462 getUserId = () => model.add('users', {})
463
464 render = () => <Room roomId={'myCoolRoom1'} />
465}
466```
467
468```js
469// Room.jsx
470import React from 'react'
471import {model, subscribe, subLocal, subDoc, subQuery} from 'react-sharedb'
472
473@subscribe(props => ({
474
475 // Subscribe to the same private path again to get the userId, which
476 // we did manually setup in the parent `<Home>` component.
477 // The pattern of subscribing and updating data in a private path
478 // can be used to expose some data from one component to another.
479 userId: subLocal('_session.userId'),
480
481 // Subscribe to the particular document from a public collection `rooms` by id.
482 // `public` means that it DOES sync with MongoDB.
483 // `public` collection names start with a regular letter a-z.
484 // You can also use ClassCase for collection names (A-Z), but it is
485 // NOT recommended unless you have such guidelines in your project.
486 room: subDoc('rooms', props.roomId)
487
488}))
489
490// All things you subscribe to end up in `props.store` under the same name.
491// We can do a second subscribe using the data we received in the first one.
492
493@subscribe(props => ({
494
495 // Subscribe to the particular document from a public collection by id
496 // which we got from the previous subscription
497 myUser: subDoc('users', props.store.userId),
498
499 // Subscribe to a query to get docs from a public `users` collection
500 // using the array of userIds from `room` received in the previous subscription
501 users: subQuery('users', {_id: {
502 $in: props.store.room && props.store.room.userIds || []
503 }})
504
505}))
506
507// Additionally, whenever you subscribe to the MongoDB query, you also
508// receive the `Ids` in store.
509// For example, subscribing to the `users` collection above populated
510// `props.store.users` (array of documents) AND
511// `props.store.userIds` (array of ids) - auto singular name with the `Ids` suffix
512
513export default class Room extends React.Component {
514 constructor (...props) {
515 super(...props)
516 let {$room, roomId} = this.props
517 let {userId, room, room: {userIds = []}} = this.props.store
518
519 // Create an empty room if it wasn't created yet
520 if (!room) model.add('rooms', {id: roomId, title: `Room #${roomId}`})
521
522 // Add user to the room unless he's already there
523 if (!userIds.includes(userId)) $room.push('userIds', userId)
524 }
525
526 changeName = (event) => {
527 let {$myUser} = this.props
528 $myUser.setEach({name: event.target.value})
529 }
530
531 render () {
532 let { myUser, room, users, userId } = this.props.store
533 return (
534 <main>
535 <h1>{room.title}</h1>
536 <section>
537 My User Name:
538 <input type='text' value={myUser.name} onChange={this.changeName} />
539 </section>
540 <h3>Users in the room:</h3>
541 {
542 users
543 .filter(({id}) => id !== userId) // exclude my user
544 .map(user =>
545 <section key={user.id}>
546 {user.name || `User ${user.id}`}
547 </section>
548 )
549 }
550 </main>
551 )
552 }
553}
554```
555
556## Licence
557
558MIT
559
560(c) Decision Mapper - http://decisionmapper.com