Skip to content

Instantly share code, notes, and snippets.

@CodyJasonBennett
Last active March 7, 2023 07:12
Show Gist options
  • Save CodyJasonBennett/0c18d6f796fc811c3cee928ae016ac92 to your computer and use it in GitHub Desktop.
Save CodyJasonBennett/0c18d6f796fc811c3cee928ae016ac92 to your computer and use it in GitHub Desktop.
react-dom with `style:hover="..."`
// https://twitter.com/Cody_J_Bennett/status/1633002580635799553
import * as React from 'react'
import Reconciler from 'react-reconciler'
import { DefaultEventPriority, ConcurrentRoot } from 'react-reconciler/constants.js'
function toString(styles) {
if (typeof styles === 'string') return styles
else if (styles === null || typeof styles !== 'object') return
let string = ''
for (const style in styles) {
const key = style
.split(/(?=[A-Z])/)
.join('-')
.toLowerCase()
const value = styles[style]
string += `${key}:${value};`
}
return string
}
let i = 0
const styles = document.head.insertAdjacentElement('beforeend', document.createElement('style'))
const classNames = new Map() // value, className
function applyProps(instance, oldProps, newProps) {
for (const key in { ...oldProps, ...newProps }) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (Object.is(oldValue, newValue) || key === 'children') continue
if (key === 'style') {
for (const k in { ...oldValue, ...newValue }) {
if (oldValue?.[k] !== newValue?.[k]) {
instance.style[k] = newValue?.[k] ?? ''
}
}
} else if (key.startsWith('style:')) {
const selector = key.replace('style:', '')
const oldStyle = toString(oldValue)
const newStyle = toString(newValue)
if (oldStyle) {
const oldClassName = classNames.get(selector + oldStyle)
if (oldClassName) instance.classList.remove(oldStyle)
}
if (newStyle) {
let newClassName = classNames.get(selector + newStyle)
if (!newClassName) {
classNames.set(selector + newStyle, (newClassName = `__pseudo${i++}`))
styles.innerHTML += `.${newClassName}:${selector}{${newStyle}}`
}
if (oldStyle !== newStyle) instance.classList.add(newClassName)
}
} else if (key.startsWith('on')) {
const event = key.slice(2).toLowerCase()
if (oldValue) instance.removeEventListener(event, oldValue)
instance.addEventListener(event, newValue)
} else if (newValue == null) {
instance.removeAttribute(key)
} else {
instance.setAttribute(key, newValue)
}
}
return instance
}
const reconciler = Reconciler({
isPrimaryRenderer: true,
supportsMutation: true,
supportsPersistence: false,
supportsHydration: false,
now: performance.now,
scheduleTimeout: setTimeout,
cancelTimeout: clearTimeout,
noTimeout: -1,
createInstance: (type, props) => applyProps(document.createElement(type), {}, props),
hideInstance() {},
unhideInstance() {},
createTextInstance: (text) => document.createTextNode(text),
hideTextInstance() {},
unhideTextInstance() {},
appendInitialChild: (parent, child) => parent.appendChild(child),
appendChild: (parent, child) => parent.appendChild(child),
appendChildToContainer: (container, child) => container.appendChild(child),
insertBefore: (parent, child, beforeChild) => parent.insertBefore(child, beforeChild),
removeChild: (parent, child) => parent.removeChild(child),
removeChildFromContainer: (container, child) => container.removeChild(child),
getPublicInstance: (instance) => instance,
getRootHostContext: () => null,
getChildHostContext: () => null,
shouldSetTextContent: () => false,
finalizeInitialChildren: () => false,
prepareUpdate: () => true,
commitUpdate: (instance, updatePayload, type, oldProps, newProps) => applyProps(instance, oldProps, newProps),
commitTextUpdate: (textInstance, oldText, newText) => (textInstance.textContent = newText),
prepareForCommit: () => null,
resetAfterCommit() {},
preparePortalMount() {},
clearContainer() {},
getCurrentEventPriority: () => DefaultEventPriority,
beforeActiveInstanceBlur: () => {},
afterActiveInstanceBlur: () => {},
detachDeletedInstance: () => {},
})
reconciler.injectIntoDevTools({
findFiberByHostInstance: () => null,
bundleType: 0,
version: React.version,
rendererPackageName: 'mini-react-dom',
})
export function createRoot(container) {
const root = reconciler.createContainer(container, ConcurrentRoot, null, false, null, '', console.error, null)
return {
render(element) {
reconciler.updateContainer(element, root, null, undefined)
},
unmount() {
this.render(null)
},
}
}
export function createPortal(element, container) {
return <>{reconciler.createPortal(element, container, null, null)}</>
}
function Foo() {
const [color, setColor] = React.useState('red')
React.useEffect(() => void setTimeout(() => setColor('blue'), 2000), [])
return <p style:hover={{ color }}>test</p>
}
createRoot(document.getElementById('root')).render(<Foo />)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment