it('supports hydration', async () => { const markup = await new Promise(resolve => resolve( ReactDOMServer.renderToString( <div> <span className="extra" /> </div>, ), ), ); // Does not hydrate by default const container1 = document.createElement('div'); container1.innerHTML = markup; const root1 = ReactDOM.unstable_createRoot(container1); root1.render( <div> <span /> </div>, ); Scheduler.flushAll(); // Accepts `hydrate` option const container2 = document.createElement('div'); container2.innerHTML = markup; const root2 = ReactDOM.unstable_createRoot(container2, {hydrate: true}); root2.render( <div> <span /> </div>, ); expect(() => Scheduler.flushAll()).toWarnDev('Extra attributes', { withoutStack: true, }); });
it('warns when unmounting with legacy API (no previous content)', () => { const root = ReactDOM.unstable_createRoot(container); root.render(<div>Hi</div>); Scheduler.flushAll(); expect(container.textContent).toEqual('Hi'); let unmounted = false; expect(() => { unmounted = ReactDOM.unmountComponentAtNode(container); }).toWarnDev( [ // We care about this warning: 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' + 'passed to ReactDOM.unstable_createRoot(). This is not supported. Did you mean to call root.unmount()?', // This is more of a symptom but restructuring the code to avoid it isn't worth it: "The node you're attempting to unmount was rendered by React and is not a top-level container.", ], {withoutStack: true}, ); expect(unmounted).toBe(false); Scheduler.flushAll(); expect(container.textContent).toEqual('Hi'); root.unmount(); Scheduler.flushAll(); expect(container.textContent).toEqual(''); });
it('applies setState in componentDidMount synchronously in a batch', done => { class App extends React.Component { state = {mounted: false}; componentDidMount() { this.setState({ mounted: true, }); } render() { return this.state.mounted ? 'Hi' : 'Bye'; } } const root = ReactDOM.unstable_createRoot(container); const batch = root.createBatch(); batch.render(<App />); Scheduler.flushAll(); // Hasn't updated yet expect(container.textContent).toEqual(''); let ops = []; batch.then(() => { // Still hasn't updated ops.push(container.textContent); // Should synchronously commit batch.commit(); ops.push(container.textContent); expect(ops).toEqual(['', 'Hi']); done(); }); });
it('works with createRoot().render combo', () => { const root = ReactDOM.unstable_createRoot(document.createElement('div')); TestRenderer.act(() => { root.render(<App />); }); confirmWarning(); });
it('unmounts children', () => { const root = ReactDOM.unstable_createRoot(container); root.render(<div>Hi</div>); Scheduler.flushAll(); expect(container.textContent).toEqual('Hi'); root.unmount(); Scheduler.flushAll(); expect(container.textContent).toEqual(''); });
it('unmounts children', () => { const root = ReactDOM.unstable_createRoot(container); root.render(<div>Hi</div>); jest.runAllTimers(); expect(container.textContent).toEqual('Hi'); root.unmount(); jest.runAllTimers(); expect(container.textContent).toEqual(''); });
it('can defer a commit by batching it', () => { const root = ReactDOM.unstable_createRoot(container); const batch = root.createBatch(); batch.render(<div>Hi</div>); // Hasn't committed yet expect(container.textContent).toEqual(''); // Commit batch.commit(); expect(container.textContent).toEqual('Hi'); });
it('handles fatal errors triggered by batch.commit()', () => { const root = ReactDOM.unstable_createRoot(container); const batch = root.createBatch(); const InvalidType = undefined; expect(() => batch.render(<InvalidType />)).toWarnDev( ['React.createElement: type is invalid'], {withoutStack: true}, ); expect(() => batch.commit()).toThrow('Element type is invalid'); });
it('resolves `work.then` callback synchronously if the work already committed', () => { const root = ReactDOM.unstable_createRoot(container); const work = root.render(<AsyncMode>Hi</AsyncMode>); jest.runAllTimers(); let ops = []; work.then(() => { ops.push('inside callback'); }); expect(ops).toEqual(['inside callback']); });
it('resolves `work.then` callback synchronously if the work already committed', () => { const root = ReactDOM.unstable_createRoot(container); const work = root.render('Hi'); Scheduler.flushAll(); let ops = []; work.then(() => { ops.push('inside callback'); }); expect(ops).toEqual(['inside callback']); });
it('is async for non-input events', () => { const root = ReactDOM.unstable_createRoot(container); let input; let ops = []; class ControlledInput extends React.Component { state = {value: 'initial'}; onChange = event => this.setState({value: event.target.value}); reset = () => { this.setState({value: ''}); }; render() { ops.push(`render: ${this.state.value}`); const controlledValue = this.state.value === 'changed' ? 'changed [!]' : this.state.value; return ( <input ref={el => (input = el)} type="text" value={controlledValue} onChange={this.onChange} onClick={this.reset} /> ); } } // Initial mount. Test that this is async. root.render(<ControlledInput />); // Should not have flushed yet. expect(ops).toEqual([]); expect(input).toBe(undefined); // Flush callbacks. jest.runAllTimers(); expect(ops).toEqual(['render: initial']); expect(input.value).toBe('initial'); ops = []; // Trigger a click event input.dispatchEvent( new Event('click', {bubbles: true, cancelable: true}), ); // Nothing should have changed expect(ops).toEqual([]); expect(input.value).toBe('initial'); // Flush callbacks. jest.runAllTimers(); // Now the click update has flushed. expect(ops).toEqual(['render: ']); expect(input.value).toBe(''); });
it('parent of input', () => { const root = ReactDOM.unstable_createRoot(container); let input; let ops = []; class ControlledInput extends React.Component { state = {value: 'initial'}; onChange = event => this.setState({value: event.target.value}); render() { ops.push(`render: ${this.state.value}`); const controlledValue = this.state.value === 'changed' ? 'changed [!]' : this.state.value; return ( <div onChange={this.onChange}> <input ref={el => (input = el)} type="text" value={controlledValue} onChange={() => { // Does nothing. Parent handler is reponsible for updating. }} /> </div> ); } } // Initial mount. Test that this is async. root.render(<ControlledInput />); // Should not have flushed yet. expect(ops).toEqual([]); expect(input).toBe(undefined); // Flush callbacks. jest.runAllTimers(); expect(ops).toEqual(['render: initial']); expect(input.value).toBe('initial'); ops = []; // Trigger a change event. setUntrackedValue.call(input, 'changed'); input.dispatchEvent( new Event('input', {bubbles: true, cancelable: true}), ); // Change should synchronously flush expect(ops).toEqual(['render: changed']); // Value should be the controlled value, not the original one expect(input.value).toBe('changed [!]'); });
it('can commit an empty batch', () => { const root = ReactDOM.unstable_createRoot(container); root.render(<AsyncMode>1</AsyncMode>); advanceCurrentTime(2000); // This batch has a later expiration time than the earlier update. const batch = root.createBatch(); // This should not flush the earlier update. batch.commit(); expect(container.textContent).toEqual(''); jest.runAllTimers(); expect(container.textContent).toEqual('1'); });
it('can commit an empty batch', () => { const root = ReactDOM.unstable_createRoot(container); root.render(1); Scheduler.advanceTime(2000); // This batch has a later expiration time than the earlier update. const batch = root.createBatch(); // This should not flush the earlier update. batch.commit(); expect(container.textContent).toEqual(''); Scheduler.flushAll(); expect(container.textContent).toEqual('1'); });
it('two batches created simultaneously are committed separately', () => { // (In other words, they have distinct expiration times) const root = ReactDOM.unstable_createRoot(container); const batch1 = root.createBatch(); batch1.render(1); const batch2 = root.createBatch(); batch2.render(2); expect(container.textContent).toEqual(''); batch1.commit(); expect(container.textContent).toEqual('1'); batch2.commit(); expect(container.textContent).toEqual('2'); });
it('`root.render` returns a thenable work object', () => { const root = ReactDOM.unstable_createRoot(container); const work = root.render('Hi'); let ops = []; work.then(() => { ops.push('inside callback: ' + container.textContent); }); ops.push('before committing: ' + container.textContent); Scheduler.flushAll(); ops.push('after committing: ' + container.textContent); expect(ops).toEqual([ 'before committing: ', // `then` callback should fire during commit phase 'inside callback: Hi', 'after committing: Hi', ]); });
it('commits an earlier batch without committing a later batch', () => { const root = ReactDOM.unstable_createRoot(container); const batch1 = root.createBatch(); batch1.render(1); // This batch has a later expiration time Scheduler.advanceTime(2000); const batch2 = root.createBatch(); batch2.render(2); expect(container.textContent).toEqual(''); batch1.commit(); expect(container.textContent).toEqual('1'); batch2.commit(); expect(container.textContent).toEqual('2'); });
it('warns when unmounting with legacy API (has previous content)', () => { // Currently createRoot().render() doesn't clear this. container.appendChild(document.createElement('div')); // The rest is the same as test above. const root = ReactDOM.unstable_createRoot(container); root.render(<div>Hi</div>); Scheduler.flushAll(); expect(container.textContent).toEqual('Hi'); let unmounted = false; expect(() => { unmounted = ReactDOM.unmountComponentAtNode(container); }).toWarnDev('Did you mean to call root.unmount()?', {withoutStack: true}); expect(unmounted).toBe(false); Scheduler.flushAll(); expect(container.textContent).toEqual('Hi'); root.unmount(); Scheduler.flushAll(); expect(container.textContent).toEqual(''); });
it('warns when hydrating with legacy API into createRoot() container', () => { const root = ReactDOM.unstable_createRoot(container); root.render(<div>Hi</div>); Scheduler.flushAll(); expect(container.textContent).toEqual('Hi'); expect(() => { ReactDOM.hydrate(<div>Hi</div>, container); }).toWarnDev( [ // We care about this warning: 'You are calling ReactDOM.hydrate() on a container that was previously ' + 'passed to ReactDOM.unstable_createRoot(). This is not supported. ' + 'Did you mean to call createRoot(container, {hydrate: true}).render(element)?', // This is more of a symptom but restructuring the code to avoid it isn't worth it: 'Replacing React-rendered children with a new root component.', ], {withoutStack: true}, ); });
it('does not clear existing children', async () => { container.innerHTML = '<div>a</div><div>b</div>'; const root = ReactDOM.unstable_createRoot(container); root.render( <div> <span>c</span> <span>d</span> </div>, ); Scheduler.flushAll(); expect(container.textContent).toEqual('abcd'); root.render( <div> <span>d</span> <span>c</span> </div>, ); Scheduler.flushAll(); expect(container.textContent).toEqual('abdc'); });
it('can wait for a batch to finish', () => { const root = ReactDOM.unstable_createRoot(container); const batch = root.createBatch(); batch.render('Foo'); Scheduler.flushAll(); // Hasn't updated yet expect(container.textContent).toEqual(''); let ops = []; batch.then(() => { // Still hasn't updated ops.push(container.textContent); // Should synchronously commit batch.commit(); ops.push(container.textContent); }); expect(ops).toEqual(['', 'Foo']); });
it('does not restart a completed batch when committing if there were no intervening updates', () => { let ops = []; function Foo(props) { ops.push('Foo'); return props.children; } const root = ReactDOM.unstable_createRoot(container); const batch = root.createBatch(); batch.render(<Foo>Hi</Foo>); // Flush all async work. Scheduler.flushAll(); // Root should complete without committing. expect(ops).toEqual(['Foo']); expect(container.textContent).toEqual(''); ops = []; // Commit. Shouldn't re-render Foo. batch.commit(); expect(ops).toEqual([]); expect(container.textContent).toEqual('Hi'); });
import React, { Placeholder } from "react"; import "./lib/index.css"; import App from "./App.start"; import { unstable_createRoot } from "react-dom"; unstable_createRoot(document.getElementById("root")).render( <Placeholder delayMs={1000} fallback={<div>Loading...</div>}> <App /> </Placeholder> );
expect(() => { ReactDOM.unstable_createRoot(<div>Hi</div>); }).toThrow(
width: progress + '%', position: 'absolute', left: 0, top: 0, backgroundColor: progress !== 100 ? '#61dafb' : 'lightgreen', zIndex: -1, opacity: 0.8, }} /> <div style={{ fontFamily: 'monospace', fontWeight: 'bold', color: 'black', }}> {url} </div> </div> ); })} </div> </Draggable> ); } } unstable_createRoot(document.getElementById('root')).render(<Shell />); render(<Debugger />, document.getElementById('debugger'));
it('checkbox input', () => { const root = ReactDOM.unstable_createRoot(container); let input; let ops = []; class ControlledInput extends React.Component { state = {checked: false}; onChange = event => { this.setState({checked: event.target.checked}); }; render() { ops.push(`render: ${this.state.checked}`); const controlledValue = this.props.reverse ? !this.state.checked : this.state.checked; return ( <input ref={el => (input = el)} type="checkbox" checked={controlledValue} onChange={this.onChange} /> ); } } // Initial mount. Test that this is async. root.render(<ControlledInput reverse={false} />); // Should not have flushed yet. expect(ops).toEqual([]); expect(input).toBe(undefined); // Flush callbacks. jest.runAllTimers(); expect(ops).toEqual(['render: false']); expect(input.checked).toBe(false); ops = []; // Trigger a change event. input.dispatchEvent( new MouseEvent('click', {bubbles: true, cancelable: true}), ); // Change should synchronously flush expect(ops).toEqual(['render: true']); expect(input.checked).toBe(true); // Now let's make sure we're using the controlled value. root.render(<ControlledInput reverse={true} />); jest.runAllTimers(); ops = []; // Trigger another change event. input.dispatchEvent( new MouseEvent('click', {bubbles: true, cancelable: true}), ); // Change should synchronously flush expect(ops).toEqual(['render: true']); expect(input.checked).toBe(false); });
expect(() => { ReactDOM.unstable_createRoot(container); }).toWarnDev(