Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposed Improvements #18

Closed
Izhaki opened this issue Nov 7, 2019 · 12 comments
Closed

Proposed Improvements #18

Izhaki opened this issue Nov 7, 2019 · 12 comments
Assignees
Labels
enhancement New feature or request

Comments

@Izhaki
Copy link

Izhaki commented Nov 7, 2019

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;
};
@quisido quisido self-assigned this Nov 8, 2019
@quisido
Copy link
Owner

quisido commented Nov 9, 2019

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 quisido added the enhancement New feature or request label Nov 9, 2019
@Izhaki
Copy link
Author

Izhaki commented Nov 11, 2019

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

@quisido
Copy link
Owner

quisido commented Nov 12, 2019

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.

@Izhaki
Copy link
Author

Izhaki commented Nov 15, 2019

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

@quisido
Copy link
Owner

quisido commented Nov 15, 2019

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.

@Izhaki
Copy link
Author

Izhaki commented Nov 18, 2019

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

?

@quisido
Copy link
Owner

quisido commented Nov 19, 2019

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.

@Izhaki
Copy link
Author

Izhaki commented Nov 20, 2019

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

@quisido
Copy link
Owner

quisido commented Nov 20, 2019

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?

@Izhaki
Copy link
Author

Izhaki commented Nov 20, 2019

Have a look at benchmark.

For example usage with React see Material UI component benchmarking.

@evoyy
Copy link

evoyy commented Jan 3, 2020

@Izhaki another one for you:

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

@quisido
Copy link
Owner

quisido commented Jan 3, 2020

@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 quisido closed this as completed Nov 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants