1 | # startupjs react-sharedb
|
2 |
|
3 | > Run `ShareDB` in `React`
|
4 |
|
5 | ## What it does
|
6 |
|
7 | 1. Brings real-time collaboration to React using [ShareDB](https://github.com/share/sharedb);
|
8 | 2. Uses [Racer](https://derbyjs.com/docs/derby-0.10/models) to add a `model`
|
9 | to your app to do any data manipulations;
|
10 | 3. 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`;
|
12 | 4. 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 | ## Installation
|
17 |
|
18 | It's recommended to just use `startupjs` package, since it proxies the API of `@startupjs/react-sharedb`.
|
19 |
|
20 | ```
|
21 | yarn add startupjs
|
22 | ```
|
23 |
|
24 | ## Hooks Usage
|
25 |
|
26 | ### `observer(FunctionalComponent, options)` HOF
|
27 |
|
28 | Higher Order Function which makes your functional component rendering reactive.
|
29 | **You have to** wrap your functional components in it to be able to use `react-sharedb` hooks.
|
30 |
|
31 | `options` object have the following properties:
|
32 | * `forwardRef` - pass `true` to use `React.forwardRef` over the inner component
|
33 | * `suspenseProps`
|
34 | * `fallback` - A React element (ie. `<MyComponent />`)
|
35 |
|
36 | ```js
|
37 | import {observer, useDoc} from 'startupjs'
|
38 |
|
39 | export default observer(function User ({userId}) {
|
40 | let [user, $user] = useDoc('users', userId)
|
41 | return (
|
42 | <input value={user.name} onChange={e => $user.set('name', e.target.value)} />
|
43 | )
|
44 | })
|
45 | ```
|
46 |
|
47 | ### `useDoc(collection, docId)`
|
48 |
|
49 | Subscribe to the particular Mongo document by id.
|
50 |
|
51 | `collection` \[String\] -- collection name. Required
|
52 |
|
53 | `docId` \[String\] -- document id. Required
|
54 |
|
55 | **Returns:** `[doc, $doc]`, where:
|
56 |
|
57 | `doc` \[Object\] -- value of document
|
58 |
|
59 | `$doc` \[Model\] -- scoped model targeting path `collection.docId`
|
60 |
|
61 | **Example:**
|
62 |
|
63 | ```js
|
64 | import React from 'react'
|
65 | import { observer, useDoc } from 'startupjs'
|
66 |
|
67 | export default observer(function Room ({
|
68 | roomId = 'DUMMY_ID'
|
69 | }) {
|
70 | let [room, $room] = useDoc('rooms', roomId)
|
71 |
|
72 | // If the document with an `_id` of `roomId` doesn't exist yet, we create it.
|
73 | // We have to wait for the document to be created by throwing the promise.
|
74 | if (!room) throw $room.createAsync({ title: 'New Room' })
|
75 |
|
76 | function onChange (e) {
|
77 | $room.set('title', e.target.value)
|
78 | }
|
79 |
|
80 | return <input onChange={onChange} value={room.title} />
|
81 | })
|
82 | ```
|
83 |
|
84 | **IMPORTANT:** The id of the document is stored internally in Mongo inside the `_id` field.
|
85 | But when it gets into the model, it gets replaced with the `id` field instead, and vice versa.
|
86 |
|
87 | ### `useQuery(collection, query)`
|
88 |
|
89 | Subscribe to the Mongo query.
|
90 |
|
91 | `collection` \[String\] -- collection name. Required
|
92 |
|
93 | `query` \[Object\] -- query (regular, `$count`, `$aggregate` queries are supported). Required
|
94 |
|
95 | **Returns:** `[docs, $docs]`, where:
|
96 |
|
97 | `docs` \[Array\] -- array of documents
|
98 |
|
99 | `$docs` \[Model\] -- scoped model targeting the whole `collection`
|
100 |
|
101 | **Example:**
|
102 |
|
103 | ```js
|
104 | let [users, $users] = useQuery('users', { roomId: props.roomId, anonymous: false })
|
105 | ```
|
106 |
|
107 | **IMPORTANT:** The scoped model `$docs`, which you receive from the hook, targets the global collection path.
|
108 | You can use it to easily reach a document with a particular id using scoped models:
|
109 |
|
110 | ```js
|
111 | let [users, $users] = useQuery('users', { roomId, anonymous: false })
|
112 | for (let user of users) {
|
113 | $users.at(user.id).setEach({
|
114 | joinedRoom: true,
|
115 | updatedAt: Date.now()
|
116 | })
|
117 | }
|
118 | ```
|
119 |
|
120 | ### `useQueryIds(collection, ids, options)`
|
121 |
|
122 | Subscribe to documents in collection by their ids
|
123 |
|
124 | `collection` \[String\] -- collection name. Required
|
125 |
|
126 | `ids` \[Array\] -- array of strings which should be document ids.
|
127 |
|
128 | `options` \[Object\] --
|
129 |
|
130 | ```js
|
131 | {
|
132 | reverse: false // reverse the order of resulting array
|
133 | }
|
134 | ```
|
135 |
|
136 | **Example:**
|
137 |
|
138 | ```js
|
139 | export default observer(function Players ({ gameId }) {
|
140 | let [game] = useDoc('games', gameId)
|
141 | let [players, $players] = useQueryIds('players', game.playerIds)
|
142 |
|
143 | return (
|
144 | <div>{players.map(i => i.name).join(' ,')}</div>
|
145 | )
|
146 | })
|
147 | ```
|
148 |
|
149 | ### `useQueryDoc(collection, query)`
|
150 |
|
151 | Subscribe to a document using a query. It's the same as `useDoc()`, but
|
152 | with `query` parameter instead of the particular `docId`.
|
153 | `$limit: 1` and `$sort: { createdAt: -1 }` are added to the query automatically (if they don't already exist).
|
154 |
|
155 | `collection` \[String\] -- collection name. Required
|
156 |
|
157 | `query` \[Object\] -- query object, same as in `useQuery()`.
|
158 |
|
159 | **Example:**
|
160 |
|
161 | ```js
|
162 | export default observer(function NewPlayer ({ gameId }) {
|
163 | // { $sort: { createdAt: -1 }, $limit: 1 }
|
164 | // is added automatically to the query, so the newest player will be returned.
|
165 | // It's also reactive, so whenever a new player joins, you'll receive the new data and model.
|
166 | let [newPlayer, $newPlayer] = useQueryDoc('players', { gameId })
|
167 | if (!newPlayer) return null // <Loading />
|
168 |
|
169 | return (
|
170 | <div>New player joined: {newPlayer.name}</div>
|
171 | )
|
172 | })
|
173 | ```
|
174 |
|
175 | ### `useLocal(path)`
|
176 |
|
177 | Subscribe to the data you already have in your local model by path.
|
178 |
|
179 | You will usually use it to subscribe to private collections like `_page` or `_session`.
|
180 | This is very useful when you want to share the state between multiple components.
|
181 |
|
182 | It's also possible to subscribe to the path from a public collection, for example when you
|
183 | want to work with some nested value of a particular document you have already subscribed to.
|
184 |
|
185 | **Returns:** `[value, $value]`, where:
|
186 |
|
187 | `value` \[any\] -- data, located on that `path`
|
188 |
|
189 | `$value` \[Model\] -- model, targeting that `path`
|
190 |
|
191 | **Example:**
|
192 |
|
193 | ```js
|
194 | const SIDEBAR_OPENED = '_page.Sidebar.opened'
|
195 |
|
196 | const Topbar = observer(() => {
|
197 | let [sidebarOpened, $sidebarOpened] = useLocal(SIDEBAR_OPENED)
|
198 | return <>
|
199 | <button
|
200 | onClick={() => $sidebarOpened.set(!sidebarOpened)}
|
201 | >Toggle Sidebar</button>
|
202 | </>
|
203 | })
|
204 |
|
205 | const Sidebar = observer(() => {
|
206 | let [sidebarOpened] = useLocal(SIDEBAR_OPENED)
|
207 | return sidebarOpened ? <p>Sidebar</p> : null
|
208 | })
|
209 |
|
210 | const App = observer(() => {
|
211 | return <>
|
212 | <Topbar />
|
213 | <Sidebar />
|
214 | </>
|
215 | })
|
216 | ```
|
217 |
|
218 | ### `useSession(path)`
|
219 |
|
220 | A convenience method to access the `_session` local collection.
|
221 |
|
222 | ```js
|
223 | let [userId, $userId] = useSession('userId')
|
224 | // It's the same as doing:
|
225 | let [userId, $userId] = useLocal('_session.userId')
|
226 | ```
|
227 |
|
228 | ### `usePage(path)`
|
229 |
|
230 | A convenience method to access the `_page` local collection.
|
231 |
|
232 | **Example:**
|
233 |
|
234 | ```js
|
235 | let [game, $game] = usePage('game')
|
236 | // It's the same as doing:
|
237 | let [game, $game] = useLocal('_page.game')
|
238 | ```
|
239 |
|
240 | ### `useValue(defaultValue)`
|
241 |
|
242 | An observable alternative to `useState`.
|
243 |
|
244 | **Example:**
|
245 |
|
246 | ```js
|
247 | const DEFAULT_USER = {
|
248 | first: 'John',
|
249 | last: 'Smith',
|
250 | address: 'Washington St.'
|
251 | }
|
252 |
|
253 | const Field = observer(({ label, $value }) => {
|
254 | return <div>
|
255 | <span>{label}: </span>
|
256 | <input value={$value.get()} onChange={e => $value.set(e.target.value)} />
|
257 | </div>
|
258 | })
|
259 |
|
260 | const User = observer(() => {
|
261 | let [user, $user] = useValue(DEFAULT_USER)
|
262 |
|
263 | return <>
|
264 | <Field label='First' $value={$user.at('first')} />
|
265 | <Field label='Last' $value={$user.at('last')} />
|
266 | <Field label='Address' $value={$user.at('address')} />
|
267 | <code>{user}</code>
|
268 | </>
|
269 | })
|
270 | ```
|
271 |
|
272 | ### `useModel(path)`
|
273 |
|
274 | Return a model scoped to `path` (memoized by the `path` argument).
|
275 | If `path` is not provided, returns the model scoped to the root path.
|
276 |
|
277 | **Example:**
|
278 |
|
279 | ```js
|
280 | import React from 'react'
|
281 | import {render} from 'react-dom'
|
282 | import {observer, useModel, useLocal} from 'startupjs'
|
283 |
|
284 | const Main = observer(() => {
|
285 | return (
|
286 | <div style={{display: 'flex'}}>
|
287 | <Content />
|
288 | <Sidebar />
|
289 | </div>
|
290 | )
|
291 | })
|
292 |
|
293 | const Content = observer(() => {
|
294 | let $showSidebar = useModel('_page.Sidebar.show')
|
295 |
|
296 | // sidebar will be opened without triggering rerendering of the <Content /> component (this component)
|
297 | return (
|
298 | <div>
|
299 | <p>I am Content</p>
|
300 | <button onClick={() => $showSidebar.setDiff(true)}>Open Sidebar</button>
|
301 | </div>
|
302 | )
|
303 | })
|
304 |
|
305 | const Sidebar = observer(() => {
|
306 | let [show, $show] = useLocal('_page.Sidebar.show')
|
307 | if (!show) return null
|
308 | return (
|
309 | <div>
|
310 | <p>I am Sidebar</p>
|
311 | <button onClick={() => $show.del()}>Close</button>
|
312 | </div>
|
313 | )
|
314 | })
|
315 |
|
316 | render(<Main />, document.body.appendChild(document.createElement('div')))
|
317 | ```
|
318 |
|
319 | ### Batching
|
320 | React batch updates during a synchronous lifecycle method or during event handlers. For other cases see examples below:
|
321 |
|
322 | ```js
|
323 | import React from 'react'
|
324 | import { observer, batch, useDoc } from 'startupjs'
|
325 | import axios from 'axios'
|
326 |
|
327 | export default observer(function Game ({gameId}) {
|
328 | const [userId, $userId] = useLocal('_session.userId')
|
329 | const [user, $user] = useDoc('users', userId)
|
330 | const [game, $game] = useDoc('games', gameId)
|
331 |
|
332 | function startGame () {
|
333 | await axios.post('/api/start-game', { gameId })
|
334 | batch(() => {
|
335 | $user.set('activeGameId', gameId)
|
336 | $game.set('startAt', +new Date())
|
337 | })
|
338 | }
|
339 |
|
340 | return (
|
341 | <button onClick={startGame}>Start game</button>
|
342 | )
|
343 | ```
|
344 |
|
345 | ```js
|
346 | import React from 'react'
|
347 | import { observer, batch, useDoc, useQuery } from 'startupjs'
|
348 |
|
349 | export default observer(function Game ({ gameId }) {
|
350 | const [game, $game] = useDoc('games', gameId)
|
351 |
|
352 | function startGame () {
|
353 | const $$players = $root.query('players', { gameId })
|
354 | await $root.subscribeAsync($$players)
|
355 | const playerIds = $$players.getIds()
|
356 | const promises = []
|
357 | const startAt = +new Date()
|
358 |
|
359 | batch(() => {
|
360 | playerIds.forEach(playerId => {
|
361 | const $player = $root.scope(`players.${playerId}`)
|
362 | promises.push($player.setAsync('startAt', startAt))
|
363 | })
|
364 | })
|
365 |
|
366 | await Promise.all(promises)
|
367 | }
|
368 |
|
369 | return (
|
370 | <button onClick={startGame}>Start game</button>
|
371 | )
|
372 | })
|
373 | ```
|
374 |
|
375 | ### Hooks Example
|
376 |
|
377 | ```js
|
378 | import React from 'react'
|
379 | import {observer, useDoc, useQuery, useLocal, useValue} from 'startupjs'
|
380 |
|
381 | export default observer(function Game ({gameId}) {
|
382 | let [secret, $secret] = useValue('Game Secret Password')
|
383 | let [userId, $userId] = useLocal('_session.userId')
|
384 | let [user, $user] = useDoc('users', userId)
|
385 | let [game, $game] = useDoc('games', gameId)
|
386 | let [players, $players] = useQuery('players', {_id: {$in: game.playerIds}})
|
387 | let [users, $users] = useQuery('users', {_id: {$in: players.map(i => i.userId)}})
|
388 |
|
389 | function updateSecret (event) {
|
390 | $secret.set(event.target.value)
|
391 | }
|
392 |
|
393 | function updateName (event) {
|
394 | $user.set('name', event.target.value)
|
395 | }
|
396 |
|
397 | return (
|
398 | <div>
|
399 | <label>
|
400 | Secret:
|
401 | <input type='text' value={code} onChange={updateSecret} />
|
402 | </label>
|
403 |
|
404 | <label>
|
405 | My User Name:
|
406 | <input type='text' value={user.name} onChange={updateName} />
|
407 | </label>
|
408 |
|
409 | <h1>Game {game.title}</h1>
|
410 |
|
411 | <h2>Users in game:</h2>
|
412 | <p>{users.map(i => i.name).join(', ')}</p>
|
413 | </div>
|
414 | )
|
415 | })
|
416 | ```
|
417 |
|
418 | ## Classes Usage
|
419 |
|
420 | ### `@subscribe(cb)` HOC
|
421 |
|
422 | `@subscribe` decorator is used to specify what you want to subscribe to.
|
423 |
|
424 | `@subscribe` gives react component a personal local scope model, located at path `$components.<random_id>`.
|
425 | This model will be automatically cleared when the component is unmounted.
|
426 |
|
427 | ### HOW TO: Subscribe to data and use it in render()
|
428 |
|
429 | `@subscribe` accepts a single argument -- `cb`, which receives `props` and must return the `subscriptions object`.
|
430 |
|
431 | Each `key` in the `subscriptions object` will fetch specified data from the MongoDB or a Local path and
|
432 | will write it into the component's personal scope model under that `key`.
|
433 |
|
434 | The read-only data of the component's model is available as `this.props.store`.
|
435 | Use it to render the data you subscribed to, same way you would use `this.state`.
|
436 |
|
437 | **IMPORTANT** As with `this.state`, the `this.props.store` **SHOULD NOT** be modified directly! Read below to find out how to modify data.
|
438 |
|
439 | Example:
|
440 |
|
441 | ```js
|
442 | import React from 'react'
|
443 | import {subscribe, subLocal, subDoc, subQuery} from 'startupjs'
|
444 |
|
445 | @subscribe(props => ({
|
446 | myUserId: subLocal('_session.userId'),
|
447 | room: subDoc('rooms', props.roomId)
|
448 | }))
|
449 | @subscribe(props => ({
|
450 | myUser: subDoc('users', props.store.userId),
|
451 | usersInRoom: subQuery('users', {_id: {
|
452 | $in: props.store.room && props.store.room.userIds || []
|
453 | }})
|
454 | }))
|
455 | export default class Room extends React.Component {
|
456 | render () {
|
457 | let {room, myUser, usersInRoom} = this.props.store
|
458 | return <Fragment>
|
459 | <h1>{room.name}</h1>
|
460 | <h2>Me: {myUser.name}</h2>
|
461 | <p>Other users in the room: {usersInRoom.map(user => user.name)}</p>
|
462 | </Fragment>
|
463 | }
|
464 | }
|
465 | ```
|
466 |
|
467 | As seen from the example, you can combine multiple `@subscribe` one after another.
|
468 |
|
469 | ### HOW TO: Modify the data you subscribed to
|
470 |
|
471 | The actual scoped model of the component is available as `this.props.$store`.
|
472 | Use it to modify the data. For the API to modify stuff refer to the [Racer documentation](https://derbyjs.com/docs/derby-0.10/models)
|
473 |
|
474 | In addition, all things from `subscriptions object` you subscribed to are
|
475 | available to you as additional scope models in `this.props` under names `this.props.$KEY`
|
476 |
|
477 | Example:
|
478 |
|
479 | ```js
|
480 | import React from 'react'
|
481 | import {subscribe, subLocal, subDoc, subQuery} from 'startupjs'
|
482 |
|
483 | @subscribe(props => ({
|
484 | room: subDoc('rooms', props.roomId)
|
485 | }))
|
486 | export default class Room extends React.Component {
|
487 | updateName = () => {
|
488 | let {$store, $room} = this.props
|
489 | $room.set('name', 'New Name')
|
490 | // You can also use $store to do the same:
|
491 | $store.set('room.name', 'New Name')
|
492 | }
|
493 | render () {
|
494 | let {room} = this.props.store
|
495 | return <Fragment>
|
496 | <h1>{room.name}</h1>
|
497 | <button onClick={this.updateName}>Update Name</button>
|
498 | </Fragment>
|
499 | }
|
500 | }
|
501 | ```
|
502 |
|
503 | ## *\[Classes\]* `sub*()` functions
|
504 |
|
505 | Use sub*() functions to define a particular subscription.
|
506 |
|
507 | <a name="subDoc"></a>
|
508 | ### `subDoc(collection, docId)`
|
509 |
|
510 | Subscribe to a particular document.
|
511 | You'll receive the document data as `props.store.{key}`
|
512 |
|
513 | `collection` \[String\] -- collection name. Required
|
514 | `docId` \[String\] -- document id. Required
|
515 |
|
516 | Example:
|
517 |
|
518 | ```js
|
519 | @subscribe(props => ({
|
520 | room: subDoc('rooms', props.roomId)
|
521 | }))
|
522 | ```
|
523 |
|
524 | <a name="subQuery"></a>
|
525 | ### `subQuery(collection, query)`
|
526 |
|
527 | Subscribe to the Mongo query.
|
528 | You'll receive the docuents as an array: `props.store.{key}`
|
529 | You'll also receive an array of ids of those documents as `props.store.{key}Ids`
|
530 |
|
531 | `collection` \[String\] -- collection name. Required
|
532 | `query` \[Object\] -- query (regular, `$count`, `$aggregate` queries are supported). Required
|
533 |
|
534 | Example:
|
535 |
|
536 | ```js
|
537 | @subscribe(props => ({
|
538 | users: subQuery('users', { roomId: props.roomId, anonymous: false })
|
539 | }))
|
540 | ```
|
541 |
|
542 | **IMPORTANT:** The scope model `${key}`, which you receive from subscription, targets
|
543 | an the array in the local model of the component, NOT the global `collection` path.
|
544 | So you won't be able to use it to efficiently update document's data. Instead you should manually
|
545 | create a scope model which targets a particular document, using the id:
|
546 |
|
547 | ```js
|
548 | let {usersInRoom, $store} = this.props.store
|
549 | for (let user of usersInRoom) {
|
550 | $store.scope(`${collection}.${user.id}`).setEach({
|
551 | joinedRoom: true,
|
552 | updatedAt: Date.now()
|
553 | })
|
554 | }
|
555 | ```
|
556 |
|
557 | <a name="subLocal"></a>
|
558 | ### `subLocal(path)`
|
559 |
|
560 | Subscribe to the data you already have in your local model by path.
|
561 | You'll receive the data on that path as `props.store.{key}`
|
562 |
|
563 | You will usually use it to subscribe to private collections like `_page` or `_session`.
|
564 | This is very useful when you want to share the state between multiple components.
|
565 |
|
566 | It's also possible to subscribe to the path from a public collection, for example when you
|
567 | want to work with some nested value of a particular document you have already subscribed to.
|
568 |
|
569 | Example:
|
570 |
|
571 | ```js
|
572 | const TopBar = subscribe(props => ({
|
573 | sidebarOpened: subLocal('_page.Sidebar.opened')
|
574 | }))({
|
575 | $sidebarOpened
|
576 | }) =>
|
577 | <button
|
578 | onClick={() => $sidebarOpened.setDiff(true)}
|
579 | >Open Sidebar</button>
|
580 |
|
581 | const Sidebar = subscribe(props => ({
|
582 | sidebarOpened: subLocal('_page.Sidebar.opened')
|
583 | }))({
|
584 | store: {sidebarOpened}
|
585 | }) =>
|
586 | sidebarOpened ? <p>Sidebar</p> : null
|
587 | ```
|
588 |
|
589 | <a name="subValue"></a>
|
590 | ### `subValue(value)`
|
591 |
|
592 | A constant value to assign to the local scoped model of the component.
|
593 |
|
594 | `value` \[String\] -- value to assign (any type)
|
595 |
|
596 | ### *\[Classes\]* Example
|
597 |
|
598 | Below is an example of a simple app with 2 components:
|
599 | 1. `Home` -- sets up my userId and renders `Room`
|
600 | 2. `Room` -- shows my user name ands lets user update it,
|
601 | shows all users which are currently in the room.
|
602 |
|
603 | ```js
|
604 | // Home.jsx
|
605 | import React from 'react'
|
606 | import Room from './Room.jsx'
|
607 | import {model, subscribe, subLocal, subDoc, subQuery} from 'startupjs'
|
608 |
|
609 | // `subscribe` means that the data is reactive and gets dynamically updated
|
610 | // whenever the data in MongoDB or in private collections gets updated.
|
611 |
|
612 | @subscribe(props => ({
|
613 | // Subscribe to the path in a private collection `_session`.
|
614 | //
|
615 | // `private` collection means that the data does NOT get synced with MongoDB.
|
616 | // Data in these collections live only on the client-side.
|
617 | //
|
618 | // Collection is considered `private` when its name starts from `_` or `$`.
|
619 | //
|
620 | // Private collections like `_session` are used as a singleton client-only storage,
|
621 | // an alternative to `Redux` or `MobX`.
|
622 | //
|
623 | // You can have as many private collections as you like, but the common
|
624 | // guideline is to use just one collection to store all private data -- `_session`
|
625 | userId: subLocal('_session.userId')
|
626 | }))
|
627 |
|
628 | export default class Home extends React.Component {
|
629 | constructor (...props) {
|
630 | super(...props)
|
631 |
|
632 | // For each thing you subscribe to, you receive a scoped `model`
|
633 | // with the same name prefixed with `$` in `props`. Use it
|
634 | // to update the data with the `model` operations available in Racer:
|
635 | // https://derbyjs.com/docs/derby-0.10/models
|
636 | let {$userId} = this.props
|
637 |
|
638 | // Update the private path `_session.userId` with the new value
|
639 | //
|
640 | // We'll use this `_session.userId` in all other children
|
641 | // components to easily get the userId without doing the potentially
|
642 | // heavy/long process of fetching the userId over and over again.
|
643 | $userId.set(this.getUserId())
|
644 | }
|
645 |
|
646 | // Get my userId somehow (for example from the server-side response).
|
647 | // For simplicity, we'll just generate a random guid in this example
|
648 | // by creating an empty user document each time, so whenever
|
649 | // you open a new page, you'll be a new user.
|
650 | getUserId = () => model.add('users', {})
|
651 |
|
652 | render = () => <Room roomId={'myCoolRoom1'} />
|
653 | }
|
654 | ```
|
655 |
|
656 | ```js
|
657 | // Room.jsx
|
658 | import React from 'react'
|
659 | import {model, subscribe, subLocal, subDoc, subQuery} from 'startupjs'
|
660 |
|
661 | @subscribe(props => ({
|
662 |
|
663 | // Subscribe to the same private path again to get the userId, which
|
664 | // we did manually setup in the parent `<Home>` component.
|
665 | // The pattern of subscribing and updating data in a private path
|
666 | // can be used to expose some data from one component to another.
|
667 | userId: subLocal('_session.userId'),
|
668 |
|
669 | // Subscribe to the particular document from a public collection `rooms` by id.
|
670 | // `public` means that it DOES sync with MongoDB.
|
671 | // `public` collection names start with a regular letter a-z.
|
672 | // You can also use ClassCase for collection names (A-Z), but it is
|
673 | // NOT recommended unless you have such guidelines in your project.
|
674 | room: subDoc('rooms', props.roomId)
|
675 |
|
676 | }))
|
677 |
|
678 | // All things you subscribe to end up in `props.store` under the same name.
|
679 | // We can do a second subscribe using the data we received in the first one.
|
680 |
|
681 | @subscribe(props => ({
|
682 |
|
683 | // Subscribe to the particular document from a public collection by id
|
684 | // which we got from the previous subscription
|
685 | myUser: subDoc('users', props.store.userId),
|
686 |
|
687 | // Subscribe to a query to get docs from a public `users` collection
|
688 | // using the array of userIds from `room` received in the previous subscription
|
689 | users: subQuery('users', {_id: {
|
690 | $in: props.store.room && props.store.room.userIds || []
|
691 | }})
|
692 |
|
693 | }))
|
694 |
|
695 | // Additionally, whenever you subscribe to the MongoDB query, you also
|
696 | // receive the `Ids` in store.
|
697 | // For example, subscribing to the `users` collection above populated
|
698 | // `props.store.users` (array of documents) AND
|
699 | // `props.store.userIds` (array of ids) - auto singular name with the `Ids` suffix
|
700 |
|
701 | export default class Room extends React.Component {
|
702 | constructor (...props) {
|
703 | super(...props)
|
704 | let {$room, roomId} = this.props
|
705 | let {userId, room, room: {userIds = []}} = this.props.store
|
706 |
|
707 | // Create an empty room if it wasn't created yet
|
708 | if (!room) model.add('rooms', {id: roomId, title: `Room #${roomId}`})
|
709 |
|
710 | // Add user to the room unless he's already there
|
711 | if (!userIds.includes(userId)) $room.push('userIds', userId)
|
712 | }
|
713 |
|
714 | changeName = (event) => {
|
715 | let {$myUser} = this.props
|
716 | $myUser.setEach({name: event.target.value})
|
717 | }
|
718 |
|
719 | render () {
|
720 | let { myUser, room, users, userId } = this.props.store
|
721 | return (
|
722 | <main>
|
723 | <h1>{room.title}</h1>
|
724 | <section>
|
725 | My User Name:
|
726 | <input type='text' value={myUser.name} onChange={this.changeName} />
|
727 | </section>
|
728 | <h3>Users in the room:</h3>
|
729 | {
|
730 | users
|
731 | .filter(({id}) => id !== userId) // exclude my user
|
732 | .map(user =>
|
733 | <section key={user.id}>
|
734 | {user.name || `User ${user.id}`}
|
735 | </section>
|
736 | )
|
737 | }
|
738 | </main>
|
739 | )
|
740 | }
|
741 | }
|
742 | ```
|
743 |
|
744 | ## Licence
|
745 |
|
746 | MIT
|
747 |
|
748 | (c) Decision Mapper - http://decisionmapper.com
|