Skip to content

Commit

Permalink
Experimental hooks for useTransition() support (#1572)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #1572

Expose experimental `useTransition()` support for React 18  by exposing variants of Recoil hooks using a different rendering mode that works with `useTransition()`:

* `useRecoilValue_TRANSITION_SUPPORT_UNSTABLE()`
* `useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE()`
* `useRecoilState_TRANSITION_SUPPORT_UNSTABLE()`

Example usage to display previous state while a selector is pending:

```
function QueryResults() {
  const queryParams = useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(queryParamsAtom);
  const results = useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(myQuerySelector(queryParams));
  return results;
}

function MyApp() {
  const [queryParams, setQueryParams] = useRecoilState_TRANSITION_SUPPORT_UNSTABLE(queryParamsAtom);
  const [inTransition, startTransition] = useTransition();
  return (
    <div>
      {inTransition ? <div>[Loading new results...]</div> : ''}
      Results: <React.Suspense><QueryResults /></React.Suspense>
      <button
        onClick={() => {
          startTransition(() => {
            setQueryParams(...new params...);
          });
        }
      >
        New Query
      </button>
    </div>
  );
}
```

Reviewed By: habond

Differential Revision: D33812933

fbshipit-source-id: 1c4c80a6d6acfe1626dc815abf6358a02a0d6f5f
  • Loading branch information
drarmstr authored and facebook-github-bot committed Jan 29, 2022
1 parent 15af2a2 commit 708fcfe
Show file tree
Hide file tree
Showing 10 changed files with 517 additions and 24 deletions.
8 changes: 1 addition & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,17 @@

**_Add new changes here as they land_**

### Pending

- Memory management
- useTransition() compatibility for OSS

## 0.6 ()

- React 18
- Leverage new React 18 APIs for improved safety and optimizations. (#1488)
- Fixes for `<StrictMode>` (#1473, #1444, #1509).
- `useTransition()` is not yet supported for open source React.
- Experimental support for `useTransition()` using hooks with `_TRANSITION_SUPPORT_UNSTABLE` suffix. (#1572, #1560)
- Recoil updates now re-render earlier:
- Recoil and React state changes from the same batch now stay in sync. (#1076)
- Renders now occur before transaction observers instead of after.

### New Features

- Add `refresh()` to the `useRecoilCallback()` interface for refreshing selector caches. (#1413)
- Callbacks from selector's `getCallback()` can now mutate, refresh, and transact Recoil state, in addition to reading it, for parity with `useRecoilCallback()`. (#1498)
- Recoil StoreID's for `<RecoilRoot>` and `Snapshot` stores accessible via `useRecoilStoreID()` hook (#1417) or `storeID` parameter for atom effects (#1414).
Expand Down
6 changes: 6 additions & 0 deletions packages/recoil/Recoil_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ const {retentionZone} = require('./core/Recoil_RetentionZone');
const {freshSnapshot} = require('./core/Recoil_Snapshot');
const {
useRecoilState,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
useRecoilStateLoadable,
useRecoilValue,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValueLoadable,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useResetRecoilState,
useSetRecoilState,
} = require('./hooks/Recoil_Hooks');
Expand Down Expand Up @@ -119,6 +122,9 @@ module.exports = {
useResetRecoilState,
useGetRecoilValueInfo_UNSTABLE: useGetRecoilValueInfo,
useRecoilRefresher_UNSTABLE: useRecoilRefresher,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,

// Hooks for complex operations
useRecoilCallback,
Expand Down
8 changes: 4 additions & 4 deletions packages/recoil/core/Recoil_ReactMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const useSyncExternalStore: <T>(
(React: any).unstable_useSyncExternalStore;

type ReactMode =
| 'CONCURRENT_SUPPORT'
| 'TRANSITION_SUPPORT'
| 'SYNC_EXTERNAL_STORE'
| 'MUTABLE_SOURCE'
| 'LEGACY';
Expand All @@ -53,13 +53,13 @@ type ReactMode =
* 1) earlier
* 2) in sync with React updates in the same batch
* 3) before transaction observers instead of after.
* concurrent: Is the current mode compatible with Concurrent Mode (i.e. useTransition())
* concurrent: Is the current mode compatible with Concurrent Mode and useTransition()
*/
function reactMode(): {mode: ReactMode, early: boolean, concurrent: boolean} {
// NOTE: This mode is currently broken with some Suspense cases
// see Recoil_selector-test.js
if (gkx('recoil_concurrent_support')) {
return {mode: 'CONCURRENT_SUPPORT', early: true, concurrent: true};
if (gkx('recoil_transition_support')) {
return {mode: 'TRANSITION_SUPPORT', early: true, concurrent: true};
}

if (gkx('recoil_sync_external_store') && useSyncExternalStore != null) {
Expand Down
64 changes: 62 additions & 2 deletions packages/recoil/hooks/Recoil_Hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const differenceSets = require('recoil-shared/util/Recoil_differenceSets');
const err = require('recoil-shared/util/Recoil_err');
const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation');
const gkx = require('recoil-shared/util/Recoil_gkx');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
const useComponentName = require('recoil-shared/util/Recoil_useComponentName');

function handleLoadable<T>(
Expand Down Expand Up @@ -421,7 +422,7 @@ function useRecoilValueLoadable_MUTABLE_SOURCE<T>(
return loadable;
}

function useRecoilValueLoadable_CONCURRENT_SUPPORT<T>(
function useRecoilValueLoadable_TRANSITION_SUPPORT<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
const storeRef = useStoreRef();
Expand Down Expand Up @@ -578,7 +579,7 @@ function useRecoilValueLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T> {
useRetain(recoilValue);
}
return {
CONCURRENT_SUPPORT: useRecoilValueLoadable_CONCURRENT_SUPPORT,
TRANSITION_SUPPORT: useRecoilValueLoadable_TRANSITION_SUPPORT,
SYNC_EXTERNAL_STORE: useRecoilValueLoadable_SYNC_EXTERNAL_STORE,
MUTABLE_SOURCE: useRecoilValueLoadable_MUTABLE_SOURCE,
LEGACY: useRecoilValueLoadable_LEGACY,
Expand Down Expand Up @@ -679,6 +680,62 @@ function useSetUnvalidatedAtomValues(): (
};
}

/**
* Experimental variants of hooks with support for useTransition()
*/

function useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
if (__DEV__) {
validateRecoilValue(
recoilValue,
'useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE',
);
if (!reactMode().early) {
recoverableViolation(
'Attepmt to use a hook with UNSTABLE_TRANSITION_SUPPORT in a rendering mode incompatible with concurrent rendering. Try enabling the recoil_sync_external_store or recoil_transition_support GKs.',
'recoil',
);
}
}
if (gkx('recoil_memory_managament_2020')) {
// eslint-disable-next-line fb-www/react-hooks
useRetain(recoilValue);
}
return useRecoilValueLoadable_TRANSITION_SUPPORT(recoilValue);
}

function useRecoilValue_TRANSITION_SUPPORT_UNSTABLE<T>(
recoilValue: RecoilValue<T>,
): T {
if (__DEV__) {
validateRecoilValue(
recoilValue,
'useRecoilValue_TRANSITION_SUPPORT_UNSTABLE',
);
}
const storeRef = useStoreRef();
const loadable =
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(recoilValue);
return handleLoadable(loadable, recoilValue, storeRef);
}

function useRecoilState_TRANSITION_SUPPORT_UNSTABLE<T>(
recoilState: RecoilState<T>,
): [T, SetterOrUpdater<T>] {
if (__DEV__) {
validateRecoilValue(
recoilState,
'useRecoilState_TRANSITION_SUPPORT_UNSTABLE',
);
}
return [
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(recoilState),
useSetRecoilState(recoilState),
];
}

module.exports = {
recoilComponentGetRecoilValueCount_FOR_TESTING,
useRecoilInterface: useRecoilInterface_DEPRECATED,
Expand All @@ -689,4 +746,7 @@ module.exports = {
useResetRecoilState,
useSetRecoilState,
useSetUnvalidatedAtomValues,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* 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+recoil
* @flow strict-local
* @format
*/
'use strict';

// Sanity tests for *_TRANSITION_SUPPORT_UNSTABLE() hooks. The actual tests
// for useTransition() support are in Recoil_useTransition-test.js

const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');

let React,
act,
selector,
stringAtom,
asyncSelector,
flushPromisesAndTimers,
renderElements,
useRecoilState,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValue,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValueLoadable,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useSetRecoilState,
reactMode;

const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));

selector = require('../../recoil_values/Recoil_selector');
({
stringAtom,
asyncSelector,
flushPromisesAndTimers,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
({
useRecoilState,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValue,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValueLoadable,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useSetRecoilState,
} = require('../Recoil_Hooks'));
});

testRecoil('useRecoilValue_TRANSITION_SUPPORT_UNSTABLE', async () => {
if (!reactMode().early) {
return;
}
const myAtom = stringAtom();
const [mySelector, resolve] = asyncSelector();
let setAtom;
function Component() {
setAtom = useSetRecoilState(myAtom);
return [
useRecoilValue(myAtom),
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(myAtom),
useRecoilValue(mySelector),
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(mySelector),
].join(' ');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('loading');

act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE');

act(() => setAtom('SET'));
expect(c.textContent).toBe('SET SET RESOLVE RESOLVE');
});

testRecoil('useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE', async () => {
if (!reactMode().early) {
return;
}
const myAtom = stringAtom();
const [mySelector, resolve] = asyncSelector();
let setAtom;
function Component() {
setAtom = useSetRecoilState(myAtom);
return [
useRecoilValueLoadable(myAtom).getValue(),
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(myAtom).getValue(),
useRecoilValueLoadable(mySelector).getValue(),
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(mySelector).getValue(),
].join(' ');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('loading');

act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE');

act(() => setAtom('SET'));
expect(c.textContent).toBe('SET SET RESOLVE RESOLVE');
});

testRecoil('useRecoilState_TRANSITION_SUPPORT_UNSTABLE', async () => {
if (!reactMode().early) {
return;
}
const myAtom = stringAtom();
const [myAsyncSelector, resolve] = asyncSelector();
const mySelector = selector({
key: 'useRecoilState_TRANSITION_SUPPORT_UNSTABLE selector',
get: () => myAsyncSelector,
set: ({set}, newValue) => set(myAtom, newValue),
});
let setAtom, setSelector;
function Component() {
const [v1] = useRecoilState(myAtom);
const [v2, setAtomValue] =
useRecoilState_TRANSITION_SUPPORT_UNSTABLE(myAtom);
setAtom = setAtomValue;
const [v3] = useRecoilState(mySelector);
const [v4, setSelectorValue] =
useRecoilState_TRANSITION_SUPPORT_UNSTABLE(mySelector);
setSelector = setSelectorValue;
return [v1, v2, v3, v4].join(' ');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('loading');

act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE');

act(() => setAtom('SET'));
expect(c.textContent).toBe('SET SET RESOLVE RESOLVE');

act(() => setSelector('SETS'));
expect(c.textContent).toBe('SETS SETS RESOLVE RESOLVE');
});
4 changes: 2 additions & 2 deletions packages/recoil/hooks/__tests__/Recoil_PublicHooks-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ describe('Render counts', () => {
if (
(reactMode().mode === 'LEGACY' &&
!gks.includes('recoil_suppress_rerender_in_callback')) ||
reactMode().mode === 'CONCURRENT_SUPPORT'
reactMode().mode === 'TRANSITION_SUPPORT'
) {
baseCalls += 1;
}
Expand Down Expand Up @@ -579,7 +579,7 @@ describe('Component Subscriptions', () => {
if (
(reactMode().mode === 'LEGACY' &&
!gks.includes('recoil_suppress_rerender_in_callback')) ||
reactMode().mode === 'CONCURRENT_SUPPORT'
reactMode().mode === 'TRANSITION_SUPPORT'
) {
baseCalls += 1;
}
Expand Down
Loading

0 comments on commit 708fcfe

Please sign in to comment.