Browse Source

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 1 year ago
parent
commit
1c59723cd1
  1. 123
      SSRComponent.js
  2. 54
      virtualRender.js

123
SSRComponent.js

@ -1,10 +1,8 @@
// Component capable of saving & restoring state during render
// (c) Vitaliy Filippov 2019+
// Version: 2021-08-07
// Version: 2021-08-08
// 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';
export default class SSRComponent extends React.PureComponent
@ -39,13 +37,9 @@ export default class SSRComponent extends React.PureComponent
if (store.children)
{
store.children = { ...store.children };
for (const className in store.children)
for (const key in store.children)
{
store.children[className] = { ...store.children[className] };
for (const key in store.children[className])
{
store.children[className][key] = SSRComponent.serializeStore(store.children[className][key]);
}
store.children[key] = SSRComponent.serializeStore(store.children[key]);
}
}
return store;
@ -63,36 +57,15 @@ export default class SSRComponent extends React.PureComponent
passStore(children)
{
return React.Children.map(children, (child) =>
walkTree(children, (child, key) =>
{
if (child && (child.type instanceof Object) && (child.type.prototype instanceof SSRComponent))
{
const className = child.type.name;
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;
this.props.store.idx[key] = true;
let chstore = (this.props.store.children ||= {});
chstore = (chstore[className] ||= {});
chstore = (chstore[key] ||= {});
return React.cloneElement(child, {
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),
});
child.props.store = chstore;
}
return child;
});
}
@ -103,19 +76,28 @@ export default class SSRComponent extends React.PureComponent
this.props.store.instance = this;
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)
{
children = this.passStore(children);
this.passStore(children);
// 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[className][key];
}
delete this.props.store.children[key];
}
}
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);
}
}

54
virtualRender.js

@ -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
let res = [];
let index = 0;
let lastType = null;
let index = 0, series = 0;
let lastType = null, lastHasKey = false;
for (let item of tree)
{
let type = item && getType(item);
if (lastType == type)
let key = item && getProps(item)?.key;
if (lastType == type && lastHasKey == (key != null))
index++;
else
{
series++;
index = 0;
lastType = type;
let key = item && getProps(item)?.key || index;
lastType = type;
lastHasKey = (key != null);
}
if (key == null)
key = ':'+series+':'+index;
else
key = '['+key+']';
let typeName;
if (type && type.name)
typeName = type.name;
@ -106,6 +114,42 @@ function walkTree(tree, visitor, context, options, path = '')
}
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)
{
const _context = tree.type._context || (tree.type.Provider && tree.type.Provider._context);

Loading…
Cancel
Save