A different look at React components

As we all know, React has functional and class components. Each type has its pros and cons.

Class components have less performance than functional ones and cause some difficulties in reusing the same logic.

My opinion

Personally, I don’t like the ubiquitous arrow functions and this.

Functional components, in turn, for optimization force us to wrap objects in useMemoand the functions in useCallback. This reduces the readability of the code, and with a large number of calls it also reduces performance (no matter how paradoxical it may be).

You may be wondering: “Do we have another option?”. Yes, it exists!
What if we take a functional component and add a “constructor” function to it, like a method of the same name in classes?. Then we don’t need to wrap in useMemo and useCallback, since objects and functions will be created once. Also, we will not lose the convenient reuse of logic and we will not need this on every line.

Pretty tempting conditions, but is it possible to do this without “crutches”?
I asked this question and found a solution: use closures to implement the “constructor”. After several evenings on the Internet, the npm package was “born” react-afc.

What could a component with complex logic look like in pure React:

import React, { useMemo, useState, useCallback, memo } from 'react'
import ComplexInput from './ComplexInput'
import ComplexOutput from './ComplexOutput'

function Component(props) {
  const [text, setText] = useState('')
  
  const config = useMemo(() => ({
    showDesc: true,
    title: 'Title'
  }), [])

  const onChangeText = useCallback(e => {
    setText(e.target.value)
  })

  const onBlur = useCallback(() => {
    // hard calculations
  })

  return <>
    <ComplexInput value={text} onChange={onChangeText} onBlur={onBlur} />
    <ComplexOutput config={config} />
  </>
}

export default memo(Component)

The example is abstract, but even in it the problems of frequent wrapping of entities are already visible. As the component becomes more complex, it only gets worse.

Same example but using react-afc:

import React from 'react'
import { afcMemo, useState } from 'react-afc'
import ComplexInput from './ComplexInput'
import ComplexOutput from './ComplexOutput'

function Component(props) {
  const [text, setText] = useState('')
  
  const config = {
    showDesc: true,
    title: 'Title'
  }

  function onChangeText(e) {
    setText(e.target.value)
  }

  function onBlur() {
    // hard calculations
  }

  return () => <>
    <ComplexInput value={text.val} onChange={onChangeValue} onBlur={onBlur} />
    <ComplexOutput config={config} />
  </>
}

export default afcMemo(Component)

What has changed?

Now the component’s function is a “constructor” and is called only once during the entire lifecycle of the component. It means that onChangeText, onBlur and config are the same every render (no wrappers), i.e. they don’t cause the “children” to be redrawn when the component is updated. The constructor returns a render function that is called every render.

What about performance?

Package maximizes reuse React Hooks: on multiple calls useState from react-afc only one hook is used React. This breaks viewing component states in reactdevtoolsbut that’s the price of performance.

In general, the difference in performance is negligible. But the more complex the component, the higher the gap between conventional and afc components (react-afc maybe be up to 10% faster).

The package may change in the future. There is no need to rewrite existing projects to it. But using new ones can be very convenient.

Looking forward to your opinion in the comments 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *