/feathers-redux

Integrate Feathers with your Redux store

Primary LanguageJavaScriptMIT LicenseMIT

feathers-redux

Build Status Code Climate Test Coverage Dependency Status Download Status

Integrate Feathers services with your Redux store

Example

On server:

app.use('/users', ...);
app.use('/messages', ...);

On client:

import reduxifyServices from 'feathers-redux';
const feathersClient = feathers(). ...;

// Create Redux actions and reducers for Feathers services
const services = reduxifyServices(feathersClient, ['users', 'messages']);

// Configure Redux store & reducers
export default combineReducers({
  users: services.users.reducer,
  messages: services.messages.reducer,
});

// Feathers service calls may now be dispatched.
store.dispatch(services.messages.get('557XxUL8PalGMgOo'));
store.dispatch(services.messages.find());
store.dispatch(services.messages.create({ text: 'Hello!' }));

Installation

npm install feathers-redux --save

Documentation: reduxifyServices

import reduxifyServices from 'feathers-redux';
const services = reduxifyServices(app, serviceNames, options);

Options:

  • app (required) - The Feathers client app.
  • serviceNames (required, string, array of strings, or object) - The paths of the Feathers services to reduxify.
    • 'messages' is short for { messages: 'messages' }. You can dispatch calls with dispatch(services.messages.create(data, params));.
    • ['users', 'messages'] is short for { users: 'users', messages: 'messages' }.
    • { '/buildings/:buildingid': 'buildings' } will reduxify the Feathers service configured with the path /buildings/:buildingid. You can dispatch calls with dispatch(services.buildings.create(data, params));.
  • options (optional) - Names for props in the Redux store, and for string fragments in the action constants. The default is
{ // Names of props for service's Redux state
  idField: 'id',
  isError: 'isError', // e.g. state.messages.isError
  isLoading: 'isLoading',
  isSaving: 'isSaving',
  isFinished: 'isFinished',
  data: 'data',
  queryResult: 'queryResult',
  store: 'store',
  // Fragments to form action constants
  PENDING: 'PENDING', // e.g. MESSAGES_CREATE_PENDING
  FULFILLED: 'FULFILLED',
  REJECTED: 'REJECTED',
}

reduxifyServices returns an object of the form

{
  messages: { // For the Feathers service with path /messages.
    // action creators
    create(data, params) {}, // Action creator for app.services('messages').create(data, params)
    update(id, data, params) {},
    patch(id, data, params) {},
    remove(id, params) {},
    find(params) {},
    get(id, params) {},
    store(object) {}, // Interface for realtime replication.
    reset() {}, // Reinitializes store for this service.
    // action types
    types: {
      RESET: 'RESET',
      STORE: 'STORE',
      SERVICES_MESSAGES_FIND: 'SERVICES_MESSAGES_FIND',
      SERVICES_MESSAGES_FIND_PENDING: 'SERVICES_MESSAGES_FIND_PENDING',
      SERVICES_MESSAGES_FIND_FULFILLED: 'SERVICES_MESSAGES_FIND_FULFILLED',
      SERVICES_MESSAGES_FIND_REJECTED: 'SERVICES_MESSAGES_FIND_REJECTED',
      // same for all methods GET, CREATE...
    },
    // reducer
    reducer() {}, // Reducers handling actions MESSAGES_CREATE_PENDING, _FULFILLED, and _REJECTED.
  },
  users: { ... },
}

Service calls may be dispatched by

dispatch(services.messages.create(data, params));

Reducers may be combined with

combineReducers({
  users: services.users.reducer,
  messages: services.messages.reducer,
});

ProTip: You have to include redux-promise-middleware and redux-thunk in your middleware.

You may listen to actions dispatched by feathers-redux, for example to manage your side-effects. With redux-saga, it would be done with:

yield take(services.users.types.SERVICES_USERS_CREATE_FULFILLED, function*(action) {
  // do something when user gets created
});

Documentation: getServicesStatus

Its common for React apps to display info messages such as "Messages are being saved." getServicesStatus returns a relevant message for the reduxified Feathers services.

import reduxifyServices, { getServicesStatus } from 'feathers-redux';
const msg = getServicesStatus(state, serviceNames)

Options:

  • state (required) - The state containing state for the services.
  • serviceNames (required, string, array of strings) - The names of the Feathers services.

The services are checked from left to right. They first are checked for an error condition (isError), and if an error is found the function returns an object similar to

{ message: 'users: Error.message',
  className = Error.className, // Can be used to internationalize the message.
  serviceName = 'users';
}

Next they are check for loading or saving, and if one is found the function returns an object similar to

{ message: 'users is loading',
  className = 'isLoading', // Or isSaving.
  serviceName = 'users';
}

Otherwise the object is returned with empty strings.

Realtime replication

The Feathers read-only, realtime replication engine is feathers-offline-realtime. You can connect this engine with

const Realtime = require('feathers-offline-realtime');
const messages = feathersClient.service('/messages');

const messagesRealtime = new Realtime(messages, { subscriber: (records, last) => {
  store.dispatch(services.messages.store({ connected: messagesRealtime.connected, last, records }));
} });

Shape of the store

The above code produces a state shaped like

state = {
  messages: {
    isLoading: boolean, // If get or find have started
    isSaving: boolean, // If update, patch or remove have started
    isFinished: boolean, // If last call finished successfully
    isError: Feathers error, // If last call was unsuccessful
    data: hook.result, // Results from other than a find call
    queryResult: hook.result, // Results from a find call. May be paginated.
    store: {
      connected: boolean, // If replication engine still listening for Feathers service events
      last: { // Read https://github.com/feathersjs/feathers-offline-realtime#event-information.
        action: string, // Replication action.
        eventName: string, // Feathers service event name. e.g. created
        records: object, // Feathers service event record.
      },
      records: [ objects ], // Sorted near realtime contents of remote service
    },
  },
  users: { ... },
};

Autobind Action Creators

Method to bind a given dispatch function with the passed services. This helps with not having to pass down store.dispatch as a prop everywhere the service is being used. Read More: http://redux.js.org/docs/api/bindActionCreators.html

import reduxifyServices, { bindWithDispatch } from 'feathers-redux';

// create a services object as described above 
const rawServices = reduxifyServices(...);

// create a store with rootReducer combining reducers from rawServices
const store = createStore(...)

// use the bindWithDispatch method to bind rawServices' action creators with store.dispatch
const services = bindWithDispatch(store.dispatch, rawServices)
// before 
store.dispatch(services.messages.get('557XxUL8PalGMgOo'));
store.dispatch(services.messages.find());
store.dispatch(services.messages.create({ text: 'Hello!' }));

// after
services.messages.get('557XxUL8PalGMgOo');
services.messages.find();
services.messages.create({ text: 'Hello!' });

Realtime Feathers Updates

If any of your services need real time updates, you can dispatch any of the following actions depending on your use case:

    dispatch(services.messages.onCreated(data));
    dispatch(services.messages.onUpdated(data));
    dispatch(services.messages.onPatched(data));
    dispatch(services.messages.onRemoved(data));

In order for the redux store to update in realtime, these action dispatches should be encapsulated within feathers service.on() event listener:

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

 messages.on('created', (data) => {
      dispatch(services.messages.onCreated(data));
  })
  messages.on('updated', (data) => {
      dispatch(services.messages.onUpdated(data));
  })
  messages.on('patched', (data) => {
      dispatch(services.messages.onPatched(data));
  })
  messages.on('removed', (data) => {
      dispatch(services.messages.onRemoved(data));
  })

Note: idField is used to match events with correct objects.

Action Pending/Loading

The following properties exist in all of the feather services:

  const pendingDefaults = {
    createPending: false,
    findPending: false,
    getPending: false,
    updatePending: false,
    patchPending: false,
    removePending: false
  };

The service pending state will be updated according to the dispatched action.

    dispatch(services.messages.create({ text: 'Hello!' })) `will update the state to:` createPending: true
    dispatch(services.messages.find()) `will update the state to:` findPending: true 
    dispatch(services.messages.get('557XxUL8PalGMgOo')) `will update the state to:` getPending: true
    dispatch(services.messages.update(id, data) `will update the state to:` updatePending: true
    dispatch(services.messages.patch(id, data) `will update the state to:` patchPending: true
    dispatch(services.messages.remove(id, params) `will update the state to:` removePending: true

Examples

example/ contains an example you may run. Its README has instructions.

feathers-redux/test/integration.test.js may answer any questions regarding details.

License

Copyright (c) 2017

Licensed under the MIT license.