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

feat(@apollo/react-hooks): implement skip option for useSubscription hook #3356

Merged
merged 9 commits into from Aug 23, 2019
1 change: 1 addition & 0 deletions packages/common/src/types/types.ts
Expand Up @@ -161,6 +161,7 @@ export interface BaseSubscriptionOptions<
| boolean
| ((options: BaseSubscriptionOptions<TData, TVariables>) => boolean);
client?: ApolloClient<object>;
skip?: boolean;
onSubscriptionData?: (options: OnSubscriptionDataOptions<TData>) => any;
onSubscriptionComplete?: () => void;
}
Expand Down
158 changes: 158 additions & 0 deletions packages/hooks/src/__tests__/useSubscription.test.tsx
Expand Up @@ -141,4 +141,162 @@ describe('useSubscription Hook', () => {
</ApolloProvider>
).unmount;
});

it('should never execute a subscription with the skip option', done => {
const subscription = gql`
subscription {
car {
make
}
}
`;

// const results = [
// {
// result: { data: { car: { make: 'Pagani' } } }
// }
// ];

const link = new MockSubscriptionLink();
const client = new ApolloClient({
link,
cache: new Cache({ addTypename: false })
});

let renderCount = 0;
let onSubscriptionDataCount = 0;
let unmount: any;

const Component = () => {
const { loading, data, error } = useSubscription(subscription, {
skip: true,
onSubscriptionData() {
onSubscriptionDataCount += 1;
}
});
switch (renderCount) {
case 0:
expect(loading).toBe(false);
expect(error).toBeUndefined();
expect(data).toBeUndefined();
setTimeout(() => {
unmount();
setTimeout(() => {
expect(onSubscriptionDataCount).toEqual(0);
expect(renderCount).toEqual(1);
done();
});
});
break;
default:
}
renderCount += 1;
return null;
};

unmount = render(
<ApolloProvider client={client}>
<Component />
</ApolloProvider>
).unmount;
});

it('should create a subscription after skip has changed from true to a falsey value', done => {
const subscription = gql`
subscription {
car {
make
}
}
`;

const results = [
{
result: { data: { car: { make: 'Pagani' } } }
},
{
result: { data: { car: { make: 'Scoop' } } }
}
];

const link = new MockSubscriptionLink();
const client = new ApolloClient({
link,
cache: new Cache({ addTypename: false })
});

let renderCount = 0;
let onSubscriptionDataCount = 0;
let unmount: any;

const Component = () => {
const [, triggerRerender] = React.useState(0);
const [skip, setSkip] = React.useState(true);
const { loading, data, error } = useSubscription(subscription, {
skip,
onSubscriptionData() {
onSubscriptionDataCount += 1;
}
});
switch (renderCount) {
case 0:
expect(loading).toBe(false);
expect(error).toBeUndefined();
expect(data).toBeUndefined();
setSkip(false);
break;
case 1:
expect(loading).toBe(true);
expect(error).toBeUndefined();
expect(data).toBeUndefined();
link.simulateResult(results[0]);
break;
case 2:
expect(loading).toBe(false);
expect(data).toEqual(results[0].result.data);
setSkip(true);
break;
case 3:
expect(loading).toBe(false);
expect(data).toBeUndefined();
expect(error).toBeUndefined();
// ensure state persists across rerenders
triggerRerender(i => i + 1);
break;
case 4:
expect(loading).toBe(false);
expect(data).toBeUndefined();
expect(error).toBeUndefined();
setSkip(false);
break;
case 5:
expect(loading).toBe(true);
expect(error).toBeUndefined();
expect(data).toBeUndefined();
link.simulateResult(results[1]);
break;
case 6:
expect(loading).toBe(false);
expect(error).toBeUndefined();
expect(data).toEqual(results[1].result.data);
setTimeout(() => {
unmount();
setTimeout(() => {
expect(renderCount).toEqual(7);
done();
});
});
break;
default:
}
renderCount += 1;
return null;
};

unmount = render(
<ApolloProvider client={client}>
<Component />
</ApolloProvider>
).unmount;
});
});
24 changes: 23 additions & 1 deletion packages/hooks/src/data/SubscriptionData.ts
Expand Up @@ -27,6 +27,12 @@ export class SubscriptionData<

public execute(result: SubscriptionResult<TData>) {
let currentResult = result;
if (this.getOptions().skip === true) {
this.endSubscription();
delete this.currentObservable.query;
currentResult = this.getSkipResult();
return { ...currentResult, variables: this.getOptions().variables };
}

if (this.refreshClient().isNew) {
currentResult = this.getLoadingResult();
Expand All @@ -49,6 +55,14 @@ export class SubscriptionData<
currentResult = this.getLoadingResult();
}

if (
this.previousOptions.skip === true &&
Boolean(this.getOptions().skip) === false
n1ru4l marked this conversation as resolved.
Show resolved Hide resolved
) {
currentResult.loading = true;
currentResult.data = undefined;
}

this.initialize(this.getOptions());
this.startSubscription();

Expand All @@ -66,7 +80,7 @@ export class SubscriptionData<
}

private initialize(options: SubscriptionOptions<TData, TVariables>) {
if (this.currentObservable.query) return;
if (this.currentObservable.query || this.getOptions().skip === true) return;
this.currentObservable.query = this.refreshClient().client.subscribe({
query: options.subscription,
variables: options.variables,
Expand All @@ -93,6 +107,14 @@ export class SubscriptionData<
};
}

private getSkipResult() {
return {
loading: false,
error: undefined,
data: undefined
};
}

private updateResult(result: SubscriptionResult) {
if (this.isMounted) {
this.setResult(result);
Expand Down
8 changes: 4 additions & 4 deletions packages/hooks/src/useSubscription.ts
Expand Up @@ -10,14 +10,14 @@ export function useSubscription<TData = any, TVariables = OperationVariables>(
options?: SubscriptionHookOptions<TData, TVariables>
) {
const context = useContext(getApolloContext());
const updatedOptions = options
? { ...options, subscription }
: { subscription };
const [result, setResult] = useState({
loading: true,
loading: updatedOptions.skip ? false : true,
n1ru4l marked this conversation as resolved.
Show resolved Hide resolved
error: undefined,
data: undefined
});
const updatedOptions = options
? { ...options, subscription }
: { subscription };

const subscriptionDataRef = useRef<SubscriptionData<TData, TVariables>>();
function getSubscriptionDataRef() {
Expand Down