React class functionality in function components
Tags: react
Published: 2020-07-30 17:25:00

Hooks are the thing everybody loves. But classes still have their uses.

So...

![Why not have both?](https://i.kym-cdn.com/photos/images/newsfeed/000/538/731/0fc.gif)

## 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!

```jsx
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`](https://github.com/salvoravida/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 :)

```js
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.

{% codesandbox react-class-hook-fun-cyh50 %}

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:

1. Use `react-universal-hooks` to provide support for hooks in classes. This way there is no need to re-implement existing React hooks.
2. Abstract the class related custom hooks so that you can use them like `useClassMount`, `useClassUpdate`, `useClassState` and `useUpdateClassIf` or something like that.

Have fun!