Hooks are the thing everybody loves. But classes still have their uses.
So...
Don't do this
So what am I talking about, really? Well, I had this idea on wrapping the good old React class components into something that combines most of the features from the hook world and classes. Make Classes Beautiful Again. Have class components with syntax that is similar to function components.
As an overall idea this probably shouldn't be done. But you know the internet. There is always somebody who does the thing which shouldn't be done.
What does it look like?
This is from my initial implementation and I should get rid of the use
object, but here we go!
const Test = classHook((use, props) => {
const [{ hello }, setState] = use.state(() => ({ hello: 'world' }))
const onClick = use.callback(() => {
setState({ hello: hello === 'world' ? 'nothing' : 'world' })
alert('Hello ' + hello + '!')
}, [hello])
use.mount(() => {
console.log('I mounted!')
return () => console.log('I will unmount!')
})
use.update(() => {
console.log('Before render')
return () => console.log('After render')
})
use.updateIf(() => true)
return <button onClick={onClick}>{JSON.stringify(props)}</button>
})
These may need some explanations... And we're really hacking the system here.
use.mount(props, state)
This is componentDidMount
. You can use it once per component.
If you want to cleanup stuff on unmount you can return a function. That is componentWillUnmount
. It will also receive props
and state
.
use.update(prevProps, prevState)
You can use this method to gather information from the DOM right before render. So yes, this is actually getSnapshotBeforeUpdate
! You can use it only once per component.
Return a function here and you can update state after render based on the information you gather before render. This is equivalent to componentDidUpdate
, but you will receive props and state (not prevProps and prevState, you already have them!)
use.updateIf(nextProps, nextState)
Return true
to render. Return false
to not render. This is shouldComponentUpdate
. You can use it only once per component.
use.state
Similar to useState
hook, but you can use it only once per component as you are working with the state of a class component.
Other supported hooks
use.callback
, use.effect
, use.memo
, and use.ref
each work as you would assume them to work.
How is this crime of Reactverse implemented?
As said this is the very first implementation just to have some fun. With a quick look on the interwebs I didn't find anyone who had done this before. I could find react-universal-hooks
but it does kind of the opposite by allowing hooks inside existing class components, while I wanted to have class components but with hook-like syntax :)
function memoDiff(arr1, arr2) {
if (arr1 == null) return false
for (let i = 0; i < arr1.length; i++) {
if (!Object.is(arr1[i], arr2[i])) return true
}
return false
}
function classHook(render) {
if (typeof render !== 'function') {
throw new Error('classHook needs function component')
}
class ClassHook extends React.PureComponent {
constructor(props) {
super(props)
this.effects = []
this.once = {}
this.use = {}
this.memoBusters = []
this.memoIndex = 0
this.menos = []
this.refIndex = 0
this.refs = []
let setState
Object.defineProperty(this.use, 'callback', {
value: (fn, memo) => {
if (typeof fn !== 'function') return
if (this.memoIndex === this.menos.length) {
this.menos.push(fn)
this.memoBusters.push(memo)
} else if (memoDiff(this.memoBusters[this.memoIndex], memo)) {
this.menos[this.memoIndex] = fn
this.memoBusters[this.memoIndex] = memo
}
const memoFn = this.menos[this.memoIndex]
this.memoIndex++
return memoFn
}
})
Object.defineProperty(this.use, 'effect', {
value: (fn, memo) => {
if (typeof fn !== 'function') return
if (this.memoIndex === this.menos.length) {
this.menos.push(fn)
this.memoBusters.push(memo)
this.effects.push(fn)
} else if (memoDiff(this.memoBusters[this.memoIndex], memo)) {
this.menos[this.memoIndex] = fn
this.memoBusters[this.memoIndex] = memo
this.effects.push(fn)
}
this.memoIndex++
}
})
Object.defineProperty(this.use, 'memo', {
value: (fn, memo) => {
if (typeof fn !== 'function') return
if (this.memoIndex === this.menos.length) {
this.menos.push(fn())
this.memoBusters.push(memo)
} else if (memoDiff(this.memoBusters[this.memoIndex], memo)) {
this.menos[this.memoIndex] = fn()
this.memoBusters[this.memoIndex] = memo
}
const memoValue = this.menos[this.memoIndex]
this.memoIndex++
return memoValue
}
})
Object.defineProperty(this.use, 'ref', {
value: value => {
if (this.refIndex === this.refs.length) {
this.refs.push(React.createRef())
if (value != null) this.refs[this.refIndex] = value
}
const ref = this.refs[this.refIndex]
this.refIndex++
return ref
}
})
Object.defineProperty(this.use, 'mount', {
value: fn => {
if (!this._componentDidMount && typeof fn === 'function') {
this._componentDidMount = fn
}
}
})
Object.defineProperty(this.use, 'update', {
value: fn => {
if (!this.once.update && typeof fn === 'function') {
this._getSnapshotBeforeUpdate = fn
this.once.update = true
}
}
})
Object.defineProperty(this.use, 'updateIf', {
value: fn => {
if (!this.once.updateIf && typeof fn === 'function') {
this.shouldComponentUpdate = fn
this.once.updateIf = true
}
}
})
Object.defineProperty(this.use, 'state', {
value: fn => {
if (this.once.state) return
if (!setState) {
setState = this.setState.bind(this)
// eslint-disable-next-line
this.state = typeof fn === 'function' ? fn() : fn
}
this.once.state = true
return [this.state, setState]
}
})
}
componentDidMount() {
if (this._componentDidMount) {
const fn = this._componentDidMount(this.props, this.state)
if (typeof fn === 'function') {
this._componentWillUnmount = fn
}
}
}
componentWillUnmount() {
if (this._componentWillUnmount) {
this._componentWillUnmount(this.props, this.state)
}
}
getSnapshotBeforeUpdate(prevProps, prevState) {
if (this._getSnapshotBeforeUpdate) {
const fn = this._getSnapshotBeforeUpdate(prevProps, prevState)
if (typeof fn === 'function') {
this._componentDidUpdate = fn
}
}
return null
}
componentDidUpdate() {
if (this.effects.length) {
this.effects.forEach(effect => effect())
this.effects.length = 0
}
if (this._componentDidUpdate) {
this._componentDidUpdate(this.props, this.state)
}
}
render() {
this.once = {}
this.memoIndex = this.refIndex = 0
return render(this.use, this.props)
}
}
return ClassHook
}
Certainly not stuff you would want to have anywhere close your production site :) I haven't done thorough testing on this either so "it seems to work" but probably breaks badly on some edge cases. But maybe someone finds this fun to play around with!
Can't I just have CodeSandbox or something?
Yes.
I'm sorry that I didn't bother to create the most awesome demo ever. This only has a couple hours worth of work on it and I'm not sure if I bother to really improve the idea :)
The two things that could be done:
- Use
react-universal-hooks
to provide support for hooks in classes. This way there is no need to re-implement existing React hooks. - Abstract the class related custom hooks so that you can use them like
useClassMount
,useClassUpdate
,useClassState
anduseUpdateClassIf
or something like that.
Have fun!
Top comments (0)