Skip to content

Commit

Permalink
Address context fragments issue (#4717)
Browse files Browse the repository at this point in the history
* Address context fragments issue

* Golf bytesize

* Add one more test
  • Loading branch information
JoviDeCroock authored Feb 27, 2025
1 parent ecf6c40 commit 58703d6
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 3 deletions.
16 changes: 14 additions & 2 deletions src/diff/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,10 @@ export function diff(

let isTopLevelFragment =
tmp != NULL && tmp.type === Fragment && tmp.key == NULL;
let renderResult = isTopLevelFragment ? tmp.props.children : tmp;
let renderResult = tmp;

if (isTopLevelFragment) {
tmp.props.children = NULL;
renderResult = cloneNode(tmp.props.children);
}

oldDom = diffChildren(
Expand Down Expand Up @@ -368,6 +368,18 @@ export function commitRoot(commitQueue, root, refQueue) {
});
}

function cloneNode(node) {
if (typeof node !== 'object' || node == NULL) {
return node;
}

if (isArray(node)) {
return node.map(cloneNode);
}

return assign({}, node);
}

/**
* Diff two virtual nodes representing DOM element
* @param {PreactElement} dom The DOM element representing the virtual nodes
Expand Down
161 changes: 160 additions & 1 deletion test/browser/fragments.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { setupRerender } from 'preact/test-utils';
import { createElement, render, Component, Fragment } from 'preact';
import {
createElement,
render,
Component,
Fragment,
createContext
} from 'preact';
import { setupScratch, teardown } from '../_util/helpers';
import { span, div, ul, ol, li, section } from '../_util/dom';
import { logCall, clearLog, getLog } from '../_util/logCall';
Expand Down Expand Up @@ -3150,4 +3156,157 @@ describe('Fragment', () => {
expect(scratch.innerHTML).to.equal([div('A'), div(1), div(2)].join(''));
expectDomLogToBe(['<div>B.remove()', '<div>2A1.appendChild(<div>2)']);
});

it('Should retain content when parent rerenders', () => {
let toggle;
const ctx = createContext(null);

class Provider extends Component {
constructor(props) {
super(props);
this.state = { value: props.value };
toggle = () => this.setState({ value: props.value + 1 });
}

render(props) {
return (
<ctx.Provider value={this.state.value}>{props.children}</ctx.Provider>
);
}
}

class App extends Component {
render() {
return (
<Provider value={1}>
<Fragment>
<p>Hello world</p>
</Fragment>
</Provider>
);
}
}

render(<App />, scratch);
expect(scratch.innerHTML).to.equal('<p>Hello world</p>');

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<p>Hello world</p>');
});

it('Should retain content when parent rerenders w/ multiple children', () => {
let toggle;
const ctx = createContext(null);

class Provider extends Component {
constructor(props) {
super(props);
this.state = { value: props.value };
toggle = () => this.setState({ value: props.value + 1 });
}

render(props) {
return (
<ctx.Provider value={this.state.value}>{props.children}</ctx.Provider>
);
}
}

class App extends Component {
render() {
return (
<Provider value={1}>
<Fragment>
<p>Hello</p>
<p>World</p>
</Fragment>
</Provider>
);
}
}

render(<App />, scratch);
expect(scratch.innerHTML).to.equal('<p>Hello</p><p>World</p>');

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<p>Hello</p><p>World</p>');
});

it('Should retain content when parent rerenders w/ text children', () => {
let toggle;
const ctx = createContext(null);

class Provider extends Component {
constructor(props) {
super(props);
this.state = { value: props.value };
toggle = () => this.setState({ value: props.value + 1 });
}

render(props) {
return (
<ctx.Provider value={this.state.value}>{props.children}</ctx.Provider>
);
}
}

class App extends Component {
render() {
return (
<Provider value={1}>
<Fragment>Hello world</Fragment>
</Provider>
);
}
}

render(<App />, scratch);
expect(scratch.innerHTML).to.equal('Hello world');

toggle();
rerender();
expect(scratch.innerHTML).to.equal('Hello world');
});

it('Should retain content when parent rerenders when deeper children are subscribed', () => {
let toggle;
const ctx = createContext(null);

class Provider extends Component {
constructor(props) {
super(props);
this.state = { value: props.value };
toggle = () => this.setState({ value: props.value + 1 });
}

render(props) {
return (
<ctx.Provider value={this.state.value}>{props.children}</ctx.Provider>
);
}
}

class App extends Component {
render() {
return (
<Provider value={1}>
<Fragment>
<ctx.Consumer>
{value => <p>I can count to {value}</p>}
</ctx.Consumer>
</Fragment>
</Provider>
);
}
}

render(<App />, scratch);
expect(scratch.innerHTML).to.equal('<p>I can count to 1</p>');

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<p>I can count to 2</p>');
});
});

0 comments on commit 58703d6

Please sign in to comment.