function renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
	if (vnode==null || typeof vnode==='boolean') {
		return '';
	}

	let nodeName = vnode.type,
		props = vnode.props,
		isComponent = false;
	context = context || {};
	opts = opts || {};

	let pretty = ENABLE_PRETTY && opts.pretty,
		indentChar = pretty && typeof pretty==='string' ? pretty : '\t';

	// #text nodes
	if (typeof vnode!=='object' && !nodeName) {
		return encodeEntities(vnode);
	}

	// components
	if (typeof nodeName==='function') {
		isComponent = true;
		if (opts.shallow && (inner || opts.renderRootComponent===false)) {
			nodeName = getComponentName(nodeName);
		}
		else if (nodeName===Fragment) {
			let rendered = '';
			let children = [];
			getChildren(children, vnode.props.children);

			for (let i = 0; i < children.length; i++) {
				rendered += renderToString(children[i], context, opts, opts.shallowHighOrder!==false, isSvgMode, selectValue);
			}
			return rendered;
		}
		else {
			let rendered;
			
			let c = vnode.__c = { __v: vnode, context, props: vnode.props };
			if (options.render) options.render(vnode);

			if (!nodeName.prototype || typeof nodeName.prototype.render!=='function') {
				// Necessary for createContext api. Setting this property will pass
				// the context value as `this.context` just for this component.
				let cxType = nodeName.contextType;
				let provider = cxType && context[cxType.__c];
				let cctx = cxType != null ? (provider ? provider.props.value : cxType._defaultValue) : context;

				// stateless functional components
				rendered = nodeName.call(vnode.__c, props, cctx);
			}
			else {
				// class-based components
				// c = new nodeName(props, context);
				c = vnode.__c = new nodeName(props, context);
				c.__v = vnode;
				// turn off stateful re-rendering:
				c._dirty = c.__d = true;
				c.props = props;
				c.context = context;
				if (nodeName.getDerivedStateFromProps) c.state = assign(assign({}, c.state), nodeName.getDerivedStateFromProps(c.props, c.state));
				else if (c.componentWillMount) c.componentWillMount();
				rendered = c.render(c.props, c.state, c.context);
			}
			
			if (c.getChildContext) {
				context = assign(assign({}, context), c.getChildContext());
			}

			return renderToString(rendered, context, opts, opts.shallowHighOrder!==false, isSvgMode, selectValue);
		}
	}

	// render JSX to HTML
	let s = '', html;

	if (props) {
		let attrs = Object.keys(props);

		// allow sorting lexicographically for more determinism (useful for tests, such as via preact-jsx-chai)
		if (opts && opts.sortAttributes===true) attrs.sort();

		for (let i=0; i<attrs.length; i++) {
			let name = attrs[i],
				v = props[name];
			if (name==='children') continue;

			if (name.match(/[\s\n\\/='"\0<>]/)) continue;

			if (!(opts && opts.allAttributes) && (name==='key' || name==='ref')) continue;

			if (name==='className') {
				if (props.class) continue;
				name = 'class';
			}
			else if (isSvgMode && name.match(/^xlink:?./)) {
				name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
			}

			if (name==='style' && v && typeof v==='object') {
				v = styleObjToCss(v);
			}

			let hooked = opts.attributeHook && opts.attributeHook(name, v, context, opts, isComponent);
			if (hooked || hooked==='') {
				s += hooked;
				continue;
			}

			if (name==='dangerouslySetInnerHTML') {
				html = v && v.__html;
			}
			else if ((v || v===0 || v==='') && typeof v!=='function') {
				if (v===true || v==='') {
					v = name;
					// in non-xml mode, allow boolean attributes
					if (!opts || !opts.xml) {
						s += ' ' + name;
						continue;
					}
				}

				if (name==='value') {
					if (nodeName==='select') {
						selectValue = v;
						continue;
					}
					else if (nodeName==='option' && selectValue==v) {
						s += ` selected`;
					}
				}
				s += ` ${name}="${encodeEntities(v)}"`;
			}
		}
	}

	// account for >1 multiline attribute
	if (pretty) {
		let sub = s.replace(/^\n\s*/, ' ');
		if (sub!==s && !~sub.indexOf('\n')) s = sub;
		else if (pretty && ~s.indexOf('\n')) s += '\n';
	}

	s = `<${nodeName}${s}>`;
	if (String(nodeName).match(/[\s\n\\/='"\0<>]/)) throw s;

	let isVoid = String(nodeName).match(VOID_ELEMENTS);
	if (isVoid) s = s.replace(/>$/, ' />');

	let pieces = [];

	let children;
	if (html) {
		// if multiline, indent.
		if (pretty && isLargeString(html)) {
			html = '\n' + indentChar + indent(html, indentChar);
		}
		s += html;
	}
	else if (props && getChildren(children = [], props.children).length) {
		let hasLarge = pretty && ~s.indexOf('\n');
		for (let i=0; i<children.length; i++) {
			let child = children[i];
			if (child!=null && child!==false) {
				let childSvgMode = nodeName==='svg' ? true : nodeName==='foreignObject' ? false : isSvgMode,
					ret = renderToString(child, context, opts, true, childSvgMode, selectValue);
				if (pretty && !hasLarge && isLargeString(ret)) hasLarge = true;
				if (ret) pieces.push(ret);
			}
		}
		if (pretty && hasLarge) {
			for (let i=pieces.length; i--; ) {
				pieces[i] = '\n' + indentChar + indent(pieces[i], indentChar);
			}
		}
	}

	if (pieces.length) {
		s += pieces.join('');
	}
	else if (opts && opts.xml) {
		return s.substring(0, s.length-1) + ' />';
	}

	if (!isVoid) {
		if (pretty && ~s.indexOf('\n')) s += '\n';
		s += `</${nodeName}>`;
	}

	return s;
}