feathersjs-ecosystem / feathers-reactive

Reactive API extensions for Feathers services
MIT License
216 stars 37 forks source link

feathers-reactive

CI Download Status

Reactive API extensions for Feathers

About

feathers-reactive adds a watch() method to services. The returned object implements all service methods as RxJS v7 observables that automatically update on real-time events.

Options

The following options are supported:

Application level

import feathers from '@feathersjs/feathers';
import { rx } from 'feathers-reactive';

const app = feathers().configure(rx(options));

Service level

With feathers-reactive configured on the application individual options can be set at the service level with service.rx:

// Set a different id field
app.service('todos').rx({
  idField: '_id'
});

Method call level

Each method call can also pass its own options via params.rx:

// Never update data for this method call
app.service('todos').watch({ listStrategy: 'never' }).find();

List strategies

List strategies are used to determine how a data stream behaves. Currently there are three strategies:

Usage

import {feathers} from '@feathersjs/feathers';
import memory from 'feathers-memory';
import { rx } from 'feathers-reactive';

const app = feathers()
  .configure(rx({
    idField: 'id'
  }))
  .use('/messages', memory());

const messages = app.service('messages');

messages.create({
  text: 'A test message'
}).then(() => {
  // Get a specific message with id 0. Emit the message data once it resolves
  // and every time it changes e.g. through an updated or patched event
  messages.watch().get(0).subscribe(message => console.log('My message', message));

  // Find all messages and emit a new list every time anything changes
  messages.watch().find().subscribe(messages => console.log('Message list', messages));

  setTimeout(() => {
    messages.create({ text: 'Another message' }).then(() =>
      setTimeout(() => messages.patch(0, { text: 'Updated message' }), 1000)
    );
  }, 1000);
});

Will output:

My message { text: 'A test message', id: 0 }
Message list [ { text: 'A test message', id: 0 } ]
Message list [ { text: 'A test message', id: 0 },
  { text: 'Another message', id: 1 } ]
My message { text: 'Updated message', id: 0 }
Message list [ { text: 'Updated message', id: 0 },
  { text: 'Another message', id: 1 } ]

Frameworks

Let's assume a simple Feathers Socket.io server in app.js like this:

npm install @feathersjs/feathers @feathersjs/socketio feathers-memory

import {feathers} from '@feathersjs/feathers';
import socketio from '@feathersjs/socketio';
import memory from 'feathers-memory';

const app = feathers()
  .configure(socketio())
  .use('/todos', memory());

app.on('connection', connection => app.channel('everybody').join(connection));
app.publish(() => app.channel('everybody'));

app.listen(3030).on('listening', () =>
  console.log('Feathers Socket.io server running on localhost:3030')
);

Usage

For an ES5 compatible version on the client (e.g. when using create-react-app) you can import feathers-reactive/dist/feathers-reactive. In client.js:

import io from 'socket.io-client';
import feathers from '@feathersjs/client';
import rx from 'feathers-reactive/dist/feathers-reactive';

const socket = io('http://localhost:3030');
const app = feathers()
  .configure(feathers.socketio(socket))
  .configure(rx({
    idField: 'id'
  }));

export default app;

React

A real-time ReactJS Todo application (with Bootstrap styles) can look like this (see the examples/react-todos folder for a working example);

import React, { Component } from 'react';
import client from './client';

class App extends Component {
  constructor (props) {
    super(props);
    this.state = {
      todos: [],
      text: ''
    };
  }

  componentDidMount () {
    this.todos = client.service('todos').watch()
      .find().subscribe(todos => this.setState(todos));
  }

  componentWillUnmount () {
    this.todos.unsubscribe();
  }

  updateText (ev) {
    this.setState({ text: ev.target.value });
  }

  createTodo (ev) {
    client.service('todos').create({
      text: this.state.text,
      complete: false
    });
    this.setState({ text: '' });
    ev.preventDefault();
  }

  updateTodo (todo, ev) {
    todo.complete = ev.target.checked;
    client.service('todos').patch(todo.id, todo);
  }

  deleteTodo (todo) {
    client.service('todos').remove(todo.id);
  }

  render () {
    const renderTodo = todo =>
      <li key={todo.id} className={`page-header checkbox ${todo.complete ? 'done' : ''}`}>
        <label>
          <input type='checkbox' onChange={this.updateTodo.bind(this, todo)}
            checked={todo.complete} />
          {todo.text}
        </label>
        <a href='javascript://' className='pull-right delete'
          onClick={this.deleteTodo.bind(this, todo)}>
          <span className='glyphicon glyphicon-remove' />
        </a>
      </li>;

    return <div className='container' id='todos'>
      <h1>Feathers real-time Todos</h1>

      <ul className='todos list-unstyled'>{this.state.todos.map(renderTodo)}</ul>
      <form role='form' className='create-todo' onSubmit={this.createTodo.bind(this)}>
        <div className='form-group'>
          <input type='text' className='form-control' name='description'
            placeholder='Add a new Todo' onChange={this.updateText.bind(this)}
            value={this.state.text} />
        </div>
        <button type='submit' className='btn btn-info col-md-12'>
          Add Todo
        </button>
      </form>
    </div>;
  }
}

export default App;

License

Copyright (c) 2023

Licensed under the MIT license.