Skip to content

fix(render): Actually hydrate with given ui #988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ module.exports = Object.assign(jestConfig, {
// full coverage across the build matrix (React 17, 18) but not in a single job
'./src/pure': {
// minimum coverage of jobs using React 17 and 18
branches: 80,
branches: 75,
functions: 78,
lines: 79,
statements: 79,
lines: 76,
statements: 76,
},
},
})
36 changes: 35 additions & 1 deletion src/__tests__/render.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import * as React from 'react'
import ReactDOM from 'react-dom'
import {render, screen} from '../'
import ReactDOMServer from 'react-dom/server'
import {fireEvent, render, screen} from '../'

afterEach(() => {
if (console.error.mockRestore !== undefined) {
console.error.mockRestore()
}
})

test('renders div into document', () => {
const ref = React.createRef()
Expand Down Expand Up @@ -134,3 +141,30 @@ test('can be called multiple times on the same container', () => {

expect(container).toBeEmptyDOMElement()
})

test('hydrate will make the UI interactive', () => {
jest.spyOn(console, 'error').mockImplementation(() => {})
function App() {
const [clicked, handleClick] = React.useReducer(n => n + 1, 0)

return (
<button type="button" onClick={handleClick}>
clicked:{clicked}
</button>
)
}
const ui = <App />
const container = document.createElement('div')
document.body.appendChild(container)
container.innerHTML = ReactDOMServer.renderToString(ui)

expect(container).toHaveTextContent('clicked:0')

render(ui, {container, hydrate: true})

expect(console.error).not.toHaveBeenCalled()

fireEvent.click(container.querySelector('button'))

expect(container).toHaveTextContent('clicked:1')
})
27 changes: 19 additions & 8 deletions src/pure.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,25 +60,36 @@ const mountedContainers = new Set()
*/
const mountedRootEntries = []

function createConcurrentRoot(container, options) {
function createConcurrentRoot(
container,
{hydrate, ui, wrapper: WrapperComponent},
) {
if (typeof ReactDOM.createRoot !== 'function') {
throw new TypeError(
`Attempted to use concurrent React with \`react-dom@${ReactDOM.version}\`. Be sure to use the \`next\` or \`experimental\` release channel (https://reactjs.org/docs/release-channels.html).'`,
)
}
const root = options.hydrate
? ReactDOM.hydrateRoot(container)
: ReactDOM.createRoot(container)
let root
if (hydrate) {
act(() => {
root = ReactDOM.hydrateRoot(
container,
WrapperComponent ? React.createElement(WrapperComponent, null, ui) : ui,
)
})
} else {
root = ReactDOM.createRoot(container)
}

return {
hydrate(element) {
hydrate() {
/* istanbul ignore if */
if (!options.hydrate) {
if (!hydrate) {
throw new Error(
'Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.',
)
}
root.render(element)
// Nothing to do since hydration happens when creating the root object.
},
render(element) {
root.render(element)
Expand Down Expand Up @@ -183,7 +194,7 @@ function render(
// eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first.
if (!mountedContainers.has(container)) {
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
root = createRootImpl(container, {hydrate})
root = createRootImpl(container, {hydrate, ui, wrapper})

mountedRootEntries.push({container, root})
// we'll add it to the mounted containers regardless of whether it's actually
Expand Down