// Component capable of saving & restoring state during render // (c) Vitaliy Filippov 2019+ // Version: 2021-08-08 // License: Dual-license MPL 1.1+ or GNU LGPL 3.0+ import React from 'react'; export default class SSRComponent extends React.PureComponent { constructor(props) { super(props); if (props.store && props.store.state) { this.unserializeState(props.store.state); delete props.store.state; } else { this.state = {}; this.init(props); } } init(props) { } static serializeStore(store) { store = { ...store }; if (store.instance) { store.state = store.instance.serializeState(); delete store.instance; } if (store.children) { store.children = { ...store.children }; for (const key in store.children) { store.children[key] = SSRComponent.serializeStore(store.children[key]); } } return store; } serializeState() { return { ...this.state }; } unserializeState(state) { this.state = state; } passStore(children) { walkTree(children, (child, key) => { if (child && (child.type instanceof Object) && (child.type.prototype instanceof SSRComponent)) { this.props.store.idx[key] = true; let chstore = (this.props.store.children ||= {}); chstore = (chstore[key] ||= {}); child.props.store = chstore; } }); } render() { if (this.props.store) { this.props.store.instance = this; this.props.store.idx = {}; } 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) { this.passStore(children); // Clear unused keys for (let key in this.props.store.children) { if (!this.props.store.idx[key]) { delete this.props.store.children[key]; } } delete this.props.store.idx; } return children; } doRender() { } } // 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); } }