4

About

This isn't a problem, just trying to gain more insight into an idea which some colleagues and I thought was cool.

We are able to achieve fewer re-renderings by using the function below rather than useCallback.

Imagine a bigger codebase, with callbacks that gets propagated from child to grand child over many levels.

This solution allows the creation of memoized callbacks that run in a scope which may change, but which does not change the reference to the callback itself.

Please take a close look at the following function (and yep, callbackContainer.current = callback is meant to be there)

function useStableCallback<T>(callback: () => T) {
  const callbackContainer = useRef(callback);
  callbackContainer.current = callback;
  return useCallback(() => callbackContainer.current(), [callbackContainer]);
}

Why this pattern?

Let me start with an example of how this was a non-issue with class components

With React class components it was easy to give child components stable callback functions as props.

In the following example the child component (MyChildComponent) receives a stable reference to a callback than can be used to refresh some data.

Point: The child component uses a function that depends on state which the child component itself does NOT depend on, (i.e. this.props.url on the parent).

If the parent receives a new url, this doesn't cause re-rendering of the child. Because the onRefresh callback exists on the parent object, it does not change.

Useful!

function MyChildComponent({
  onRefresh,
  title,
  body
}: {
  onRefresh: () => any;
  title: string;
  body: string;
}) {
  return (
    <article>
      <SomeOtherChildComponent title={title} body={body} />
      <button onClick={onRefresh}>Refresh</button>
    </article>
  );
}

interface ParentState {
  title?: string;
  body?: string;
  isLoading: boolean;
}

interface ParentProps {
  url: string;
}

class MyParentComponent extends React.Component<ParentProps> {
  state: ParentState = { isLoading: false };

  refresh = () => {
    this.setState({ isLoading: true });
    fetch(this.props.url)
      .then(r => r.json())
      .then(r =>
        this.setState({
          title: r.title,
          body: r.body,
          isLoading: false
        })
      );
  };

  render() {
    return this.state.isLoading ? (
      <h3>Loading...</h3>
    ) : (
      <MyChildComponent
        title={this.state.title}
        body={this.state.body}
        onRefresh={this.refresh}
      />
    );
  }
}

Now with hooks

In the following block MyParentComponent as been implemented as a function with hooks.

Uh-oh! As we can see, the refresh-function will now change every time MyParentComponent receives a new url!

function MyParentComponent({ url }: ParentProps) {
  const [isLoading, setIsLoading] = useState(false);
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');

  const refresh = useCallback(() => {
    setIsLoading(true);
    fetch(url)
      .then(r => r.json())
      .then(r => {
        setIsLoading(false);
        setTitle(r.title);
        setBody(r.body);
      });
  }, [url, setIsLoading, setTitle, setBody]);

  return isLoading ? (
    <h3>Loading...</h3>
  ) : (
    <MyChildComponent title={title} body={body} onRefresh={refresh} />
  );
}

With suggested pattern

refresh can be defined as follows:

const refresh = useStableCallback(() => {
  setIsLoading(true);
  fetch(url)
    .then(r => r.json())
    .then(r => {
      setIsLoading(false);
      setTitle(r.title);
      setBody(r.body);
    });
});

This is cool because...

  • The refresh function will never change (and thus won't cause re-renderings)
  • It brings some handy functionality from class components into the world of functional components
  • The function will always run in the most recent scope of the component in which it was defined. In other words: refresh knows about changes to url, even though the function itself did not change.

Is it a good pattern?

This definitely helps us to avoid a bunch of re-renderings, but are there any potential problems with this pattern? What do you think?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Marius Brataas
  • 614
  • 1
  • 5
  • 8
  • I really like the idea, but "*Opinions are welcome*" is off-topic on StackOverflow so I've rephrased the question a bit – Bergi Mar 29 '22 at 15:46
  • Possible duplicate of [Why can't `useCallback` always return the same ref](https://stackoverflow.com/q/65890278/1048572) – Bergi Mar 29 '22 at 15:53
  • To gather opinions on the idea, it might be worth submitting it to https://www.reddit.com/r/reactjs/ (instead of `/r/typescript` as I suggested earlier) – Oblosys Mar 29 '22 at 16:01
  • I think this entry fits better in the https://codereview.stackexchange.com/ site. – James Nov 09 '22 at 01:44

0 Answers0