Skip to content

Commit

Permalink
Add unstable prefix to experimental APIs
Browse files Browse the repository at this point in the history
We've been shipping unprefixed experimental APIs (like `createRoot` and
`useTransition`) to the Experimental release channel, with the rationale
that because these APIs do not appear in any stable release, we're free
to change or remove them later without breaking any downstream projects.

What we didn't consider is that downstream projects might be tempted to
use feature detection:

```js
const useTransition = React.useTransition || fallbackUseTransition;
```

This pattern assumes that the version of `useTransition` that exists in
the Experimental channel today has the same API contract as the final
`useTransition` API that we'll eventually ship to stable.

To discourage feature detection, I've added an `unstable_` prefix to
all of our unstable APIs.

The Facebook builds still have the unprefixed APIs, though. We will
continue to support those; if we make any breaking changes, we'll
migrate the internal callers like we usually do. To make testing easier,
I added the `unstable_`-prefixed APIs to the www builds, too. That way
our tests can always use the prefixed ones without gating on the
release channel.
  • Loading branch information
acdlite committed May 5, 2020
1 parent cd4a960 commit 9fe4075
Show file tree
Hide file tree
Showing 31 changed files with 280 additions and 214 deletions.
2 changes: 1 addition & 1 deletion fixtures/blocks/src/server/App.block.js
Expand Up @@ -7,7 +7,7 @@
/* eslint-disable import/first */

import * as React from 'react';
import {block} from 'react';
import {unstable_block as block} from 'react';

// Server

Expand Down
2 changes: 1 addition & 1 deletion fixtures/dom/src/__tests__/wrong-act-test.js
Expand Up @@ -179,7 +179,7 @@ it("doesn't warn if you use nested acts from different renderers", () => {

if (__EXPERIMENTAL__) {
it('warns when using createRoot() + .render', () => {
const root = ReactDOM.createRoot(document.createElement('div'));
const root = ReactDOM.unstable_createRoot(document.createElement('div'));
expect(() => {
TestRenderer.act(() => {
root.render(<App />);
Expand Down
Expand Up @@ -366,121 +366,123 @@ describe('ReactHooksInspectionIntegration', () => {
]);
});

if (__EXPERIMENTAL__) {
it('should support composite useTransition hook', () => {
function Foo(props) {
React.useTransition();
const memoizedValue = React.useMemo(() => 'hello', []);
return <div>{memoizedValue}</div>;
}
const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{
id: 0,
isStateEditable: false,
name: 'Transition',
value: undefined,
subHooks: [],
},
{
id: 1,
isStateEditable: false,
name: 'Memo',
value: 'hello',
subHooks: [],
},
]);
});

it('should support composite useDeferredValue hook', () => {
function Foo(props) {
React.useDeferredValue('abc', {
timeoutMs: 500,
});
const [state] = React.useState(() => 'hello', []);
return <div>{state}</div>;
}
const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{
id: 0,
isStateEditable: false,
name: 'DeferredValue',
value: 'abc',
subHooks: [],
},
{
id: 1,
isStateEditable: true,
name: 'State',
value: 'hello',
subHooks: [],
},
]);
});

it('should support composite useOpaqueIdentifier hook', () => {
function Foo(props) {
const id = React.unstable_useOpaqueIdentifier();
const [state] = React.useState(() => 'hello', []);
return <div id={id}>{state}</div>;
}

const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);

expect(tree.length).toEqual(2);

expect(tree[0].id).toEqual(0);
expect(tree[0].isStateEditable).toEqual(false);
expect(tree[0].name).toEqual('OpaqueIdentifier');
expect((tree[0].value + '').startsWith('c_')).toBe(true);
// @gate experimental
it('should support composite useTransition hook', () => {
function Foo(props) {
React.unstable_useTransition();
const memoizedValue = React.useMemo(() => 'hello', []);
return <div>{memoizedValue}</div>;
}
const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{
id: 0,
isStateEditable: false,
name: 'Transition',
value: undefined,
subHooks: [],
},
{
id: 1,
isStateEditable: false,
name: 'Memo',
value: 'hello',
subHooks: [],
},
]);
});

expect(tree[1]).toEqual({
// @gate experimental
it('should support composite useDeferredValue hook', () => {
function Foo(props) {
React.unstable_useDeferredValue('abc', {
timeoutMs: 500,
});
const [state] = React.useState(() => 'hello', []);
return <div>{state}</div>;
}
const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{
id: 0,
isStateEditable: false,
name: 'DeferredValue',
value: 'abc',
subHooks: [],
},
{
id: 1,
isStateEditable: true,
name: 'State',
value: 'hello',
subHooks: [],
});
},
]);
});

// @gate experimental
it('should support composite useOpaqueIdentifier hook', () => {
function Foo(props) {
const id = React.unstable_useOpaqueIdentifier();
const [state] = React.useState(() => 'hello', []);
return <div id={id}>{state}</div>;
}

const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);

expect(tree.length).toEqual(2);

expect(tree[0].id).toEqual(0);
expect(tree[0].isStateEditable).toEqual(false);
expect(tree[0].name).toEqual('OpaqueIdentifier');
expect((tree[0].value + '').startsWith('c_')).toBe(true);

expect(tree[1]).toEqual({
id: 1,
isStateEditable: true,
name: 'State',
value: 'hello',
subHooks: [],
});
});

it('should support composite useOpaqueIdentifier hook in concurrent mode', () => {
function Foo(props) {
const id = React.unstable_useOpaqueIdentifier();
const [state] = React.useState(() => 'hello', []);
return <div id={id}>{state}</div>;
}
// @gate experimental
it('should support composite useOpaqueIdentifier hook in concurrent mode', () => {
function Foo(props) {
const id = React.unstable_useOpaqueIdentifier();
const [state] = React.useState(() => 'hello', []);
return <div id={id}>{state}</div>;
}

const renderer = ReactTestRenderer.create(<Foo />, {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushWithoutYielding();
const renderer = ReactTestRenderer.create(<Foo />, {
unstable_isConcurrent: true,
});
expect(Scheduler).toFlushWithoutYielding();

const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);

expect(tree.length).toEqual(2);
expect(tree.length).toEqual(2);

expect(tree[0].id).toEqual(0);
expect(tree[0].isStateEditable).toEqual(false);
expect(tree[0].name).toEqual('OpaqueIdentifier');
expect((tree[0].value + '').startsWith('c_')).toBe(true);
expect(tree[0].id).toEqual(0);
expect(tree[0].isStateEditable).toEqual(false);
expect(tree[0].name).toEqual('OpaqueIdentifier');
expect((tree[0].value + '').startsWith('c_')).toBe(true);

expect(tree[1]).toEqual({
id: 1,
isStateEditable: true,
name: 'State',
value: 'hello',
subHooks: [],
});
expect(tree[1]).toEqual({
id: 1,
isStateEditable: true,
name: 'State',
value: 'hello',
subHooks: [],
});
}
});

describe('useDebugValue', () => {
it('should support inspectable values for multiple custom hooks', () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Expand Up @@ -352,18 +352,18 @@ describe('Store', () => {
};
const Wrapper = ({shouldSuspense}) => (
<React.Fragment>
<React.SuspenseList revealOrder="forwards" tail="collapsed">
<React.unstable_SuspenseList revealOrder="forwards" tail="collapsed">
<Component key="A" />
<React.Suspense fallback={<Loading />}>
{shouldSuspense ? <SuspendingComponent /> : <Component key="B" />}
</React.Suspense>
<Component key="C" />
</React.SuspenseList>
</React.unstable_SuspenseList>
</React.Fragment>
);

const container = document.createElement('div');
const root = ReactDOM.createRoot(container);
const root = ReactDOM.unstable_createRoot(container);
act(() => {
root.render(<Wrapper shouldSuspense={true} />);
});
Expand Down

0 comments on commit 9fe4075

Please sign in to comment.