Skip to content

Commit

Permalink
Force a refetch in useLazyLoadQueryNode when the component is updated…
Browse files Browse the repository at this point in the history
… by fast refersh

Reviewed By: jstejada

Differential Revision: D19129463

fbshipit-source-id: 96f3135f20943e5c88036f2515fc0bd5c7dcd49f
  • Loading branch information
tyao1 authored and facebook-github-bot committed Dec 20, 2019
1 parent ff3bd43 commit 6f8e83d
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -68,6 +68,7 @@
"promise-polyfill": "6.1.0",
"react": "16.11.0",
"react-test-renderer": "16.11.0",
"react-refresh": "^0.4.0",
"signedsource": "^1.0.0",
"webpack": "^4.30.0",
"webpack-stream": "^5.1.1",
Expand Down
@@ -0,0 +1,176 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+relay
* @flow
* @format
*/

// flowlint ambiguous-object-type:error

'use strict';

const React = require('react');
const ReactTestRenderer = require('react-test-renderer');
const RelayEnvironmentProvider = require('../RelayEnvironmentProvider');

const useLazyLoadQueryNode = require('../useLazyLoadQueryNode');

const {createOperationDescriptor} = require('relay-runtime');

function expectToBeRendered(renderFn, readyState) {
// Ensure useEffect is called before other timers
ReactTestRenderer.act(() => {
jest.runAllImmediates();
});
expect(renderFn).toBeCalledTimes(1);
expect(renderFn.mock.calls[0][0]).toEqual(readyState);
renderFn.mockClear();
}

function expectToHaveFetched(environment, query) {
expect(environment.execute).toBeCalledTimes(1);
expect(environment.execute.mock.calls[0][0].operation).toMatchObject({
fragment: expect.anything(),
root: expect.anything(),
request: {
node: query.request.node,
variables: query.request.variables,
},
});
expect(
environment.mock.isLoading(query.request.node, query.request.variables, {
force: true,
}),
).toEqual(true);
}

describe('useLazyLoadQueryNode', () => {
let environment;
let gqlQuery;
let renderFn;
let createMockEnvironment;
let generateAndCompile;
let query;
let variables;

beforeEach(() => {
jest.resetModules();
jest.spyOn(console, 'warn').mockImplementationOnce(() => {});
jest.mock('../ExecutionEnvironment', () => ({
isServer: false,
}));

({
createMockEnvironment,
generateAndCompile,
} = require('relay-test-utils-internal'));

environment = createMockEnvironment();

const generated = generateAndCompile(`
fragment UserFragment on User {
name
}
query UserQuery($id: ID) {
node(id: $id) {
id
name
...UserFragment
}
}
`);
gqlQuery = generated.UserQuery;
variables = {id: '1'};
query = createOperationDescriptor(gqlQuery, variables);
renderFn = jest.fn(result => result?.node?.name ?? 'Empty');
});

afterEach(() => {
environment.mockClear();
jest.clearAllTimers();
});

it('force a refetch in fast refresh', () => {
// $FlowFixMe: We don't have the module on WWW, but also don't run the test there
const ReactRefreshRuntime = require('react-refresh/runtime');
ReactRefreshRuntime.injectIntoGlobalHook(global);
const V1 = function(props) {
const _query = createOperationDescriptor(gqlQuery, props.variables);
const result = useLazyLoadQueryNode<_>({
query: _query,
fetchPolicy: 'network-only',
componentDisplayName: 'TestDisplayName',
});
return renderFn(result);
};
ReactRefreshRuntime.register(V1, 'Renderer');

const instance = ReactTestRenderer.create(
<RelayEnvironmentProvider environment={environment}>
<React.Suspense fallback="Fallback">
<V1 variables={variables} />
</React.Suspense>
</RelayEnvironmentProvider>,
);

expect(instance.toJSON()).toEqual('Fallback');
expectToHaveFetched(environment, query);
expect(renderFn).not.toBeCalled();
expect(environment.retain).toHaveBeenCalledTimes(1);

expect(environment.execute).toBeCalledTimes(1);
ReactTestRenderer.act(() =>
environment.mock.resolve(gqlQuery, {
data: {
node: {
__typename: 'User',
id: variables.id,
name: 'Alice',
},
},
}),
);

const data = environment.lookup(query.fragment).data;
expectToBeRendered(renderFn, data);

environment.execute.mockClear();
renderFn.mockClear();
function V2(props) {
const _query = createOperationDescriptor(gqlQuery, props.variables);
const result = useLazyLoadQueryNode<_>({
query: _query,
fetchPolicy: 'network-only',
componentDisplayName: 'TestDisplayName',
});
return renderFn(result);
}
// Trigger a fast fresh
ReactRefreshRuntime.register(V2, 'Renderer');
ReactTestRenderer.act(() => {
ReactRefreshRuntime.performReactRefresh();
});
// It should start a new fetch in fast refresh
expectToHaveFetched(environment, query);
expect(renderFn).toBeCalledTimes(1);
expect(instance.toJSON()).toEqual('Fallback');
// It should render with the result of the new fetch
ReactTestRenderer.act(() =>
environment.mock.resolve(gqlQuery, {
data: {
node: {
__typename: 'User',
id: variables.id,
name: 'Bob',
},
},
}),
);
expect(instance.toJSON()).toEqual('Bob');
});
});
3 changes: 2 additions & 1 deletion packages/relay-experimental/package.json
Expand Up @@ -14,7 +14,8 @@
"@babel/runtime": "^7.0.0",
"fbjs": "^1.0.0",
"react-relay": "8.0.0",
"relay-runtime": "8.0.0"
"relay-runtime": "8.0.0",
"react-refresh": "^0.4.0"
},
"directories": {
"": "./"
Expand Down
29 changes: 28 additions & 1 deletion packages/relay-experimental/useLazyLoadQueryNode.js
Expand Up @@ -35,7 +35,7 @@ import type {
RenderPolicy,
} from 'relay-runtime';

const {useContext, useEffect} = React;
const {useContext, useEffect, useState, useRef} = React;

function useLazyLoadQueryNode<TQuery: OperationType>(args: {|
query: OperationDescriptor,
Expand Down Expand Up @@ -75,7 +75,34 @@ function useLazyLoadQueryNode<TQuery: OperationType>(args: {|
);
});

let _forceUpdate;
let _maybeFastRefresh;
if (__DEV__) {
/* eslint-disable react-hooks/rules-of-hooks */
[, _forceUpdate] = useState(0);
_maybeFastRefresh = useRef(false);
useEffect(() => {
return () => {
// Detect fast refresh, only runs multiple times in fast refresh
_maybeFastRefresh.current = true;
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
/* eslint-enable react-hooks/rules-of-hooks */
}

useEffect(() => {
if (__DEV__) {
if (_maybeFastRefresh && _maybeFastRefresh.current) {
/**
* This block only runs during fast refresh, the current resource and
* it's cache is disposed in the previous cleanup. Stop retaining and
* force a re-render to restart fetchObservable and retain correctly.
*/
_maybeFastRefresh.current = false;
_forceUpdate && _forceUpdate(n => n + 1);
return;
}
}
const disposable = QueryResource.retain(preparedQueryResult);
return () => {
disposable.dispose();
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Expand Up @@ -5921,6 +5921,11 @@ react-is@^16.8.6:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.10.2.tgz#984120fd4d16800e9a738208ab1fba422d23b5ab"
integrity sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==

react-refresh@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.4.2.tgz#54a277a6caaac2803d88f1d6f13c1dcfbd81e334"
integrity sha512-kv5QlFFSZWo7OlJFNYbxRtY66JImuP2LcrFgyJfQaf85gSP+byzG21UbDQEYjU7f//ny8rwiEkO6py2Y+fEgAQ==

react-test-renderer@16.11.0:
version "16.11.0"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.11.0.tgz#72574566496462c808ac449b0287a4c0a1a7d8f8"
Expand Down

0 comments on commit 6f8e83d

Please sign in to comment.