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

v2.1 Implemented the Query Component #1398

Merged
merged 16 commits into from Dec 22, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
164 changes: 164 additions & 0 deletions src/Query.tsx
@@ -0,0 +1,164 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import ApolloClient, { ObservableQuery } from 'apollo-client';
import { DocumentNode } from 'graphql';
import { ZenObservable } from 'zen-observable-ts';
import * as invariant from 'invariant';
import * as pick from 'lodash.pick';

import shallowEqual from './shallowEqual';
import ApolloConsumer from './ApolloConsumer';

import {
MutationOpts,
ChildProps,
OperationOption,
ComponentDecorator,
QueryOpts,
QueryProps,
MutationFunc,
OptionProps,
} from './types';

type Props = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use an interface and it will need to be exported when tsc -d runs and creates the .d.ts file. I usually name these after the component so they are more useful in autocomplete, such as QueryProps here - aids is composition of any components that might use these.

query: DocumentNode;
options?: QueryOpts;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely agree that we should flatten the options here - no need for a nested options object.

skip?: Boolean;
loading?: () => React.ReactNode;
error?: (error: any) => React.ReactNode;
render?: (result: any) => React.ReactNode;
};

type State = {
result: any;
};

function observableQueryFields(observable) {
const fields = pick(
observable,
'variables',
'refetch',
'fetchMore',
'updateQuery',
'startPolling',
'stopPolling',
);

Object.keys(fields).forEach(key => {
if (typeof fields[key] === 'function') {
fields[key] = fields[key].bind(observable);
}
});

return fields;
}

class Query extends React.Component<Props, State> {
private client: ApolloClient<any>;
private queryObservable: ObservableQuery<any>;
private querySubscription: ZenObservable.Subscription;

static contextTypes = {
client: PropTypes.object.isRequired,
};

constructor(props, context) {
super(props, context);

invariant(
!!context.client,
`Could not find "client" in the context of Query. Wrap the root component in an <ApolloProvider>`,
);
this.client = context.client;

this._initializeQueryObservable(props);
this.state = {
result: this.queryObservable.currentResult(),
};
}

componentDidMount() {
if (this.props.skip) {
return;
}
this._startQuerySubscription();
}

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

if (nextProps.skip) {
if (!this.props.skip) {
this._removeQuerySubscription();
}
return;
}
this._removeQuerySubscription();
this._initializeQueryObservable(nextProps);
this._startQuerySubscription();
this._updateCurrentData();
}

componentWillUnmount() {
this._removeQuerySubscription();
}

_initializeQueryObservable = props => {
const { options, query } = props;

const clientOptions = { ...options, query };

this.queryObservable = this.client.watchQuery(clientOptions);
};

_startQuerySubscription = () => {
this.querySubscription = this.queryObservable.subscribe({
next: this._updateCurrentData,
error: this._updateCurrentData,
});
};

_removeQuerySubscription = () => {
if (this.querySubscription) {
this.querySubscription.unsubscribe();
}
};
_updateCurrentData = () => {
this.setState({ result: this.queryObservable.currentResult() });
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these methods, instead of underscore, use private updateCurrentData to scope them. I may PR a tslint update that will auto fix the current codebase.


getRenderProps = () => {
const { result } = this.state;

const { loading, error, networkStatus, data } = result;

const renderProps = {
data,
loading,
error,
networkStatus,
...observableQueryFields(this.queryObservable),
};

return renderProps;
};

render() {
const { render, loading, error } = this.props;
const result = this.getRenderProps();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just put getRenderProps implementation into render. Alternatively you could keep it and rename it to getResult to be consistent.


if (result.loading && loading) {
return loading();
}

if (result.error && error) {
return error(result.error);
}

return render(result);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I much prefer the render callback to execute children, as noted in general comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer to my comment in #1399 about my thoughts here. I will leave it as a render prop for now and await feedback from some more people before updating anything

}
}

export default Query;