diff --git a/Changelog.md b/Changelog.md index 62a2387737..46ca7ea5dd 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ Expect active development and potentially significant breaking changes in the `0 - Feature: Remove nested imports for apollo-client. Making local development eaiser. [#234](https://github.com/apollostack/react-apollo/pull/234) - Feature: Move types to dev deps [#251](https://github.com/apollostack/react-apollo/pull/251) +- Feature: New method for skipping queries which bypasses HOC internals [#253](https://github.com/apollostack/react-apollo/pull/253) ### v0.5.7 diff --git a/src/graphql.tsx b/src/graphql.tsx index af246a2d46..71721b2ce6 100644 --- a/src/graphql.tsx +++ b/src/graphql.tsx @@ -52,6 +52,7 @@ export declare interface QueryOptions { noFetch?: boolean; pollInterval?: number; fragments?: FragmentDefinition[] | FragmentDefinition[][]; + // deprecated skip?: boolean; } @@ -62,6 +63,7 @@ const defaultQueryData = { const defaultMapPropsToOptions = props => ({}); const defaultMapResultToProps = props => props; +const defaultMapPropsToSkip = props => false; function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; @@ -109,6 +111,7 @@ export function withApollo(WrappedComponent) { export interface OperationOption { options?: Object | ((props: any) => QueryOptions | MutationOptions); props?: (props: any) => any; + skip?: boolean | ((props: any) => boolean); name?: string; withRef?: boolean; } @@ -119,10 +122,14 @@ export default function graphql( ) { // extract options - const { options = defaultMapPropsToOptions } = operationOptions; + const { options = defaultMapPropsToOptions, skip = defaultMapPropsToSkip } = operationOptions; + let mapPropsToOptions = options as (props: any) => QueryOptions | MutationOptions; if (typeof mapPropsToOptions !== 'function') mapPropsToOptions = () => options; + let mapPropsToSkip = skip as (props: any) => boolean; + if (typeof mapPropsToSkip !== 'function') mapPropsToSkip = (() => skip as any); + const mapResultToProps = operationOptions.props; // safety check on the operation @@ -181,6 +188,7 @@ export default function graphql( } function fetchData(props, { client }) { + if (mapPropsToSkip(props)) return; if (operation.type === DocumentType.Mutation) return false; const opts = calculateOptions(props) as any; opts.query = document; @@ -257,6 +265,7 @@ export default function graphql( this.queryObservable = {}; this.querySubscription = {}; + if (mapPropsToSkip(props)) return; this.setInitialProps(); } @@ -265,11 +274,16 @@ export default function graphql( this.hasMounted = true; if (this.type === DocumentType.Mutation) return; + if (mapPropsToSkip(this.props)) return; this.subscribeToQuery(this.props); - } componentWillReceiveProps(nextProps) { + // if this has changed, remove data and unsubscribeFromQuery + if (!mapPropsToSkip(this.props) && mapPropsToSkip(nextProps)) { + delete this.data; + return this.unsubscribeFromQuery(); + } if (shallowEqual(this.props, nextProps)) return; if (this.type === DocumentType.Mutation) { @@ -312,7 +326,7 @@ export default function graphql( const queryOptions = this.calculateOptions(this.props); const fragments = calculateFragments(queryOptions.fragments); - const { variables, forceFetch, skip } = queryOptions as QueryOptions; + const { variables, forceFetch, skip } = queryOptions as QueryOptions; // tslint:disable-line let queryData = assign({}, defaultQueryData) as any; queryData.variables = variables; @@ -558,6 +572,8 @@ export default function graphql( } render() { + if (mapPropsToSkip(this.props)) return createElement(WrappedComponent, this.props); + const { haveOwnPropsChanged, hasOperationDataChanged, renderedElement, props, data } = this; this.haveOwnPropsChanged = false; diff --git a/test/react-web/client/graphql/queries-1.test.tsx b/test/react-web/client/graphql/queries-1.test.tsx index 3714afdd26..21c381d24c 100644 --- a/test/react-web/client/graphql/queries-1.test.tsx +++ b/test/react-web/client/graphql/queries-1.test.tsx @@ -374,7 +374,7 @@ describe('queries', () => { renderer.create(); }); - it('allows you to skip a query', (done) => { + it('allows you to skip a query (deprecated)', (done) => { const query = gql`query people { allPeople(first: 1) { people { name } } }`; const data = { allPeople: { people: [ { name: 'Luke Skywalker' } ] } }; const networkInterface = mockNetworkInterface({ request: { query }, result: { data } }); @@ -400,6 +400,152 @@ describe('queries', () => { }, 25); }); + it('allows you to skip a query without running it', (done) => { + const query = gql`query people { allPeople(first: 1) { people { name } } }`; + const data = { allPeople: { people: [ { name: 'Luke Skywalker' } ] } }; + const networkInterface = mockNetworkInterface({ request: { query }, result: { data } }); + const client = new ApolloClient({ networkInterface }); + + let queryExecuted; + @graphql(query, { skip: ({ skip }) => skip }) + class Container extends React.Component { + componentWillReceiveProps(props) { + queryExecuted = true; + } + render() { + expect(this.props.data).toBeFalsy(); + return null; + } + }; + + renderer.create(); + + setTimeout(() => { + if (!queryExecuted) { done(); return; } + done(new Error('query ran even though skip present')); + }, 25); + }); + + it('doesn\'t run options or props when skipped', (done) => { + const query = gql`query people { allPeople(first: 1) { people { name } } }`; + const data = { allPeople: { people: [ { name: 'Luke Skywalker' } ] } }; + const networkInterface = mockNetworkInterface({ request: { query }, result: { data } }); + const client = new ApolloClient({ networkInterface }); + + let queryExecuted; + @graphql(query, { + skip: ({ skip }) => skip, + options: ({ willThrowIfAccesed }) => ({ pollInterval: willThrowIfAccesed.pollInterval }), + props: ({ willThrowIfAccesed }) => ({ pollInterval: willThrowIfAccesed.pollInterval }), + }) + class Container extends React.Component { + componentWillReceiveProps(props) { + queryExecuted = true; + } + render() { + expect(this.props.data).toBeFalsy(); + return null; + } + }; + + renderer.create(); + + setTimeout(() => { + if (!queryExecuted) { done(); return; } + done(new Error('query ran even though skip present')); + }, 25); + }); + + it('allows you to skip a query without running it (alternate syntax)', (done) => { + const query = gql`query people { allPeople(first: 1) { people { name } } }`; + const data = { allPeople: { people: [ { name: 'Luke Skywalker' } ] } }; + const networkInterface = mockNetworkInterface({ request: { query }, result: { data } }); + const client = new ApolloClient({ networkInterface }); + + let queryExecuted; + @graphql(query, { skip: true }) + class Container extends React.Component { + componentWillReceiveProps(props) { + queryExecuted = true; + } + render() { + expect(this.props.data).toBeFalsy(); + return null; + } + }; + + renderer.create(); + + setTimeout(() => { + if (!queryExecuted) { done(); return; } + done(new Error('query ran even though skip present')); + }, 25); + }); + + it('removes the injected props if skip becomes true', (done) => { + let count = 0; + const query = gql` + query people($first: Int) { + allPeople(first: $first) { people { name } } + } + `; + + const data1 = { allPeople: { people: [ { name: 'Luke Skywalker' } ] } }; + const variables1 = { first: 1 }; + + const data2 = { allPeople: { people: [ { name: 'Leia Skywalker' } ] } }; + const variables2 = { first: 2 }; + + const networkInterface = mockNetworkInterface( + { request: { query, variables: variables1 }, result: { data: data1 } }, + { request: { query, variables: variables2 }, result: { data: data2 } } + ); + + const client = new ApolloClient({ networkInterface }); + + @graphql(query, { + skip: () => count === 1, + options: (props) => ({ variables: props }), + }) + class Container extends React.Component { + componentWillReceiveProps({ data }) { + // loading is true, but data still there + if (count === 0) expect(data.allPeople).toEqual(data1.allPeople); + if (count === 1 ) expect(data).toBeFalsy(); + if (count === 2 && data.loading) expect(data.allPeople).toBeFalsy(); + if (count === 2 && !data.loading) { + expect(data.allPeople).toEqual(data2.allPeople); + done(); + } + } + render() { + return null; + } + }; + + class ChangingProps extends React.Component { + state = { first: 1 }; + + componentDidMount() { + setTimeout(() => { + count++; + this.setState({ first: 2 }); + }, 50); + + setTimeout(() => { + count++; + this.setState({ first: 3 }); + }, 100); + } + + render() { + return ; + } + } + + renderer.create(); + }); + it('reruns the query if it changes', (done) => { let count = 0; const query = gql` diff --git a/test/react-web/server/index.test.tsx b/test/react-web/server/index.test.tsx index 7cfa44451b..3c6a8378c7 100644 --- a/test/react-web/server/index.test.tsx +++ b/test/react-web/server/index.test.tsx @@ -55,6 +55,52 @@ describe('SSR', () => { ; }); + it('should correctly skip queries (deprecated)', () => { + + const query = gql`{ currentUser { firstName } }`; + const data = { currentUser: { firstName: 'James' } }; + const networkInterface = mockNetworkInterface( + { request: { query }, result: { data }, delay: 50 } + ); + const apolloClient = new ApolloClient({ networkInterface }); + + const WrappedElement = graphql(query, { options: { skip: true }})(({ data }) => ( +
{data.loading ? 'loading' : 'skipped'}
+ )); + + const app = (); + + return getDataFromTree(app) + .then(() => { + const markup = ReactDOM.renderToString(app); + expect(markup).toMatch(/skipped/); + }) + ; + }); + + it('should correctly skip queries (deprecated)', () => { + + const query = gql`{ currentUser { firstName } }`; + const data = { currentUser: { firstName: 'James' } }; + const networkInterface = mockNetworkInterface( + { request: { query }, result: { data }, delay: 50 } + ); + const apolloClient = new ApolloClient({ networkInterface }); + + const WrappedElement = graphql(query, { skip: true })(({ data }) => ( +
{!data ? 'skipped' : 'dang'}
+ )); + + const app = (); + + return getDataFromTree(app) + .then(() => { + const markup = ReactDOM.renderToString(app); + expect(markup).toMatch(/skipped/); + }) + ; + }); + it('should run return the initial state for hydration', () => { const query = gql`{ currentUser { firstName } }`; const data = { currentUser: { firstName: 'James' } };