v2.1 Implemented the Query Component #1398
Changes from 2 commits
c50a56a
0e507a5
028f26e
23934e9
af7df0c
7d4ab66
44706a5
b1f6879
e1e49be
42c84d3
a48b79d
9e682f5
faf3993
581ffc2
c94afd2
9b2a5d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = { | ||
query: DocumentNode; | ||
options?: QueryOpts; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() }); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For these methods, instead of underscore, use |
||
|
||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just put |
||
|
||
if (result.loading && loading) { | ||
return loading(); | ||
} | ||
|
||
if (result.error && error) { | ||
return error(result.error); | ||
} | ||
|
||
return render(result); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I much prefer the render callback to execute There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
There was a problem hiding this comment.
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 asQueryProps
here - aids is composition of any components that might use these.