diff --git a/SSRComponent.js b/SSRComponent.js index 0b047c1..dd33563 100644 --- a/SSRComponent.js +++ b/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, - }); + child.props.store = chstore; } - 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.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); + } +} diff --git a/virtualRender.js b/virtualRender.js index 30677bb..2e2eb1b 100644 --- a/virtualRender.js +++ b/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);