React hooks & the closure hell

anpos231

anpos231

Posted on June 24, 2019

React hooks & the closure hell

React hooks & the closure hell

Since Facebook introduced functional components and hooks, event handlers become simple closures. Don't get me wrong, I like functional components, but there is a number of issues that niggle at me, and when I ask about them in the community, the most common answer is: "don't worry about premature optimizations".

But that is the problem for me, I grew up programming in C, and I frequently worry about the performance of my applications, even if others find it less significant.

The problem?

Since event handlers are closures we need to either re-create them on each render or whenever one of it's dependencies changes. This means components that only depend on the event handler (and possibly not on the handler's dependencies) will have to re-render too.

Consider this example code (Try here):

import React, { useState, useCallback, memo } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

let times = 0

const ExpensiveComponent = memo(({ onClick }) => (
  <p onClick={onClick}>I am expensive form component: {times++}</p>
))

const App = () => {
  const [value, setValue] = useState(1);

  const handleClick = useCallback(
    () => {
      setValue(value + 1)
    },
    [value],
  );

  return (
    <div className="app">
      <ExpensiveComponent onClick={handleClick} />
      <button onClick={handleClick}>
        I will trigger expensive re-render
      </button>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Enter fullscreen mode Exit fullscreen mode

In the previous example, clicking on the button will cause ExpensiveComponent to re-render. In case of class based components it would be unnecessary.

Solution?

The experimental tinkerer I am I tried to find the solution to this problem, solution where we can use functional components, but don't have to create a new callback every time we create a new value.

So I created useBetterCallback(fn, deps). The signature for this function/hook is identical to useCallback(fn, deps). The difference is that it will always return the same identical handler no matter what.

Some of you might think: 'So how do I access fresh state values?'. useBetterCallback will call your handler with one additional argument, and that argument is an array with all dependencies your callback depends on. So instead of recreating the callback we pass new values to existing one.

Here is the source code for the useBetterCallback hook.

const useBetterCallback = (callback, values) => {
  const self = useRef({
    values: values,
    handler: (...args) => {
      return callback(...args, self.current.values)
    }
  });
  self.current.values = values
  return self.current.handler
}
Enter fullscreen mode Exit fullscreen mode

And here is an example of the useBetterCallback in action (Try here):

import React, { useState, useRef, memo } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

const useBetterCallback = (callback, values) => {
  const self = useRef({
    values: values,
    handler: (...args) => {
      return callback(...args, self.current.values)
    }
  });
  self.current.values = values
  return self.current.handler
}

let times = 0

const ExpensiveComponent = memo(({ onClick }) => (
  <p onClick={onClick}>I am expensive form component: {times++}</p>
))

const App = () => {
  const [value, setValue] = useState(1);

  const handleClick = useBetterCallback((event, [ value, setValue ]) => {
    setValue( value + 1 )
  }, [value, setValue])

  console.log("Value: " + value)

  return (
    <div className="app">
      <ExpensiveComponent onClick={handleClick} />
      <button onClick={handleClick}>
        I will not trigger expensive re-render
      </button>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Enter fullscreen mode Exit fullscreen mode

Review?

What do you think?

💖 💪 🙅 🚩
anpos231
anpos231

Posted on June 24, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related