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

Introduce a new hook that called useStateRef #201

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

behnammodi
Copy link

@behnammodi behnammodi commented Aug 25, 2021

Summary

Introduce a new hook to reduce/improve some processes.

Basic example

In this example we see useEffect that doesn't need a dependency to read updated count's value, so that's mean we don't need to perform useEffect effect function event count will change. useStateRef just is a name, so might we need to change to a better name.

const [count, setCount, getCount] = useStateRef();

useEffect(() => {
  const handleVisibility = () => {
    console.log(getCount());
  };

  document.addEventListener('visibilitychange', handleVisibility);

  return () =>  document.removeEventListener('visibilitychange', handleVisibility);
}, []);

Also, in this example, useCallback doesn't need a dependency to know count's value, so useCallback inside function just performs once. that's awesome

const [count, setCount, getCount] = useStateRef();

const handleClick = useCallback(() => {
  console.log(getCount());
}, []);

Motivation

useEffect and useCallback need to know dependencies to redefine function depended on new values of dependencies. but these are redundant process for React because we don't know what time we need it exactly ex: handleClick depend on user behavior so we just need count's value when the user needs it.

Detailed design

A basic of implementation for useStateRef that we can use in project is:

TS version:

function useStateRef<T>(initialValue: T): [T, (nextState: T) => void, () => T] {
  const [state, setState] = useState(initialValue);
  const stateRef = useRef(state);
  stateRef.current = state;
  const getState = useCallback(() => stateRef.current, []);
  return [state, setState, getState];
}

JS version:

function useStateRef(initialValue) {
  const [state, setState] = useState(initialValue);
  const stateRef = useRef(state);
  stateRef.current = state;
  const getState = useCallback(() => stateRef.current, []);
  return [state, setState, getState];
}

Drawbacks

This is a new hook, so we don't have any breaking change, also we can implement that by internal React hooks.

Alternatives

Alternative can be a package, maybe

Adoption strategy

Fortunately, we don't have any breaking change, also we can embed this hook to useState without breaking change

const [count, setCount, getCount] = useState();

How we teach this

This RFC introduce a new hook, so we have to add some section to React documentation

Unresolved questions

  • do we need to implement a new hook or embed it to useState?

@behnammodi behnammodi changed the title introduce a new hook that called useStateRef Introduce a new hook that called useStateRef Aug 25, 2021
@aghArdeshir
Copy link

aghArdeshir commented Oct 28, 2021

Nice. Encouraging the use of getters can also prevent issues like this from happening more often: https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function

(But, of course, may result in other type of bugs and mis-assumptions to appear more often, like when you intentionally want to see the value of state when you triggered an action, and not the current value)

See this for example:
https://codesandbox.io/s/epic-fire-frpyo?file=/src/App.js

Also from syntax point of view, hooks removed variables and assignments and instead provided variables and setters. Encouraging use of getters feels more like removing variables and assignments and instead providing getters and setters. With getters you can have best of both worlds more easily.

@badeggg
Copy link

badeggg commented Nov 18, 2021

How do you think of const [state, setState, stateRef] = useState();? Inspired by facebook/react#21931 . No need to add new hook and still no break change.

And I think useCallbackRef is really needed. Use useCallbackRef like:

const [doSomething, doSomethingRef] = useCallbackRef(() => {/* some code here */}, [dep1, dep2]);

Chances are that I need call a function but don't really want the function change trigger the calling:

const [state, setState] = useState(5);
const [shouldPrint, setShouldPrint] = useState(false);
 
const [print, printRef] = useCallbackRef(() => {
    console.log(state);
}, [state]);
 
useEffect(() => {
    // I need call print here, but don't really want print in dependence
    // array which will trigger code execution here
    if (shouldPrint) {
        printRef.current();
    }
}, [shouldPrint, printRef]);

As I mentioned in facebook/react#22773, the real problem we are talking here is

that hooks mix the concept of dependence and effect. An effect execution may need the fresh value of a state but doesn't want the changing of the state trigger the execution. So the state has a confusing place to set. Should the state appear in dependency array?

useCallback has similar problem.

roli667 referenced this pull request Nov 22, 2021
I've started by copying the Yarn RFCs repo, with some tweaks to the
README and the RFC template. We'll continue iterating on these.
@viniciusdacal
Copy link

Just had this same idea and I'm so glad someone already open an RFC for it.
Just giving my 2 cents, I imagine we could use React.useState, adding an additional returning value to the tuple.
Also, there might be a case that the suggested implementation wouldn't cover, which is an immediately getState after a setState. Not sure if it's a real scenario, but it's worthy to keep it in mind:

const [count, setCount, getCount] = React.useStateRef(0);

const handleClick = useCallback(() => {
  setCount((c) => c + 1);
  console.log(getCount()); // this would print 0 as the state doesn't update sincronously
}, []);

I'm not familiar with react internal code, but I imagine it keeps a stack of all recent calls to setState. I believe such a stack could be accessed and also executed (in the case an update used a callback) to get the latest state.

@behnammodi
Copy link
Author

@viniciusdacal Thank you, your consideration is perfect, but the purpose of this is to remove dependencie from useCallback and useEffect and not change React default behavior, please see the below example:

const [a, setA] = useState(0);
const [b, setB] = useState(0);

const handleClick = useCallback(() => {
  setA(a + 1);    // a is 0 here
  setB(a + b);    // a is 0 here yet
  console.log(a, b); // 0 0
}, [a, b]);

so we want to remove dependencie from above code:

const [a, setA, getA] = useStateRef(0);
const [b, setB, getB] = useStateRef(0);

const handleClick = useCallback(() => {
  setA(getA() + 1);       // getA() returns 0 here
  setB(getA() + getB());  // getA() returns 0 here yet as above code
  console.log(getA(), getB()); // 0 0
}, []);

as you see we didn't change behavior we just prevent re-initiate handleClick, but imagine getA() returns 1 instead of 0 in the next line.

@behnammodi
Copy link
Author

@aghArdeshir Thanks, yes I agree with you on some points, I tried to prevent breaking-changes.

@behnammodi
Copy link
Author

@badeggg Thank you for the comment, as you see this hook could be embedded to useState and the main purpose is removing dependencies from useCallback and useEffect not anymore

@webcarrot
Copy link

webcarrot commented Feb 20, 2022

I use something like:

import {
  useState,
  useRef,
  useMemo,
  useCallback,
  useEffect,
  SetStateAction,
  Dispatch,
} from "react";

export const useStateRef = <S>(
  initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>, () => S] => {
  const [, updateRenderState] = useState(0);
  const stateRef = useRef<S>(
    useMemo(
      initialState instanceof Function ? initialState : () => initialState,
      []
    )
  );
  useEffect(
    () => () => {
      stateRef.current = null as S; // Just in case help to free some memory
    },
    []
  );
  return [
    stateRef.current,
    useCallback<Dispatch<SetStateAction<S>>>((v) => {
      stateRef.current = v instanceof Function ? v(stateRef.current) : v;
      updateRenderState((v) => (v + 1) % Number.MAX_SAFE_INTEGER);
    }, []),
    useCallback(() => stateRef.current, []),
  ];
};

In this implementation getState always return most recent value of state.

PS. This is a simplified version - as a lazy programmer I handle a bit more useful cases like:

import {
  useState,
  useRef,
  useMemo,
  useCallback,
  useEffect,
  SetStateAction,
  Dispatch,
} from "react";

const INI = Object.create(null);

export const useSmartState = <S>(
  initialState: S | (() => S),
  updateStateOnInitialStateChange = false
): [S, Dispatch<SetStateAction<S>>, () => S] => {
  const [, updateRenderState] = useState(0);
  const stateRef = useRef<S>(INI as S);
  useMemo(() => {
    stateRef.current =
      stateRef.current === INI || updateStateOnInitialStateChange
        ? initialState instanceof Function
          ? initialState()
          : initialState
        : stateRef.current;
  }, [initialState, updateStateOnInitialStateChange]);
  useEffect(
    () => () => {
      stateRef.current = null as S;
    },
    []
  );
  return [
    stateRef.current,
    useCallback<Dispatch<SetStateAction<S>>>((v) => {
      const current = stateRef.current;
      stateRef.current = v instanceof Function ? v(current) : v;
      if (stateRef.current !== current) {
        updateRenderState((v) => (v + 1) % Number.MAX_SAFE_INTEGER);
      }
    }, []),
    useCallback(() => stateRef.current, []),
  ];
};

@gaearon
Copy link
Member

gaearon commented May 4, 2022

We have posted an alternative proposal that we think achieves the same goals (but differently). Feedback welcome!

#220

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants