use-force-update icon indicating copy to clipboard operation
use-force-update copied to clipboard

Proposed Improvements

Open Izhaki opened this issue 5 years ago • 12 comments

Current Code

export default function useForceUpdate(): () => void {
  const [ , dispatch ] = useState<{}>(Object.create(null));

  // Turn dispatch(required_parameter) into dispatch().
  const memoizedDispatch = useCallback(
    (): void => {
      dispatch(Object.create(null));
    },
    [ dispatch ],
  );
  return memoizedDispatch;
}

Proposed Improvements

JS, but can easily be typed:

import { useRef, useState } from 'react';

// Creates an empty object, but one that doesn't inherent from Object.prototype
const newValue = () => Object.create(null);

export default () => {
  const setState = useState(newValue())[1];

  const forceUpdate = useRef(() => {
    setState(newValue());
  }).current;

  return forceUpdate;
};

Rational:

First, dispatch unclear. Looking at the history, it is a residue from when useReducer was used, but this is no longer the case.

Then, from the docs (see note):

React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.

So there's no need providing it as a useCallback dependency.

Either useCallback or useMemo with empty dependencies ([] - ie, one-off) is identical to useRef().current which somehow communicates the intent better - this value will not change. Also, with useRef, react doesn't call Object.is(old, new) each render.

Also note that in the current code we have memoizedDispatch but because dispatch is stable, there's no real memoization going on - useCallback will always return the same function.


Finally, in my case, initial render could trigger dozens of forceUpdates so a further improvement could be (I'm not actually using it because the trade-off doesn't seem to be worth it, but others may find this useful):

import { useRef, useState, useEffect } from 'react';

// Creates an empty object, but one that doesn't inherent from Object.prototype
const newValue = () => Object.create(null);

export default () => {
  const setState = useState(newValue())[1];

  const updatePending = useRef(false);

  const forceUpdate = useRef(() => {
    if (!updatePending.current) {
      setState(newValue());
      updatePending.current = true;
    }
  }).current;

  useEffect(() => {
    updatePending.current = false;
  });

  return forceUpdate;
};

Izhaki avatar Nov 07 '19 23:11 Izhaki

I like renaming dispatch and dropping it from the dependency array.

Calling forceUpdate twice resulting in two updates is by design. I did not want to hinder a developer's ability to do that if they wanted, for whatever reason. If you call forceUpdate twice synchronously, you get two re-renders. The only reason I would want to change this is if class components' this.forceUpdate de-dupes. I'm not positive that it does, but since this hook is meant to replace that method, it would be the definitive source for behavior.

Can you elaborate more on if there is real benefit to useRef over useCallback? This was a discussion before, but there was no justification for useRef, while useCallback matched the exact documentation for use case -- memoizing a function. In this case, we are memoizing the removal of a parameter. The idea is that for dependency-less memoization, useRef avoids comparisons? Is there ever a situation where a dependency-less useMemo or useCallback would be a better choice than useRef? For readability or for performance?

quisido avatar Nov 09 '19 16:11 quisido

A few things:

I did not want to hinder a developer's ability to do that if they wanted, for whatever reason.

I agree with this decision. I think it is based on assumptions; it clutters the code; and its benefit depends on the use-case. As I mentioned, I don't even use this myself.

If you call forceUpdate twice synchronously, you get two re-renders.

This is not the case. Within a render cycle, once you call setState in a component, react simply renders it as 'dirty', which will queue a new render-cycle (at the end of the current one) where any dirty component will be re-rendered. But you can so long you are within the same render cycle, you can call setState 1000 times on the same component - it will still only re-render once. (Also, worth remembering that only the value in the last setState is used, and that within the same render cycle setState doesn't actually modify the state. Rather, it queues the update).

...while useCallback matched the exact documentation for use case -- memoizing a function

As I said, this is not memoizing per-se. The idea with memoizing is that if the input doesn't change you simply return the previous computed value (and if the input changes, you re-compute; save it; return the value). In your case the input value ([]) never changes other than in the first ever call.

The idea is that for dependency-less memoization, useRef avoids comparisons?

Both useMemo and useCallback still call Object.is to compare the previous and current dependencies (where the previous [] and the current [] will always return "no change". I'm pretty sure this is the line of code where this happens.

As stated in the docs:

If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.

It is exactly the same mechanism with useMemo and useCallback.

useRef on the other hand means - always return the same value, always. So you always get the same ref and you can mutate ref.current at will. There are no dependencies involved here.

Also, and although hardly relevant, when you have:

const x = useMemo(()=> {...}, []);

You still allocation a new [] in each render.

Is there ever a situation where a dependency-less useMemo or useCallback would be a better choice than useRef?

My instinct is to say that there isn't such a situation, but I may have overlooked some cases? I took it to the SO community

Izhaki avatar Nov 11 '19 23:11 Izhaki

This is not the case. Within a render cycle, once you call setState in a component, react simply renders it as 'dirty', which will queue a new render-cycle (at the end of the current one) where any dirty component will be re-rendered. But you can so long you are within the same render cycle, you can call setState 1000 times on the same component - it will still only re-render once.

There is no guarantee that two calls to forceUpdate will be within the same render cycle. React re-renders synchronously. Try it!

This is why unstable__batchedUpdates was introduced. Otherwise, setState1(); setState2(); would trigger 2 re-renders.

I'll try to stay up-to-date with the SO post. I've never tried before today, but there doesn't appear to be a "subscribe" feature.

quisido avatar Nov 12 '19 06:11 quisido

To be honest, I just came across the React implementation for this, which I reckon is superior to the proposed above:

const [, forceUpdate] = useReducer(x => x + 1, 0);

Izhaki avatar Nov 15 '19 12:11 Izhaki

To be honest, I just came across the React implementation for this, which I reckon is superior to the proposed above:

This was the original implementation in this package, and it was requested to be changed so as not to have to deal with max integer values in [primarily older] browsers with a cap.

quisido avatar Nov 15 '19 19:11 quisido

const [, forceUpdate] = useReducer(x => (x + 1) % Number.MAX_VALUE, 0);

?

Izhaki avatar Nov 18 '19 15:11 Izhaki

See the initial commit.

It was changed to be simpler to read and more performant than addition and modulo per call, much the way ref would reduce an Object.is comparison per call.

quisido avatar Nov 19 '19 00:11 quisido

Benchmarks:

x = (x + 1) % Number.MAX_VALUE;
6,842,819 ops/s ±0.89%
98.99% slower
x = Object.create(null)
675,664,884 ops/s ±0.66%
fastest

Yet the slow option takes 6.8 microseconds to run - not something that needs optimising.


Another option:

const [, forceUpdate] = useReducer(x => Object.create(null));

Izhaki avatar Nov 20 '19 01:11 Izhaki

I am fond of the fact that a library that can amount to being a single line long has nothing more to worry about than micro-optimization. It's why useCallback(f, []) versus useRef(f).current is so interesting.

Are you aware of any performance test suites that apply to React components?

quisido avatar Nov 20 '19 06:11 quisido

Have a look at benchmark.

For example usage with React see Material UI component benchmarking.

Izhaki avatar Nov 20 '19 10:11 Izhaki

@Izhaki another one for you:

const forceUpdate = useReducer(() => Symbol())[1];

evoyy avatar Jan 03 '20 01:01 evoyy

@evoyy Symbol has poor browser support. As much as I dislike IE, I wouldn't want to cut out its users from all dependents on this package.

quisido avatar Jan 03 '20 20:01 quisido