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
For example, suppose we have a CRUD (create, read, update, delete) API for managing shops
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
- Add raw-loader to your webpack.config.js for importing .gql file
{ test: /\.(txt|gql)$/, use: 'raw-loader', },
# 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
- Syntax highlighting for GraphQL schema language and query language
- GraphQL schema language
JSON
type is built-in and defined by graphql-type-json ("field type for object with dynamic keys")
- Other built-in scalar types:
Date
,Time
,DateTime
(graphql-iso-date)- You can define your own custom scalar
- If you want to automatically normalize (see below) the data of a field, its field name must be plural (for the above example, the field names for
Service
type andShop
type areservices
andshops
) or 'xxxx_$list' when its type is a List and must be singular or uncountable when its type is not a List.
// 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
createSource
'sschema
andresolvers
options are the same as graphql-tools'smakeExecutableSchema
- For more complex query or mutation operations, you can use composition libraries such as graphql-resolvers.
- Examples of how to modularize the above code: react-redux-restapi-app/common/apis
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 bemerge
(default),replace
, orcrop
- Examples: CRUD API demo in react-source-sample
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 to compile GraphQL queries at the build time
- How to use the output of source query (like
shopsSource
) with redux-cube
- New docs coming soon! (based on the new
createCube
API and webcube's SSR feature)- Example: redux-source-sample
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 anid
field
- The name of the
id
field can be customized byidAttribute
option (see below)
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: [],
// }
// }
New docs coming soon! (based on the new
createCube
API and webcube's SSR feature)
See redux-source-with-block-ui
Examples for Immutable.js store: app/react-redux-restapi-app/immutableJsStore