Skip to content

Commit

Permalink
fix: nested render error in hydrating (#2039)
Browse files Browse the repository at this point in the history
* fix: nested render error in hydrating

* v2.1.4

* fix: lint

* fix: test case title

* fix: throw error only in hydrate change

* fix: spelling

* v1.1.0

* fix: spelling

* v2.2.0
  • Loading branch information
chenjun1011 authored Dec 24, 2020
1 parent 448ed94 commit ab8db5b
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 31 deletions.
2 changes: 1 addition & 1 deletion packages/driver-dom/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "driver-dom",
"version": "2.1.3",
"version": "2.2.0",
"description": "DOM driver for Rax",
"license": "BSD-3-Clause",
"main": "lib/index.js",
Expand Down
61 changes: 60 additions & 1 deletion packages/driver-dom/src/__tests__/hydrate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createElement, render } from 'rax';
import { createElement, render, useEffect } from 'rax';

describe('Hydrate', () => {
let DriverDOM;
Expand Down Expand Up @@ -33,6 +33,65 @@ describe('Hydrate', () => {
expect(container.childNodes[0].childNodes[2].data).toBe('Rax');
});

it('should not be affected by render in useEffect', () => {
const childContainer = document.createElement('div');
(document.body || document.documentElement).appendChild(childContainer);

const Child = () => {
useEffect(() => {
render(<span>child content</span>, childContainer);
});

return null;
};

const Component = () => {
return (
<div class="container">
<div>About Rax</div>
<Child />
</div>
);
};

jest.useFakeTimers();

render(<Component />, container, { driver: DriverDOM, hydrate: true });

jest.runAllTimers();

expect(container.childNodes[0].childNodes[1].nodeType).toBe(8); // comment;
});

it('should throw error for nested render when hydrating', () => {
const childContainer = document.createElement('div');
(document.body || document.documentElement).appendChild(childContainer);

const Child = () => {
render(<span>child content</span>, childContainer);
return null;
};

const Component = () => {
return (
<div class="container">
<div>About Rax</div>
<Child />
</div>
);
};

expect(() => {
jest.useFakeTimers();

render(<Component />, container, { driver: DriverDOM, hydrate: true });

jest.runAllTimers();
}).toThrowError('Nested render is not allowed when hydrating. If necessary, trigger render in useEffect.', {
withoutStack: true,
});
});

it('should warn for replaced hydratable element', () => {
const Component = () => {
return (
Expand Down
12 changes: 12 additions & 0 deletions packages/driver-dom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,18 @@ export function setStyle(node, style, __shouldConvertUnitlessToRpx, __shouldConv
}

export function beforeRender({ hydrate }) {
// Nested render may reset `isHydrating`, `recolectHydrationChild` will not work correctly after render.
if (isHydrating && !hydrate) {
if (__DEV__) {
throw new Error(
'Nested render is not allowed when hydrating. ' +
'If necessary, trigger render in useEffect.'
);
} else {
throw new Error('Nested render found.');
}
}

isHydrating = hydrate;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/rax-create-portal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rax-create-portal",
"version": "1.0.0",
"version": "1.1.0",
"description": "Rax createPortal",
"license": "BSD-3-Clause",
"main": "lib/index.js",
Expand All @@ -24,6 +24,7 @@
"devDependencies": {
"rax": "^1.0.0",
"driver-server": "^1.0.0",
"driver-dom": "^2.1.3",
"rax-proptypes": "^1.0.0"
}
}
28 changes: 28 additions & 0 deletions packages/rax-create-portal/src/__tests__/createPortal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import PropTypes from 'rax-proptypes';
import { Component, createElement, render, shared, createContext, useContext, useState } from 'rax';
import ServerDriver from 'driver-server';
import * as DriverDOM from 'driver-dom';
import unmountComponentAtNode from 'rax-unmount-component-at-node';
import createPortal from '../';

Expand Down Expand Up @@ -237,4 +238,31 @@ describe('createPortal', () => {
jest.runAllTimers();
expect(portalTarget.childNodes[0].childNodes[0].data).toBe('2');
});

it('should not affect the result of hydrate', () => {
const container = document.createElement('div');
container.innerHTML = '<div class="container"><div>About Rax</div><div>Docs</div></div>';
(document.body || document.documentElement).appendChild(container);

const portalContainer = document.createElement('div');
(document.body || document.documentElement).appendChild(portalContainer);

const App = () => {
return (
<div class="container">
<div>About Rax</div>
{createPortal(<div>portal</div>, portalContainer)}
</div>
);
};

jest.useFakeTimers();

render(<App />, container, { driver: DriverDOM, hydrate: true });

jest.runAllTimers();

expect(container.childNodes[0].childNodes[1].nodeType).toBe(8); // comment;
expect(portalContainer.childNodes[0].childNodes[0].data).toBe('portal');
});
});
41 changes: 13 additions & 28 deletions packages/rax-create-portal/src/index.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
import { Component, render, createElement } from 'rax';
import { render, createElement, useEffect } from 'rax';
import unmountComponentAtNode from 'rax-unmount-component-at-node';

class Portal extends Component {
constructor(props, context) {
super(props, context);
}

componentDidMount() {
this.renderPortal();
}

componentDidUpdate(prevProps) {
if (prevProps.container !== this.props.container) {
unmountComponentAtNode(prevProps.container);
}

this.renderPortal();
}

componentWillUnmount() {
unmountComponentAtNode(this.props.container);
}

renderPortal() {
render(this.props.element, this.props.container, {
function Portal(props) {
useEffect(() => {
// Nested render will cause error when hydrating, it should be trigger in useEffect.
render(props.element, props.container, {
parent: this
});
}
});

useEffect(() => {
return () => {
unmountComponentAtNode(props.container);
};
}, [props.container]);

render() {
return null;
}
return null;
}

export default function createPortal(element, container) {
Expand Down

0 comments on commit ab8db5b

Please sign in to comment.