npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-sharedb

v9.0.0-alpha.31

Published

> Run `ShareDB` in `React`

Downloads

65

Readme

react-sharedb

Run ShareDB in React

What it does

  1. Brings real-time collaboration to React using ShareDB;
  2. Uses Racer to add a model to your app to do any data manipulations;
  3. 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;
  4. Makes the render reactive similar to how it's done in MobX -- rerendering happens whenever any model data you used in render changes.

[Hooks] use*()

observer(FunctionalComponent) HOF

Higher Order Function which makes your functional component rendering reactive. You have to wrap your functional components in it to be able to use react-sharedb hooks.

import {observer, useDoc} from 'react-sharedb'
export default observer(function User ({userId}) {
  let [user, $user] = useDoc('users', userId)  
  if (!user) return null // <Loading />
  return (
    <input value={user.name} onChange={e => $user.set('name', e.target.value)} />
  )
})

useDoc(collection, docId)

Refer to the documentation of subDoc() below

useLocalDoc(collection, docId)

A convenience method to get the document you are already subscribed to using the same API as useDoc()

let [game, $game] = useLocalDoc('games', gameId)
// It's the same as doing:
let [game, $game] = useLocal('games.' + gameId)

useQuery(collection, query)

Refer to the documentation of subQuery() below

useQueryIds(collection, ids, options)

Subscribe to documents in collection by their ids

collection [String] -- collection name. Required ids [Array] -- array of strings which should be document ids. options [Object] -- js { reverse: false // reverse the order of resulting array }

Example:

observer(function Players ({ gameId }) {
  let [game] = useDoc('games', gameId)
  if (!game) return null // <Loading />
  let [players, $players] = useQueryIds('players', game.playerIds)
  if (!players) return null // <Loading />

  return (
    <div>{players.map(i => i.name).join(' ,')}</div>
  )
})

useQueryDoc(collection, query)

Subscribe to a document using a query. It's the same as useDoc(), but with query parameter instead of the particular docId. $limit: 1 and $sort: { createdAt: -1 } are added to the query automatically (if they don't already exist).

collection [String] -- collection name. Required query [Object] -- query object, same as in useQuery().

Example:

observer(function NewPlayer ({ gameId }) {
  // { $sort: { createdAt: -1 }, $limit: 1 }   
  // is added automatically to the query, so the newest player will be returned.
  // It's also reactive, so whenever a new player joins, you'll receive the new data and model.
  let [newPlayer, $newPlayer] = useQueryDoc('players', { gameId })
  if (!newPlayer) return null // <Loading />

  return (
    <div>New player joined: {newPlayer.name}</div>
  )
})

useLocal(path)

Refer to the documentation of subLocal() below

useSession(path)

A convenience method to access the _session local collection.

let [userId, $userId] = useSession('userId')
// It's the same as doing:
let [userId, $userId] = useLocal('_session.userId')

usePage(path)

A convenience method to access the _page local collection.

let [game, $game] = usePage('game')
// It's the same as doing:
let [game, $game] = useLocal('_page.game')

useValue(value)

Refer to the documentation of subValue() below

useModel(path)

Return a model scoped to path (memoized by the path argument). If path is not provided, returns the model scoped to the root path.

import React from 'react'
import {render} from 'react-dom'
import {observer, useModel, useLocal} from 'react-sharedb'

const Main = observer(() => {
  return (
    <div style={{display: 'flex'}}>
      <Content />
      <Sidebar />
    </div>
  )
})

const Content = observer(() => {
  let $showSidebar = useModel('_page.Sidebar.show')

  // sidebar will be opened without triggering rerendering of the <Content /> component (this component)
  return (
    <div>
      <p>I am Content</p>
      <button onClick={() => $showSidebar.setDiff(true)}>Open Sidebar</button>
    </div>
  )
})

const Sidebar = observer(() => {
  let [show, $show] = useLocal('_page.Sidebar.show')
  if (!show) return null
  return (
    <div>
      <p>I am Sidebar</p>
      <button onClick={() => $show.del()}>Close</button>
    </div>
  )
})

render(<Main />, document.body.appendChild(document.createElement('div')))

[Hooks] Example

import React from 'react'
import {observer, useDoc, useQuery, useLocal, useValue} from 'react-sharedb'

export default observer(function Game ({gameId}) {
  let [secret, $secret] = useValue('Game Secret Password')
  let [userId, $userId] = useLocal('_session.userId')
  let [user, $user] = useDoc('users', userId)
  let [game, $game] = useDoc('games', gameId)
  if (!(game && user)) return null // <Loading />
  
  let [players, $players] = useQuery('players', {_id: {$in: game.playerIds}})
  if (!players) return null // <Loading />
  
  let [users, $users] = useQuery('users', {_id: {$in: players.map(i => i.userId)}})
  if (!users) return null // <Loading />
  
  function updateSecret (event) {
    $secret.set(event.target.value)
  }
  
  function updateName (event) {
    $user.set('name', event.target.value)
  }
  
  return (
    <div>
      <label>
        Secret:
        <input type='text' value={code} onChange={updateSecret} />
      </label>
    
      <label>
        My User Name:
        <input type='text' value={user.name} onChange={updateName} />
      </label>
      
      <h1>Game {game.title}</h1>
      
      <h2>Users in game:</h2>
      <p>{users.map(i => i.name).join(', ')}</p>
    </div>
  )
})

TODO:

  1. useSubscribe(fns)
  2. <Suspense /> support

[Classes] HOC @subscribe(cb)

@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 'react-sharedb'

@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 'react-sharedb'

@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')
    // You can also use $store to do the same:
    $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:

  1. Home -- sets up my userId and renders Room
  2. Room -- shows my user name ands lets user update it, shows all users which are currently in the room.
// Home.jsx
import React from 'react'
import Room from './Room.jsx'
import {model, subscribe, subLocal, subDoc, subQuery} from 'react-sharedb'

// `subscribe` means that the data is reactive and gets dynamically updated
// whenever the data in MongoDB or in private collections gets updated.

@subscribe(props => ({
  // Subscribe to the path in a private collection `_session`.
  //
  // `private` collection means that the data does NOT get synced with MongoDB.
  // Data in these collections live only on the client-side.
  //  
  // Collection is considered `private` when its name starts from `_` or `$`.
  //
  // Private collections like `_session` are used as a singleton client-only storage,
  // an alternative to `Redux` or `MobX`.
  // 
  // You can have as many private collections as you like, but the common
  // guideline is to use just one collection to store all private data -- `_session`
  userId: subLocal('_session.userId')
}))

export default class Home extends React.Component {
  constructor (...props) {
    super(...props)
    
    // For each thing you subscribe to, you receive a scoped `model`
    // with the same name prefixed with `$` in `props`. Use it
    // to update the data with the `model` operations available in Racer:
    // https://derbyjs.com/docs/derby-0.10/models     
    let {$userId} = this.props

    // Update the private path `_session.userId` with the new value
    //
    // We'll use this `_session.userId` in all other children
    // components to easily get the userId without doing the potentially
    // heavy/long process of fetching the userId over and over again.
    $userId.set(this.getUserId())    
  }  

  // Get my userId somehow (for example from the server-side response).
  // For simplicity, we'll just generate a random guid in this example
  // by creating an empty user document each time, so whenever 
  // you open a new page, you'll be a new user.        
  getUserId = () => model.add('users', {})

  render = () => <Room roomId={'myCoolRoom1'} />
}
// Room.jsx
import React from 'react'
import {model, subscribe, subLocal, subDoc, subQuery} from 'react-sharedb'

@subscribe(props => ({

  // Subscribe to the same private path again to get the userId, which
  // we did manually setup in the parent `<Home>` component.
  // The pattern of subscribing and updating data in a private path
  // can be used to expose some data from one component to another.   
  userId: subLocal('_session.userId'),
  
  // Subscribe to the particular document from a public collection `rooms` by id.
  // `public` means that it DOES sync with MongoDB.
  // `public` collection names start with a regular letter a-z.
  // You can also use ClassCase for collection names (A-Z), but it is
  // NOT recommended unless you have such guidelines in your project.  
  room: subDoc('rooms', props.roomId)
  
}))

// All things you subscribe to end up in `props.store` under the same name.
// We can do a second subscribe using the data we received in the first one.

@subscribe(props => ({

  // Subscribe to the particular document from a public collection by id
  // which we got from the previous subscription
  myUser: subDoc('users', props.store.userId),
  
  // Subscribe to a query to get docs from a public `users` collection
  // using the array of userIds from `room` received in the previous subscription 
  users: subQuery('users', {_id: {
    $in: props.store.room && props.store.room.userIds || []
  }})
    
}))

// Additionally, whenever you subscribe to the MongoDB query, you also
// receive the `Ids` in store.
// For example, subscribing to the `users` collection above populated
// `props.store.users` (array of documents) AND
// `props.store.userIds` (array of ids) - auto singular name with the `Ids` suffix
 
export default class Room extends React.Component {
  constructor (...props) {
    super(...props)    
    let {$room, roomId} = this.props
    let {userId, room, room: {userIds = []}} = this.props.store
    
    // Create an empty room if it wasn't created yet
    if (!room) model.add('rooms', {id: roomId, title: `Room #${roomId}`})
    
    // Add user to the room unless he's already there
    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>
    )
  }
}

Licence

MIT

(c) Decision Mapper - http://decisionmapper.com