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 directlymaster
parent
d615990529
commit
1c59723cd1
123
SSRComponent.js
123
SSRComponent.js
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue