Skip to content

Latest commit

 

History

History

redux-source

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

redux-source

< Back to Project WebCube

NPM Version

Nodei

Using GraphQL schema and query language to access any data source (eg. RESTful APIs) and automatically generate reducers, actions and normalized state

npm install --save redux-source

For Immutable.js store: redux-source-immutable

Examples

Get Started

For example, suppose we have a CRUD (create, read, update, delete) API for managing shops

Create A Data Source

createSource

Create a data source instance

// shopManageApp/apis/shops/index.js
import { createSource } from 'redux-source';
// for Immutable.js store:
// import { createSource } from 'redux-source-immutable';
import schema from './schema/index.gql';
import resolvers from './resolvers';

export const source = createSource({
  schema,
  resolvers,
});

TIPS

Schema

# shopManageApp/apis/shops/schema/index.gql
type Shop {
  id: ID!
  name: String
  address: String
  deliveryEnabled: Boolean!
  services: [Service!]
  orders: [JSON!]
  position: Position
}
type Service {
  id: ID!
  name: String!
  price: Float!
}
type Query {
  shops: [Shop!]
}
type Mutation {
  updateShop(shopId: String!, shopData: JSON!): [Shop!]
  deleteShop(shopId: String!): [Shop!]
}

TIPS

Resolvers

// shopManageApp/apis/shops/resolvers/index.js
import hifetch from 'hifetch';

const resolvers = {
  Query: {
    shops: () => hifetch({
      url: `https://example.com/api/shops`,
    }).send().then(response => response.shops),
  },
  Mutation: {
    updateShop: (_, { shopId, shopData }) =>
      hifetch({
        url: `https://example.com/api/shops/${shopId}`,
        method: 'post',
        data: shopData,
      }).send().then(response => [response.shop]),
    deleteShop: (_, { shopId }) =>
      hifetch({
        url: `https://example.com/api/shops/${shopId}`,
        method: 'delete',
      }).send().then(response => [response.shop]),
  },
  Shop {
    services: shop => hifetch({
      url: `https://example.com/api/shops/${shop.id}/services`,
    }).send().then(response => [response.services]),
  },
};

export default resolvers;

TIPS

Create A Data Source Query

Queries

Use GraphQL query language to automatically generate reducers, actions and normalized state:

// shopManageApp/ducks/shops.js
import gql from 'graphql-tag';
import { source } from '../apis/shops';

export const shopsSource = source(
  gql`
    query fetchShops {
      __config__ {
        combineResult: replace
      }
      shops {
        ...shopFields
      }
    }

    mutation addShop($id: ID!, $data: JSON!) {
      shops: updateShop(shopId: $id, shopData: $data) {
        ...shopFields
      }
    }

    mutation updateShop($id: ID!, $data: JSON!) {
      shops: updateShop(shopId: $id, shopData: $data) {
        ...shopFields
      }
    }

    mutation deleteShop($id: ID!) {
      __config__ {
        combineResult: crop
      }
      shops: deleteShop(shopId: $id) {
        ...shopFields
      }
    }

    fragment shopFields on Shop {
      id
      name
      address
      deliveryEnabled
      services {
        id
        name
        price
      }
      orders
      position {
        latitude
        longitude
      }
    }
  `,
);

// use `shopsSource` to generate reducer function and action creators

export {
  reducer,
  actions,
  types,
}

TIPS

  • GraphQL query language
  • __config__ is an extended syntax supported by redux-source
    • combineResult is used to indicate how the autogenerated reducer to change the state, its value can be merge (default), replace, or crop
    • Examples: CRUD API demo in react-source-sample

Reducers, Actions and States

The output of source query:

console.log(shopsSource.actions)
// {
//   'REDUX_SOURCE/FETCH_SHOPS': asyncActionCreator,
//   'REDUX_SOURCE/FETCH_SHOPS_PENDING': actionCreator,
//   'REDUX_SOURCE/FETCH_SHOPS_SUCCESS': actionCreator,
//   'REDUX_SOURCE/FETCH_SHOPS_ERROR': actionCreator,
//   'REDUX_SOURCE/ADD_SHOP': asyncActionCreator,
//   'REDUX_SOURCE/ADD_SHOP_PENDING': actionCreator,
//   'REDUX_SOURCE/ADD_SHOP_SUCCESS': actionCreator,
//   'REDUX_SOURCE/ADD_SHOP_ERROR': actionCreator,
//   'REDUX_SOURCE/UPDATE_SHOP': asyncActionCreator,
//   'REDUX_SOURCE/UPDATE_SHOP_PENDING': actionCreator,
//   'REDUX_SOURCE/UPDATE_SHOP_ERROR': actionCreator,
//   'REDUX_SOURCE/UPDATE_SHOP_SUCCESS': actionCreator,
//   'REDUX_SOURCE/DELETE_SHOP': asyncActionCreator,
//   'REDUX_SOURCE/DELETE_SHOP_PENDING': actionCreator,
//   'REDUX_SOURCE/DELETE_SHOP_ERROR': actionCreator,
//   'REDUX_SOURCE/DELETE_SHOP_SUCCESS': actionCreator,
// }

console.log(shopsSource.reducerMap)
// {
//   'REDUX_SOURCE/FETCH_SHOPS_PENDING': reducer,
//   'REDUX_SOURCE/FETCH_SHOPS_SUCCESS': reducer,
//   'REDUX_SOURCE/FETCH_SHOPS_ERROR': reducer,
//   'REDUX_SOURCE/ADD_SHOP_PENDING': reducer,
//   'REDUX_SOURCE/ADD_SHOP_SUCCESS': reducer,
//   'REDUX_SOURCE/ADD_SHOP_ERROR': reducer,
//   'REDUX_SOURCE/UPDATE_SHOP_PENDING': reducer,
//   'REDUX_SOURCE/UPDATE_SHOP_ERROR': reducer,
//   'REDUX_SOURCE/UPDATE_SHOP_SUCCESS': reducer,
//   'REDUX_SOURCE/DELETE_SHOP_PENDING': reducer,
//   'REDUX_SOURCE/DELETE_SHOP_ERROR': reducer,
//   'REDUX_SOURCE/DELETE_SHOP_SUCCESS': reducer,
// }

console.log(shopsSource.initialState)
// {
//   source: {
//     result: {},
//     entities: {},
//     isPending: false,
//     errors: [],
//   }
// }

TIPS

How will the action creators (shopsSource.actions) and reducers (shopsSource.reducerMap) change the state slice (shopsSource.initialState):

shopsSource.actions.reduxSource.fetchShops()
// state slice:
// {
//   source: {
//     result: {
//       shops: ['shop-id-0001', 'shop-id-0002'],
//     },
//     entities: {
//       shop: {
//         'shop-id-0001': {
//           id: 'shop-id-0001',
//           name: 'Shop A',
//           services: ['service-id-0001', 'serivce-id-0001'],
//           position: {
//             latitude: '...',
//             longitude: '...',
//           },
//           ...
//         },
//         'shop-id-0002': {
//           id: 'shop-id-0002',
//           name: 'Shop B',
//           services: ['shop-id-0002', 'shop-id-0003'],
//           position: {
//             latitude: '...',
//             longitude: '...',
//           },
//           ...
//         },
//       },
//       service: {
//         'service-id-0001': {
//           name: 'Service A',
//           ...
//         },
//         'service-id-0002': {
//           ...
//         },
//         'service-id-0003': {
//           ...
//         },
//       },
//     },
//     isPending: false,
//     errors: [],
//   }
// }

shopsSource.actions.reduxSource.addShop({
  id: 'shop-id-0003',
  data: {
    name: 'Shop C',
    services: [{
      id: 'shop-id-0004',
      name: 'Service D',
      // ...
    }],
    // ...
  },
})
// state slice:
// {
//   source: {
//     result: {
//       shops: ['shop-id-0001', 'shop-id-0002', 'shop-id-0003'],
//     },
//     entities: {
//       shop: {
//         'shop-id-0001': {
//           ...
//         },
//         'shop-id-0002': {
//           ...
//         },
//         'shop-id-0003': {
//           id: 'shop-id-0003',
//           name: 'Shop C',
//           ...
//         },
//       },
//       service: {
//         'service-id-0001': {
//           ...
//         },
//         'service-id-0002': {
//           ...
//         },
//         'service-id-0003': {
//           ...
//         },
//         'service-id-0004': {
//           name: 'Service D',
//           ...
//         },
//       },
//     },
//     isPending: false,
//     errors: [],
//   }
// }

shopsSource.actions.reduxSource.updateShop({
  id: 'shop-id-0001',
  data: {
    name: 'Shop A (modified)',
    // ...
  },
})
// state slice:
// {
//   source: {
//     result: {
//       shops: ['shop-id-0001', 'shop-id-0002', 'shop-id-0003'],
//     },
//     entities: {
//       shop: {
//         'shop-id-0001': {
//           id: 'shop-id-0001',
//           name: 'Shop A (modified)',
//           ...
//         },
//         'shop-id-0002': {
//           ...
//         },
//         'shop-id-0003': {
//           ...
//         },
//       },
//       service: {
//         ...
//       },
//     },
//     isPending: false,
//     errors: [],
//   }
// }

shopsSource.actions.reduxSource.deleteShop({
  id: 'shop-id-0002',
})
// state slice:
// {
//   source: {
//     result: {
//       shops: ['shop-id-0001', 'shop-id-0003'],
//     },
//     entities: {
//       shop: {
//         'shop-id-0001': {
//           ...
//         },
//         'shop-id-0002': {
//           ...
//         },
//         'shop-id-0003': {
//           ...
//         },
//       },
//       service: {
//         'service-id-0001': {
//           ...
//         },
//         'service-id-0002': {
//           ...
//         },
//         'service-id-0003': {
//           ...
//         },
//         'service-id-0004': {
//           ...
//         },
//       },
//     },
//     isPending: false,
//     errors: [],
//   }
// }

TIPS

  • redux-source can automatically generate the schema definition for normalizr and automatically normalize the output of GraphQL resolvers (equal to shopsSource.normalize(outputOfAllResolvers))
  • The position field in the result is not normalized because it does not contain an id field
    • The name of the id field can be customized by idAttribute option (see below)

How To Customize

const shopsSource = source(
  gql`
    ...
  `,
  {
    // for normalize
    // default value is 'id'
    idAttribute: 'otherId',
    // for state slice
    stateName: 'shopsSource',
    // for action types
    namespace: 'SHOPS_NAMESPACE',
    // for action types
    delimiter: '|',
  }
})

console.log(shopsSource.actions)
// {
//   'SHOPS_NAMESPACE|FETCH_SHOPS': asyncActionCreator,
//   'SHOPS_NAMESPACE|FETCH_SHOPS_PENDING': actionCreator,
//   'SHOPS_NAMESPACE|FETCH_SHOPS_SUCCESS': actionCreator,
//   'SHOPS_NAMESPACE|FETCH_SHOPS_ERROR': actionCreator,
//   ...
//   'SHOPS_NAMESPACE|DELETE_SHOP_SUCCESS': actionCreator,
// }

console.log(shopsSource.initialState)
// {
//   shopsSource: {
//     result: {},
//     entities: {},
//     isPending: false,
//     errors: [],
//   }
// }

Higher-order Components

Connect To React Components

New docs coming soon! (based on the new createCube API and webcube's SSR feature)

See redux-source-connect

Notification

See redux-source-with-notify

Block UI

See redux-source-with-block-ui

Immutable.js Store

Examples for Immutable.js store: app/react-redux-restapi-app/immutableJsStore