/react-ddp

ddp client with subs, methods, minimongo supported without much work (see readme), will not make minimongo a dep.

Primary LanguageJavaScriptMIT LicenseMIT

react-ddp

A lightweight ddp-client for react-native. Expanded on work by mondora.

Purpose

The goal is not to implement a full-blown Meteor client in react-native. This library is meant to include minimally necessary set of functions which are implemented on traditional Meteor clients, but to say absolutely nothing about how your data is handled. You may use redux, minimongo, or a custom object to store data; whatever you want. I tried to couple this library with my own store until I realized two things:

  1. Creating a reactive front-end data store is non-trivial.
  2. React-native users have mixed opinions about data storage, best to not force them.

Thus, this ddp client does not implement a cache of any sort. You will get the traditional added, changed, and removed messages (compliant with the DDP version 1 spec) which you will use to populate a (potentially reactive) data-store yourself.

With react-ddp and minimongo-cache, we can achieve a front-end on react-native which is eerily similar to a traditional Meteor front end, with little work on your end, whilst being modular. Don't like minimongo? Feel free to use Redux or something. The coupling between the data-store and react-ddp is as easy as writing custom added, changed, and remove callbacks. For minimongo, this is trivial because the ddp messages are already in "mongo" form. An example below will show you how easy it is to get started with Pete Hunt's minimongo-cache and react-ddp.

Implementation

Reactive Client Metadata

ReactiveVar (npm install reactive-var) is used sparingly in react-ddp to provide a reactive handle to user state:

Client.loggingIn() //true or false
Client.userId() // "someUserId" or null
Client.status() // "connecting", "connected", "reconnecting", "disconnected"

While Tracker (npm install trackr) is not an explicit dependency of react-ddp, you will need Tracker in your application in order to make use of the reactive nature of ReactiveVar. Read more about how to use Tracker with react-native

Main methods

Instantiation

In the top level of your react-native app, create an instance of the react-ddp client:

//connect.js

import DDP from "react-ddp";
export const client = new DDP({
  SocketConstructor: WebSocket,
  endpoint:"ws://localhost:3000/websocket",
  //endpoint: "wss://mysecurewebsite.example.com/websocket",
  debug:false,
  autoConnect: true,
  autoReconnect: true,
  appId: "myAwesomeAppSDFSLjfwqijdkf",
  reconnectInterval: 10000
})

Read more about debug and appId here See mondora's ddp.js docs for more information on things like endpoint, autoReconnect, etc.

Regarding Encryption, Authentication & Passwords

As of now, when you call a method via client.call(...), any key in the data field of opts which is one of: "password", "oldPassword", "newPassword" will be encrypted before it is sent to the server. This is used in the login method, as well as custom sign up/ password changing methods.

When the react-ddp client is instantiated, the socket opens and sends a connect message to server automatically (thanks to mondora's raw ddp client). react-ddp will attempt to (re)log the user in automatically once a connection has been successfully (re)established. This is achieved by using react-native's AsyncStorage. Read about authentication state flicker and how it's fixed with react-ddp

Methods

import { myValidationFunction } from "./validation.js";
import { client } from "./connect.js";

client.login

...
client.login({
  check: myValidationFunction, //takes args (data, currentUserId)
  data: {
    user: {
      email: "hello@example.com"
    },
    password: "terriblePassword" //not sent in plain text over the wire (encrypted)
  }
}, optionalCallback)

//optionalCallback is provided (error, result) arguments.

client.logout

...
client.logout(optionalCallback)
//optionalCallback is provided (error) arguments.

client.subscribe

The syntax differs slightly from vanilla Meteor. Supports caching.

...
client.subscribe({
  cache: false, //default
  check: myValidationFunction, //takes args (data, currentUserId)
  data: {
    location: [42.2345,-72.68583],
    topic: "Trees"
  },
  name: "treesAroundMe"
}, optionalCallback)

//optionalCallback is provided with (error, sub) arguments.
//sub is of the form {id: "someSubId"}

Subscribes to the treesAroundMe subscription with a single argument (data). If cache is true, then this subscription will not get overridden by a new subscription of the same name. For example (with cache === false) if we perform the same subscription with data.locationequal to[40, -72], then the client will unsub from the previous subscription, and will subscribe to the new one. By default, a call to the exact same subscription (same name, same data`) will not result in a re-run of the subscription.

client.unsubscribe

...
client.unsubscribe({
  id: "theSubIdIfKnown", //if you know this, you don't need to enter name or data.
  name: "nameOfSub", //if you don't have the id, but know the name.
  data: {
    some:"data"
  }
}, optionalCallback)

//optionalCallback is provided with (error, sub) arguments.
//sub is of the form {id: "someSubId"}

If you provide the id, the client will try to unsub from that exact id. If you provide a sub name, you should provide the data passed to the sub as well so that you can differentiate between possibly cached subscriptions.

client.call

The syntax differs slightly from vanilla Meteor.

client.call({
  name:"someMethod",
  data: {
    userInput1: "I love potatoes",
    userInput2: "I love tomatoes"
  },
  check: myValiationFunction //takes args (data, currentUserId)
}, optionalCallback)

//optionalCallback is provided (error, result) arguments.

Using minimongo-cache

Using this library with minimongo-cache is relatively easy because of how DDP messages are structured. On the client:

//database.js
import minimongo from "minimongo-cache";
import { client } from "./connect.js";

process.nextTick = setImmediate; //react-native polyfill
const db = new minimongo();

//initialize collections
const collections = ['posts','users','messages','trees','animals'];
collections.forEach(function(coll){
  db.addCollection(coll)
})

client.on('added', function(data){
  if (!db[data.collection]){
    db.addCollection(data.collection);
  }
  db[data.collection].upsert({_id: data.id, ...data.fields})
})

client.on('changed', function(data){
  db[data.collection].upsert({_id: data.id, ...data.fields});
})

client.on('removed', function(data){
  db[data.collection].remove({_id:data.id});
})

export { db }; //use db.posts.find(...), db.posts.findOne(...), etc. as in minimongo-cache

minimongo-cache makes it easy to get reactivity into react-native:

...
import { db } from "./database.js";
export default class MyComponent extends Component {
  constructor(props){
    super(props);
    this.state = {posts:[]}
  }
  componentWillMount(){
    this.observer = db.observe(()=>{
      return db.posts.find({},{fields:{name:1,body:1}})
    })
    this.observer.subscribe((posts)=>{
      //this function will run whenever the result set "posts" changes
      //for example, due to your incoming DDP messages!
      this.setState({posts:posts})
    })
  }
  componentWillUnmount(){
    this.observer.dispose();
  }
  render(){
    ...
  }
}