DEV Community

loading...

React hooks & the closure hell

anpos231 profile image anpos231 ・2 min read

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);

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
}

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);

Review?

What do you think?

Discussion

pic
Editor guide
Collapse
robchristian profile image
Rob Christian

This is a lot more code than you need. Taking your original sample, put value into a ref from useRef, and inside the useCallback function, get value from your ref. Also, don't use [value] as a dependency, use only [] dependencies, this way it only runs once.

const App = () => {
  const [value, setValue] = useState(1);
  const valueRef = useRef(value);
  valueRef.current = value;


  const handleClick = useCallback(
    () => {
      setValue(valueRef.current + 1)
    },
    [],
  );
Collapse
anpos231 profile image
anpos231 Author

There is another way to go about this, maybe cleaner for some people.

const useSelfish = (load) => {
  const self = useRef(load)
  self.current.props = load.props
  self.current.state = load.state
  return [ self.current ]
}

It can be used like this (Try here):

const [ self ] = useSelfish({
    props: props,
    state: {
      value: useState(1)
    },
    handleClick: function() {
      const [ value, setValue ] = self.state.value
      setValue(value + 1)
    }
})

This version will be nicer for people who preferred old-school OOP programming. You've got the object, and you can set whatever you want there.

Example

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

import "./styles.css";

const useSelfish = (load) => {
  const self = useRef(load)
  self.current.props = load.props
  self.current.state = load.state
  return [ self.current ]
}

let times = 0;

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

const App = (props) => {

  const [ self ] = useSelfish({
    props: props,
    state: {
      value: useState(1)
    },
    handleClick: function() {
      const [ value, setValue ] = self.state.value
      setValue(value + 1)
    }
  })

  const [ value ] = self.state.value

  console.log("Value: " + value);

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

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Collapse
ntvinhit profile image
Nguyễn Trọng Vĩnh

How can I use effect in this?
Or just with normal useEffect with dependencies [self.state.value]?

Collapse
anpos231 profile image
anpos231 Author

useEffect with dependencies [self.state.value]

Should work fine.

But I advise you try the new, cleaner version: dev.to/anpos231/react-hooks-the-cl...

Collapse
amsterdamharu profile image
Harm Meijer

React team actually discurrages this patern: reactjs.org/docs/hooks-faq.html#ho...

Instead of the pattern you use you can pass a callback to the state setter:

const handleClick = React.useCallback(() => {
  setValue(value => value + 1);
}, []);
Collapse
alexpanchuk profile image
Alex Panchuk

How about this one?

function useStateWithGetter(initial) {
  const ref = useRef(initial);
  const [state, setState] = useState(initial);

  useEffect(() => {
    ref.current = state
  }, [state])

  const getValue = useCallback(() => {
    return ref.current;
  }, []);

  return [state, setState, getValue];
}

// ...

const [, setValue, getValue] = useStateWithGetter(1);

const handleClick = useCallback(() => {
  const value = getValue();
  setValue(value + 1);
}, [getValue, setValue]);
Enter fullscreen mode Exit fullscreen mode
Collapse
mikeaustin profile image
Mike Austin

I love how it has a similar interface to the original useCallback(). I saw your update, but I still like this simple replacement. I was headed down a similar road using refs, but I like how the logic is encapsulated in the hook. I was going nuts trying to optimize my app, where event handlers are almost always dependent on props, causing them to be regenerated and causing re-renders of children.

This also simplifies the dependencies, since you don't need to provide full paths such as [state.x.y.z, ...]. You can just pass [state], and you get a nice snapshot of everything, quickly, since it's immutable. As others has noted, it heads toward simulating dynamic scoping, a.k.a "this" in object-orientation.

Thanks again for the inspiration!

Collapse
timkindberg profile image
Tim Kindberg
const handleClick = useCallback(
  () => {
    setValue(oldValue => oldValue + 1)
  },
  []
)