Here are some thoughts on a slight tweak to the query API and the mutation API.
Currently the API has a watchQuery
method that is passed to the user to use in order to create queries. The build a dictionary of [key: string]: WatchQueryHandles that gets mapped to the props passed to the wrapped component. The adjustments below have the user creating a dictionary of [key: string]: WatchQueryOptions. The reason for doing this is two fold. 1st the connect component decides how to get data from the store. Behind the scenes it will be calling watchQuery
but in the future if we wanted to add / make changes behind the scenes we could call different methods for different data (not sure if useful). The real benefit in my option is the WatchQueryOptions
. Since the watchQuery
handle isn't called when getting the dictionary anymore, we can get the dictionary in componentWillMount
, use the WatchQueryOptions
to see if we already have all of the data using getFromStore
methods and if we do, the intial state passed to the wrapped component would be the full data + no errors + not loading. Then we could bind the watchQuery on componentDidMount
for reactivity purposes. If we don't have the data, we can easily pass default data { loading: false, error: null, result: null }
on componentWillMount
and create the query on componentDidMount
. Doing this also allows us to extend and when rendering on the server, we can call client.query(WatchQueryOptions)
and use the resulting promise to delay SSR until the data has returned and prefill the store.
Similar to the changes in query, mutations return a dictionary of [key: string]: MutationOptions which is used to bind the resulting props. The prop of each key represents a custom function (with as many args as wanted) which calls client.mutate(options) behind the scenes. The key on the props also represents the state of props in the same shape as the query props ({ loading: false, error: null, result: null }
). We could also add a hasBeenCalled: boolean
or some other key if wanted but I think between loading, error, and result, a dev should be able to reason that if needed. This gets us the same benefits on the SSR side so something like an analytics mutation component could report SSR page loads or other data if desired. I think the mixed method + object is a little odd but it seems to be the cleanest solution overall
Simillar to redux's @connect
, I think that our's should pass dispatch
, query
, and mutate
as props to the wrapped component. That way if custom client actions (one off mutations or queries) are wanted, they are possible. It also means @connect()
is a non reactive way to get access to the applications client API.
import { connect } from 'apollo-react';
function mapQueriesToProps({ ownProps, state }) {
return {
category: {
query: `
query getCategory($categoryId: Int!) {
category(id: $categoryId) {
name
color
}
}
`,
variables: {
categoryId: 5,
},
forceFetch: false,
returnPartialData: true,
}
}
}
function mapMutationsToProps({ ownProps, state }) {
return {
addCategory(/* args */) {
return {
mutation: `
mutation postReply(
$topic_id: ID!
$category_id: ID!
$raw: String!
) {
createPost(
topic_id: $topic_id
category: $category_id
raw: $raw
) {
id
cooked
}
}
`,
variables: {
// Use the container component's props
topic_id: ownProps.topic_id,
// Use the redux state
category_id: state.selectedCategory,
// Use an argument passed from the callback
/* args */,
}
};
},
otherAction(controlArg1, controlArg2) {
let mutation = `
mutation postReply(
$topic_id: ID!
$category_id: ID!
$raw: String!
) {
createPost(
topic_id: $topic_id
category: $category_id
raw: $raw
) {
id
cooked
}
}
`
if (controlArg1) {
mutation = `
different mutation
`
}
return {
mutation,
variables: {
// Use the container component's props
topic_id: ownProps.topic_id,
// Use the redux state
category_id: state.selectedCategory,
// Use an argument passed from the callback
/* args */,
}
};
},
}
}
@connect({ mapQueriesToProps, mapMutationsToProps })
class AddCategory extends Component {
onAddCategoryClick(e) {
const { target } = e;
const { addCategory } = this.props;
this.props.addCategory(target.id, target.value);
}
onOtherAction(e) {
const { target } = e;
const { otherAction } = this.props;
this.props.otherAction(target.id, target.value);
}
onCustomComponentMutation(mutation, variables) {
this.props.mutate({ mutation, variables })
.then((graphQLResult) => {
const { errors, data } = graphQLResult;
if (data) {
console.log('got data', data);
}
if (errors) {
console.log('got some GraphQL execution errors', errors);
}
}).catch((error) => {
console.log('there was an error sending the query', error);
});
}
onCustomQuery(query, variables) {
this.props.query({ query, variables })
.then((graphQLResult) => {
const { errors, data } = graphQLResult;
if (data) {
console.log('got data', data);
}
if (errors) {
console.log('got some GraphQL execution errors', errors);
}
}).catch((error) => {
console.log('there was an error sending the query', error);
});
}
render() {
/*
this.props.addCategory is a function that calls a mutation and has the added data of:
{
loading: boolean,
error: Error,
result: GraphQLResult,
}
this.props.categories is a just an object representing the query:
{
loading: boolean,
error: Error,
result: GraphQLResult,
}
*/
}
}
Thoughs?