Faster and more correct variant which does not recreate full DOM tree on every run

On the other hand, it has to disable Object.freeze in development builds
because React freezes props in development builds and we want to modify them
directly
master
Vitaliy Filippov 2021-08-08 12:22:48 +03:00
parent d615990529
commit 1c59723cd1
2 changed files with 131 additions and 46 deletions

View File

@ -1,10 +1,8 @@
// Component capable of saving & restoring state during render // Component capable of saving & restoring state during render
// (c) Vitaliy Filippov 2019+ // (c) Vitaliy Filippov 2019+
// Version: 2021-08-07 // Version: 2021-08-08
// License: Dual-license MPL 1.1+ or GNU LGPL 3.0+ // License: Dual-license MPL 1.1+ or GNU LGPL 3.0+
// NOTE: Child components of the same class should have unique `key`s
import React from 'react'; import React from 'react';
export default class SSRComponent extends React.PureComponent export default class SSRComponent extends React.PureComponent
@ -39,13 +37,9 @@ export default class SSRComponent extends React.PureComponent
if (store.children) if (store.children)
{ {
store.children = { ...store.children }; store.children = { ...store.children };
for (const className in store.children) for (const key in store.children)
{ {
store.children[className] = { ...store.children[className] }; store.children[key] = SSRComponent.serializeStore(store.children[key]);
for (const key in store.children[className])
{
store.children[className][key] = SSRComponent.serializeStore(store.children[className][key]);
}
} }
} }
return store; return store;
@ -63,36 +57,15 @@ export default class SSRComponent extends React.PureComponent
passStore(children) passStore(children)
{ {
return React.Children.map(children, (child) => walkTree(children, (child, key) =>
{ {
if (child && (child.type instanceof Object) && (child.type.prototype instanceof SSRComponent)) if (child && (child.type instanceof Object) && (child.type.prototype instanceof SSRComponent))
{ {
const className = child.type.name; this.props.store.idx[key] = true;
this.props.store.idx[className] ||= { num: 0, keys: {} };
let key;
if (child.props.key)
key = ':'+child.props.key;
else
{
this.props.store.idx[className].num++;
key = '['+this.props.store.idx[className].num;
}
this.props.store.idx[className].keys[key] = true;
let chstore = (this.props.store.children ||= {}); let chstore = (this.props.store.children ||= {});
chstore = (chstore[className] ||= {});
chstore = (chstore[key] ||= {}); chstore = (chstore[key] ||= {});
return React.cloneElement(child, { child.props.store = chstore;
store: chstore,
children: child.props && child.props.children ? this.passStore(child.props.children) : undefined,
});
} }
else if (child && child.props && child.props.children)
{
return React.cloneElement(child, {
children: this.passStore(child.props.children),
});
}
return child;
}); });
} }
@ -103,19 +76,28 @@ export default class SSRComponent extends React.PureComponent
this.props.store.instance = this; this.props.store.instance = this;
this.props.store.idx = {}; this.props.store.idx = {};
} }
let children = this.doRender(); let children;
if (process.env.NODE_ENV === 'production')
{
children = this.doRender();
}
else
{
// Monkey-patch Object.freeze so fucking React can't freeze props
const freeze = Object.freeze;
Object.freeze = function() {};
children = this.doRender();
Object.freeze = freeze;
}
if (this.props.store) if (this.props.store)
{ {
children = this.passStore(children); this.passStore(children);
// Clear unused keys // Clear unused keys
for (let className in this.props.store.idx) for (let key in this.props.store.children)
{ {
for (let key in this.props.store.children[className]) if (!this.props.store.idx[key])
{ {
if (!this.props.store.idx[className].keys[key]) delete this.props.store.children[key];
{
delete this.props.store.children[className][key];
}
} }
} }
delete this.props.store.idx; delete this.props.store.idx;
@ -127,3 +109,62 @@ export default class SSRComponent extends React.PureComponent
{ {
} }
} }
// Preact puts children directly on element, and React via props
const getChildren = element =>
element.props && element.props.children ? element.props.children : element.children ? element.children : undefined;
// Preact uses "nodeName", React uses "type"
const getType = element => element.type || element.nodeName;
const isReactElement = element => !!getType(element);
// Recurse a React Element tree, running the provided visitor against each element.
// Similar to React.Children.map(), but tracks keys.
function walkTree(tree, visitor, path = '')
{
if (!tree)
{
return;
}
if (tree instanceof Array)
{
// Process array, remembering keys and indices within series of elements of the same type
let index = 0, series = 0;
let lastType = null, lastHasKey = false;
for (let item of tree)
{
if (item)
{
let type = getType(item);
let key = item.key;
if (lastType == type && lastHasKey == (key != null))
index++;
else
{
series++;
index = 0;
lastType = type;
lastHasKey = (key != null);
}
if (key == null)
key = ':'+series+':'+index;
else
key = '['+key+']';
let typeName;
if (type && type.name)
typeName = type.name;
else if (typeof type == 'symbol')
typeName = type.toString();
else
typeName = type;
walkTree(item, visitor, path+'/'+typeName+key);
}
}
}
if (isReactElement(tree))
{
visitor(tree, path);
walkTree(getChildren(tree), visitor, path);
}
}

View File

@ -84,17 +84,25 @@ function walkTree(tree, visitor, context, options, path = '')
{ {
// Process array, remembering keys and indices within series of elements of the same type // Process array, remembering keys and indices within series of elements of the same type
let res = []; let res = [];
let index = 0; let index = 0, series = 0;
let lastType = null; let lastType = null, lastHasKey = false;
for (let item of tree) for (let item of tree)
{ {
let type = item && getType(item); let type = item && getType(item);
if (lastType == type) let key = item && getProps(item)?.key;
if (lastType == type && lastHasKey == (key != null))
index++; index++;
else else
{
series++;
index = 0; index = 0;
lastType = type; lastType = type;
let key = item && getProps(item)?.key || index; lastHasKey = (key != null);
}
if (key == null)
key = ':'+series+':'+index;
else
key = '['+key+']';
let typeName; let typeName;
if (type && type.name) if (type && type.name)
typeName = type.name; typeName = type.name;
@ -106,6 +114,42 @@ function walkTree(tree, visitor, context, options, path = '')
} }
return res; return res;
} }
if (tree instanceof Array)
{
// Process array, remembering keys and indices within series of elements of the same type
let index = 0, series = 0;
let lastType = null, lastHasKey = false;
for (let item of tree)
{
if (item)
{
let type = getType(item);
let key = item.key;
if (lastType == type && lastHasKey == (key != null))
index++;
else
{
series++;
index = 0;
lastType = type;
lastHasKey = (key != null);
}
if (key == null)
key = ':'+series+':'+index;
else
key = '['+key+']';
let typeName;
if (type && type.name)
typeName = type.name;
else if (typeof type == 'symbol')
typeName = type.toString();
else
typeName = type;
res.push(walkTree(item, visitor, context, options, path+'/'+typeName+'['+key+']'));
}
}
return res;
}
if (tree.type) if (tree.type)
{ {
const _context = tree.type._context || (tree.type.Provider && tree.type.Provider._context); const _context = tree.type._context || (tree.type.Provider && tree.type.Provider._context);