Skip to content
This repository was archived by the owner on Apr 13, 2023. It is now read-only.

Mutation component #1520

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bc034dc
Initial implementation for Mutation component
excitement-engineer Jan 5, 2018
2907909
Updated the error message
excitement-engineer Jan 5, 2018
1122462
Improved the component. Added some initial tests
excitement-engineer Jan 6, 2018
618191f
Added more props to the mutation component.
excitement-engineer Jan 6, 2018
23c0a11
Improved typescript typings
excitement-engineer Jan 9, 2018
42e813b
fixed listing issues.
excitement-engineer Jan 9, 2018
29ead4b
Added onComplete and onError props
excitement-engineer Jan 27, 2018
889bfc3
Implemented logic for handling changes in props.
excitement-engineer Jan 27, 2018
c639e64
Merge branch 'apollographql/master' into mutation-component
excitement-engineer Jan 27, 2018
a49e268
Changelog
excitement-engineer Jan 27, 2018
333b813
prop-types validation
excitement-engineer Jan 27, 2018
a344bcf
The render prop only shows the result of the most recent mutation in …
excitement-engineer Jan 31, 2018
4968fca
Only verify document if the mutation has updated
excitement-engineer Jan 31, 2018
351121f
Moved the onCompletedMutation outside the try-catch
excitement-engineer Feb 4, 2018
0b2daca
Improved the type for the update prop.
excitement-engineer Feb 4, 2018
97c704a
Allow for passing variables as options in the mutation function inste…
excitement-engineer Feb 14, 2018
8762931
Merge branch 'master' into mutation-component
Feb 16, 2018
dab8b30
Merge branch 'master' into mutation-component
Feb 16, 2018
70b7fe2
Merge branch 'mutation-component' into mutation-component
Feb 16, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Change log

### vNext
* Added `<Mutation />` component [#1520](https://github.com/apollographql/react-apollo/pull/1520)
* HoC `props` result-mapping function now receives prior return value as second argument.
* Fix errorPolicy when 'all' not passing data and errors

Expand Down
266 changes: 266 additions & 0 deletions src/Mutation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import ApolloClient, {
PureQueryOptions,
ApolloError
} from 'apollo-client';
import { DataProxy } from "apollo-cache";
const invariant = require('invariant');
import { DocumentNode, GraphQLError } from 'graphql';
const shallowEqual = require('fbjs/lib/shallowEqual');

import { OperationVariables } from './types';
import { parser, DocumentType } from './parser';

export interface MutationResult<TData = any> {
data: TData;
error?: ApolloError;
loading: boolean;
}

export interface ExecutionResult<T = {[key: string]: any }> {
data?: T;
extensions?: { [key: string]: any };
errors?: GraphQLError[];
}

// Improved MutationUpdaterFn type, need to port them back to Apollo Client
export declare type MutationUpdaterFn<T = {
[key: string]: any;
}> = (proxy: DataProxy, mutationResult: FetchResult<T>) => void;

export declare type FetchResult<C = Record<string, any>, E = Record<string, any>> = ExecutionResult<C> & {
extensions?: E;
context?: C;
};

export declare type MutationOptions<TVariables = OperationVariables> = {
variables?: TVariables
};

export interface MutationProps<TData = any, TVariables = OperationVariables> {
mutation: DocumentNode;
optimisticResponse?: Object;
refetchQueries?: string[] | PureQueryOptions[];
update?: MutationUpdaterFn<TData>;
children: (
mutateFn: (options?: MutationOptions<TVariables>) => void,
result?: MutationResult<TData>,
) => React.ReactNode;
onCompleted?: (data: TData) => void;
onError?: (error: ApolloError) => void;
}

export interface MutationState<TData = any> {
notCalled: boolean;
error?: ApolloError;
data?: TData;
loading?: boolean;
}

const initialState = {
notCalled: true,
};

class Mutation<
TData = any,
TVariables = OperationVariables
> extends React.Component<
MutationProps<TData, TVariables>,
MutationState<TData>
> {
static contextTypes = {
client: PropTypes.object.isRequired,
};

static propTypes = {
mutation: PropTypes.object.isRequired,
variables: PropTypes.object,
optimisticResponse: PropTypes.object,
refetchQueries: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.arrayOf(PropTypes.object)
]),
update: PropTypes.func,
children: PropTypes.func.isRequired,
onCompleted: PropTypes.func,
onError: PropTypes.func
};

private client: ApolloClient<any>;
private mostRecentMutationId: number;

constructor(props: MutationProps<TData, TVariables>, context: any) {
super(props, context);

this.verifyContext(context);
this.client = context.client;

this.verifyDocumentIsMutation(props.mutation);

this.mostRecentMutationId = 0;
this.state = initialState;
}

componentWillReceiveProps(nextProps, nextContext) {
if (
shallowEqual(this.props, nextProps) &&
this.client === nextContext.client
) {
return;
}

if (this.props.mutation !== nextProps.mutation) {
this.verifyDocumentIsMutation(nextProps.mutation);
}

if (this.client !== nextContext.client) {
this.client = nextContext.client;
this.setState(initialState);
}
}

render() {
const { children } = this.props;
const { loading, data, error, notCalled } = this.state;

const result = notCalled
? undefined
: {
loading,
data,
error,
};

return children(this.runMutation, result);
}

private runMutation = async (options: MutationOptions<TVariables> = {}) => {
this.onStartMutation();

const mutationId = this.generateNewMutationId();

let response;
try {
response = await this.mutate(options);
} catch (e) {
this.onMutationError(e, mutationId);
return;
}

this.onCompletedMutation(response, mutationId);
};

private mutate = (options: MutationOptions<TVariables>) => {

const {
mutation,
optimisticResponse,
refetchQueries,
update,
} = this.props;

const { variables } = options;

return this.client.mutate({
mutation,
variables,
optimisticResponse,
refetchQueries,
update,
});
};

private onStartMutation = () => {
if (!this.state.loading) {
this.setState({
loading: true,
error: undefined,
data: undefined,
notCalled: false,
});
}
};

private onCompletedMutation = (response, mutationId) => {

const { onCompleted } = this.props;

const data = response.data as TData;

const callOncomplete = () => {
if (onCompleted) {
onCompleted(data);
}
}

if (this.isMostRecentMutation(mutationId)) {
this.setState(
{
loading: false,
data,
},
() => {
callOncomplete();
},
);
} else {
callOncomplete();
}
};

private onMutationError = (error, mutationId) => {
const { onError } = this.props;

let apolloError = error as ApolloError;

const callOnError = () => {
if (onError) {
onError(apolloError);
}
}

if (this.isMostRecentMutation(mutationId)) {
this.setState(
{
loading: false,
error: apolloError,
},
() => {
callOnError();
},
);
}
else {
callOnError();
}
};

private generateNewMutationId = () => {
this.mostRecentMutationId = this.mostRecentMutationId + 1;
return this.mostRecentMutationId;
}

private isMostRecentMutation = mutationId => {
return this.mostRecentMutationId === mutationId;
}

private verifyDocumentIsMutation = mutation => {
const operation = parser(mutation);
invariant(
operation.type === DocumentType.Mutation,
`The <Mutation /> component requires a graphql mutation, but got a ${
operation.type === DocumentType.Query ? 'query' : 'subscription'
}.`,
);
};

private verifyContext = context => {
invariant(
!!context.client,
`Could not find "client" in the context of Mutation. Wrap the root component in an <ApolloProvider>`,
);
};
}

export default Mutation;
3 changes: 3 additions & 0 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export * from './ApolloProvider';
export { default as Query } from './Query';
export * from './Query';

export { default as Mutation } from './Mutation';
export * from './Mutation';

export { default as graphql } from './graphql';
export * from './graphql';

Expand Down
Loading