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

recycled query teardown #740

Merged
merged 6 commits into from
Jun 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#### BREAKING FOR TYPESCRIPT USERS
- Feature: Enhanced typescript definitions to allow for more valid type checking of graphql HOC [PR #695](https://github.com/apollographql/react-apollo/pull/695)
- Feature: Flow types: [PR #695](https://github.com/apollographql/react-apollo/pull/695)

- Fix: Fix bug with sync re-renders and recyled queries [PR #740](https://github.com/apollographql/react-apollo/pull/740)

### 1.3.0
- Feature: Support tree shaking and smaller (marginally) bundles via rollup [PR #691](https://github.com/apollographql/react-apollo/pull/691)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
"uglify-js": "^3.0.13"
},
"dependencies": {
"apollo-client": "^1.2.2",
"apollo-client": "^1.4.0",
"graphql-anywhere": "^3.0.0",
"graphql-tag": "^2.0.0",
"hoist-non-react-statics": "^1.2.0",
Expand Down
6 changes: 6 additions & 0 deletions src/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,13 @@ export default function graphql<TResult = {}, TProps = {}, TChildProps = Default
delete this.queryObservable;
}

// It is critical that this happens prior to recyling the query
// if not it breaks the loading state / network status because
// an orphan observer is created in AC (intended) which is cleaned up
// when the browser has time via a setTimeout(0)
// Unsubscribe from our query subscription.
this.unsubscribeFromQuery();

}

if (this.type === DocumentType.Subscription) this.unsubscribeFromQuery();
Expand Down Expand Up @@ -609,6 +614,7 @@ class ObservableQueryRecycler {
observableQuery.setOptions({
fetchPolicy: 'standby',
pollInterval: 0,
fetchResults: false, // ensure we don't create another observer in AC
});

this.observableQueries.push({
Expand Down
123 changes: 95 additions & 28 deletions test/react-web/client/graphql/queries/observableQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function wait(ms) {
describe('[queries] observableQuery', () => {

// observableQuery
it('will recycle `ObservableQuery`s when re-rendering the entire tree', () => {
it('will recycle `ObservableQuery`s when re-rendering the entire tree', (done) => {
const query = gql`query people { allPeople(first: 1) { people { name } } }`;
const data = { allPeople: { people: [ { name: 'Luke Skywalker' } ] } };
const networkInterface = mockNetworkInterface(
Expand All @@ -43,48 +43,115 @@ describe('[queries] observableQuery', () => {
);
const client = new ApolloClient({ networkInterface, addTypename: false });

@graphql(query)
// storage
let queryObservable1: ObservableQuery<any>;
let queryObservable2: ObservableQuery<any>;
let originalOptions;
let wrapper1;
let wrapper2;
let count = 0;
let recycledOptions;

const assert1 = () => {
expect(Object.keys((client as any).queryManager.observableQueries)).toEqual(['1']);
queryObservable1 = (client as any).queryManager.observableQueries['1'].observableQuery;
originalOptions = Object.assign({}, queryObservable1.options);
}

const assert2 = () => {
expect(Object.keys((client as any).queryManager.observableQueries)).toEqual(['1']);
}

@graphql(query, { options: { fetchPolicy: 'cache-and-network' }})
class Container extends React.Component<any, any> {
componentWillMount() {

// during the first mount, the loading prop should be true;
if (count === 0) {
expect(this.props.data.loading).toBe(true);
}

// during the second mount, the loading prop should be false, and data should
// be present;
if (count === 3) {
expect(this.props.data.loading).toBe(false);
expect(this.props.data.allPeople).toEqual(data.allPeople);
}
}

componentDidMount(){
if (count === 4) {
wrapper1.unmount();
done();
}
}

componentDidUpdate(prevProps) {
if (count === 3) {
expect(prevProps.data.loading).toBe(true);
expect(this.props.data.loading).toBe(false);
expect(this.props.data.allPeople).toEqual(data.allPeople);

// ensure first assertion and umount tree
assert1();
wrapper1.find("#break").simulate("click");

// ensure cleanup
assert2();
}
}

render () {
// side effect to keep track of render counts
count++;
return null;
}
}

const wrapper1 = renderer.create(
<ApolloProvider client={client}>
<Container/>
</ApolloProvider>
);
class RedirectOnMount extends React.Component<any, any> {
componentWillMount() {
this.props.onMount();
}

expect(Object.keys((client as any).queryManager.observableQueries)).toEqual(['1']);
const queryObservable1: ObservableQuery<any> = (client as any).queryManager.observableQueries['1'].observableQuery;
render() {
return null;
}
}

const originalOptions = Object.assign({}, queryObservable1.options);
class AppWrapper extends React.Component<any, any> {
state = {
renderRedirect: false,
};

wrapper1.unmount();
goToRedirect = () => {
this.setState({ renderRedirect: true });
};

expect(Object.keys((client as any).queryManager.observableQueries)).toEqual(['1']);
handleRedirectMount = () => {
this.setState({ renderRedirect: false });
};

const wrapper2 = renderer.create(
render() {
if (this.state.renderRedirect) {
return <RedirectOnMount onMount={this.handleRedirectMount} />;
} else {
return (
<div>
<Container />
<button id="break" onClick={this.goToRedirect}>Break things</button>
</div>
);
}
}
}

wrapper1 = mount(
<ApolloProvider client={client}>
<Container/>
<AppWrapper/>
</ApolloProvider>
);

expect(Object.keys((client as any).queryManager.observableQueries)).toEqual(['1']);
const queryObservable2: ObservableQuery<any> = (client as any).queryManager.observableQueries['1'].observableQuery;

const recycledOptions = queryObservable2.options;

expect(queryObservable1).toBe(queryObservable2);
expect(recycledOptions.query).toEqual(originalOptions.query);
expect(recycledOptions.metadata).toEqual(originalOptions.metadata);
expect(recycledOptions.notifyOnNetworkStatusChange).toEqual(originalOptions.notifyOnNetworkStatusChange);

wrapper2.unmount();

expect(Object.keys((client as any).queryManager.observableQueries)).toEqual(['1']);
});
});

it('will not try to refetch recycled `ObservableQuery`s when resetting the client store', (done) => {
const query = gql`query people { allPeople(first: 1) { people { name } } }`;
Expand Down