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

Add get function to useState #14543

Open
liyuanqiu opened this issue Jan 8, 2019 · 34 comments
Open

Add get function to useState #14543

liyuanqiu opened this issue Jan 8, 2019 · 34 comments

Comments

@liyuanqiu
Copy link

liyuanqiu commented Jan 8, 2019

Do you want to request a feature or report a bug?

  • feature

What is the current behavior?
Code from Introducing Hooks:

import { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
// each time "count" changed, this arrow function will be created again.
// so that it can access the latest "count"
onClick={() => setCount(count + 1)}

I don't think it is good to create a fixed function many times, so I try to modify the code:
(Update on Jul 2022: No matter using the inline anonymous function or wrapping with useCallback, the function will always be created. The difference is that, in useCallback approach, the function reference will not be changed, which could be helpful if we use memo to wrap the component who receives the function as a property)

const [count, setCount] = useState(0);
const handleClick = useCallback(() => setCount(count + 1), []);

But obviously the callback in useCallback couldn't get the latest count because I pass in an empty inputs array to avoid this callback been generated again and again.

So, in fact, the inputs array decide two things:

  1. when to recreate the callback
  2. which state can be accessed in the callback

In most situation, the two things are one thing, but here they conflict.

So I think maybe it's good to add a get function to useState like this:

import { useState, useCallback } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount, getCount] = useState(0);

  const handleClick = useCallback(() => setCount(getCount() + 1), []);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>
        Click me
      </button>
    </div>
  );
}

Maybe it's confusing because getCount can totally replace count, but it brings the possible to avoid creating callbacks again and again.

Edited

#14543 (comment) exactly resolves the case above. But there‘re many other scenarios can't use updater to resolve. Here are some more code snippets:

1. Access states in a timer.

useEffect(() => {
  // or setInterval
  const id = setTimeout(() => {
    // access states
  }, period);
  return () => clearTimeout(id);
}, inputs);

2. Access states in WebSocket callbacks

useEffect(() => {
  // create a WebSocket client named "ws"
  ws.onopen = () => {
    // access states
  };
  ws.onmessage = () => {
    // access states
  };
  return () => ws.close();
}, inputs);

3. Access states in Promise

useEffect(() => {
  create_a_promise().then(() => {
    // access states
  });
}, inputs);

4. Access states in event callbacks

useEffect(() => {
  function handleThatEvent() {
    // access states
  }
  instance.addEventListener('eventName', handleThatEvent);
  return instance.removeEventListener('eventName', handleThatEvent);
}, inputs);

We had to use some workaround patterns to resolve those cases, like
#14543 (comment)
#14543 (comment)
#14543 (comment)
Or a funny way:

const [state, setState] = useState();
useEffect(() => {
  // or setInterval
  const id = setTimeout(() => {
    // access states
    setState((prevState) => {
      // Now I can do anything with state...🤮
      ...
      return prevState;
    });
  }, period);
  return () => clearTimeout(id);
}, inputs);

So let's discuss and wait...
#14543 (comment)

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

What is the expected behavior?

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

  • React 16.7.0-alpha.2
@saranchonkau
Copy link

saranchonkau commented Jan 8, 2019

@liyuanqiu you can use updater function in setCount

const handleClick = useCallback(() => setCount(prevCount => prevCount + 1), []);

useState API reference

@liyuanqiu
Copy link
Author

liyuanqiu commented Jan 8, 2019

@liyuanqiu you can use updater function in setCount

const handleClick = useCallback(() => setCount(prevCount => prevCount + 1), []);

useState API reference

@Saranchenkov Thank you very much, it's all my fault haven't read the document carefully.

But I have another question. As I said before:

So, in fact, the inputs array decide two things:

  1. when to recreate the callback
  2. which state can be accessed in the callback

Sometimes I want to do some side effect in hooks like useEffect, for example:

const [count, setCount] = useState(0);
useEffect(() => {
  // send count to server every 5 seconds
  const id = setInterval(() => {
    xhr(count);
  }, 5000);
  return () => clearInterval(id);
}, []);

If I pass [count] to useEffect, the interval will be cleared and recreated.
If I pass [] to useEffect, I can not get the latest count.

In this situation, maybe a get function is needed?

@liyuanqiu liyuanqiu reopened this Jan 8, 2019
@escaton
Copy link

escaton commented Jan 9, 2019

You could probably do something like this

const [count, setCount] = useState(0);
const countRef = useRef(count)
useEffect(() => {
  countRef.current = count
}, [count])
useEffect(() => {
  // send count to server every 5 seconds
  const id = setInterval(() => {
    xhr(countRef.current);
  }, 5000);
  return () => clearInterval(id);
}, []);

@liyuanqiu
Copy link
Author

You could probably do something like this

const [count, setCount] = useState(0);
const countRef = useRef(count)
useEffect(() => {
  countRef.current = count
}, [count])
useEffect(() => {
  // send count to server every 5 seconds
  const id = setInterval(() => {
    xhr(countRef.current);
  }, 5000);
  return () => clearInterval(id);
}, []);

Thank you @escaton , useRef really can solve this problem.

And the official document thinks this is convoluted but bearable:

This is a rather convoluted pattern but it shows that you can do this escape hatch optimization if you need it. It’s more bearable if you extract it to a custom Hook

But I think it is more like a workaround, couldn't be a paradigm.

One component may have many states. Using three kinds of hooks and five lines of code(or using a custom hook to replace useState) to define an internal state will be a disaster.

@ignatiusreza
Copy link

I really like the suggestion for having pair of getter and setter returned from useState.. which would make it easier to keep things fresh.. if it doesn't end up in the official implementation, I think it can be implemented in user land using custom hook like:

const useGetterState = (initialState) => {
  const [state, setState] = useState(initialState);
  const stateRef = useRef(state);
  useEffect(() => {
    stateRef.current = state
  }, [state]);

  return [() => stateRef.current, setState];
}

?

@escaton
Copy link

escaton commented Jan 9, 2019

@liyuanqiu
I see such options:

  • use ref for holding the whole callback rather then single state
  • create custom useStateWithRef() hook which could decorate the original useState and mirror value to ref.current
  • reimplement timer logic so it would be able to resume after state changing
const [count, setCount] = useState(0);
const timerAdjustment = useRef(0)
useEffect(() => {
  let id;
  let absoluteTimeout;
  function tick(firstTime) {
    // call xhr only on subsequent calls
    !firstTime && xhr(count)
    // schedule timer considering previous call
    const adjustment = timerAdjustment.current
    const timeout = adjustment > 0 ? adjustment : 5000
    // remember absolute time to calc adjusted timeout later
    absoluteTimeout = Date.now() + timeout
    // reset timer adjustment
    timerAdjustment.current = 0
    id = setTimeout(tick, timeout)
  }
  tick(true)
  return () => {
    clearTimeout(id)
    // set timer adjustment
    timerAdjustment.current = absoluteTimeout - Date.now()
  }
}, [count])

@ignatiusreza there is the problem with your solution:

[getCount, setCount] = useGetterState(0)
return (
  <button onClick={() => setCount(c => c+1)}>
    {getCount()} - increment
  </button>
)

on the first render in would be "0 - increment", but after click it would be still "0 - increment" and only on second click it will update. That's because you mutate the reference in useEffect which is fired after component renders.
And while it is fixable:

function useStateWithGetter(initial) {
  const [state, setState] = useState(initial)
  const ref = useRef(state)
  const updater = value => {
    if (typeof value === 'function') {
      setState(prev => {
        const result = value(prev);
        ref.current = result
        return result
      })
    } else {
      ref.current = value
      setState(value)
    }
  }
  const getter = () => ref.current
  return [state, updater, getter]
}

there are still issues, because now we referencing last scheduled state, not the current.

upd:
Hmm, what if...

function useStateWithRef(initial) {
  const [state, setState] = useState(initial)
  const ref = useRef()
  ref.current = state
  return [state, setState, ref]
}

@gaearon
Copy link
Collaborator

gaearon commented Jan 9, 2019

Just to set expectations, we’ve considered all these options a few months ago and decided against them at the time. I’ll keep this open so we can later provide a better response. I don't remember off the top of my mind what the problems were.

@btraljic
Copy link

I think this is an elegant solution: https://codesandbox.io/s/m1y7vl0vp

function App() {
  const [count, setCount] = useState(0);

  // ***** Initialize countRef.current with count
  const countRef = useRef(count);

  const handleClick = useCallback(() => setCount(add1), []);

  useEffect(() => {
    // ***** countRef.current is xhr function argument
    const intervalId = setInterval(() => xhr(countRef.current), 5000);
    return () => clearInterval(intervalId);
  }, []);

  // ***** Set countRef.current to current count
  countRef.current = count;

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

@liyuanqiu
Copy link
Author

@liyuanqiu
I see such options:

  • use ref for holding the whole callback rather then single state
  • create custom useStateWithRef() hook which could decorate the original useState and mirror value to ref.current
  • reimplement timer logic so it would be able to resume after state changing

@escaton Maybe only the second option is effective.


  • use ref for holding the whole callback rather then single state

Do you mean https://codesandbox.io/s/jj40lz07l3 ?

/* code snippets */
const [count, setCount] = useState(0);
const repeat = useRef(() => xhr(count));
useEffect(() => {
    const intervalId = setInterval(repeat.current, 1000);
    return () => clearInterval(intervalId);
}, []);

Not working too.

  • reimplement timer logic so it would be able to resume after state changing

It's not the timer's problem. When you use event dispatcher, WebSocket, ajax, promise, same problem. I think no one dares to reimplement them just for adopting React Hooks API.

@escaton
Copy link

escaton commented Jan 10, 2019

@btraljic yeah, i suggested the same thing

function useStateWithRef(initial) {
 const [state, setState] = useState(initial)
 const ref = useRef()
 ref.current = state
 return [state, setState, ref]
}

it should work, but it brings side effect countRef.current = count; in the component body rather then in useEffect and that confuses.

@liyuanqiu https://codesandbox.io/s/72jlzz1o86

+useEffect(
+    () => {
+      repeat.current = () => xhr(count);
+    },
+    [count]
+  );

-const intervalId = setInterval(repeat.current, 1000);
+const intervalId = setInterval(() => repeat.current(), 1000);

It's not the timer's problem. When you use event dispatcher, WebSocket, ajax, promise, same problem. I think no one dares to reimplement them just for adopting React Hooks API.

By the reimplementation i mean the proper restart of effect, not the setInterval itself. There is nothing wrong with it, you just want different behaviour.
Could you provide different example with WebSocket or promise?

@liyuanqiu
Copy link
Author

@btraljic Thank you.
I think this line of code been written in the function body is not encouraged by Hooks API:

countRef.current = count;

@see https://reactjs.org/docs/hooks-reference.html#useeffect

Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as React’s render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.

Any state change will lead this line been executed. Although in this example, there's no wrong.

If doing so, I think maybe it's better to put it in useLayoutEffect:

useLayoutEffect(() => {
  countRef.current = count;
});

And think deeper, here we rely on the layout update to update countRef. Actually, it does not rely on layout update, it relies on the change of state: count.

So finally, it turns back to:
#14543 (comment)

@btraljic
Copy link

Is it ok now :)
useEffect(() => countRef.current = count);

@escaton
Copy link

escaton commented Jan 10, 2019

@btraljic not really :)
now you have actual value in reference and can use it in next effects, but if once you decide to use it in markup, you would see lag between state and ref. Look at example in my answer to @ignatiusreza

@btraljic
Copy link

@escaton Ok, but we are living in an asynchronous world. Aren't we? :)

@escaton
Copy link

escaton commented Jan 10, 2019

Speaking about accessing state in setInterval — i came up with another idea.
It can be treated as two separate side effects: one is timer tick, another is xhr.
So it could be

const [count, setCount] = useState(0)
const [tick, setTick] = useState(0)

useEffect(() => {
  const timerId = setInterval(() => {
    setTick(t => t+1)
  }), 5000)
  return () => clearInterval(timerId)
}, [])

useEffect(() => {
  xhr(count)
}, [tick])

Pros: easy to understand what happens, no refs
Cons: wasteful rerenders on tick updates every 5 seconds

@TrySound
Copy link
Contributor

I usually use this way with reducer

const [count, setCount] = useState(0)
const [commitIndex, commit] = React.useReducer(state => state +1, 0)

useEffect(() => {
  const timerId = setInterval(commit, 5000)
  return () => clearInterval(timerId)
}, [])

useEffect(() => {
  xhr(count)
}, [commitIndex])

@Jokcy
Copy link

Jokcy commented Jan 10, 2019

If we can use reft to hold state, why not just use an global object?

const obj = {}
function Comp() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    xhr(obj.count)
  })

  obj.count = count
}

So obj may hold multi state for one component.

@escaton
Copy link

escaton commented Jan 10, 2019

Doing so, you can't use more than one Comp on the page, but it could be fixed

function CompFabric () {
  const obj = {}
  return function Comp() {
    const [count, setCount] = useState(0)
  
    useEffect(() => {
      xhr(obj.count)
    })
  
    obj.count = count
  }
}

Although it's almost same as useRef i strongly discourage you to use it. It is both non idiomatic and confuses other contributors.

@liyuanqiu
Copy link
Author

liyuanqiu commented Jan 10, 2019

Everybody here tells many solutions, but really like a sentence: "Life, Uh, Finds a Way."
Just a joke, no offense :)

Look back, my requirement is so easy, just want to repeatedly send a state to server. But with React Hooks API, it became strange and complex.

Let's see how to program in old class style:

https://codesandbox.io/s/40p9qqr009

class App extends React.Component {
  state = {
    count: 0,
  };

  handleClick = () => {
    const { count } = this.state;
    this.setState({
      count: count + 1,
    });
  };

  xhr = () => {
    const { count } = this.state;
    console.log(`Send ${count} to server.`);
    // TODO send count to my server by XMLHttpRequest
  };

  componentDidMount() {
    this.intervalId = setInterval(this.xhr, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.intervalId);
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <p>You clicked {count} times</p>
        <button onClick={this.handleClick}>Click me</button>
      </div>
    );
  }
}

Naturally and reasonable, right? No magic, no tricks.

I hope Hooks API will finally be so.

I complain a lot, don't diss me 🙏 XD

@escaton
Copy link

escaton commented Jan 10, 2019

Sure, it looks familiar.
The only difference with hooks here is that component's state is explicitly bounded to instance and so could be accessed with this anywhere.
To achieve the same in functional component it needs to break the rule and to bound state to ref right in the component body without useEffect wrapping.
Let's wait for @gaearon asnwer.

@escaton
Copy link

escaton commented Feb 4, 2019

@liyuanqiu
Copy link
Author

I guess this is it https://overreacted.io/making-setinterval-declarative-with-react-hooks/
:)

In this post, a pattern is introduced to capsulate those APIs who has “impedance mismatch” with the React programming model.

So we have to write helper functions(custom hooks) to help us using these APIs. That's still annoying.

Although Dan's useInterval brings many great features like dynamic delay and pause and resume to setInterval, but that's not the first motivation to write useInterval. Those great features are just derivatives.

We may encounter many APIs that has “impedance mismatch” with the React programming model. Capsulating them one by one just like @types/xxx in Typescript is hard. Maybe I should create an organization named DefinitelyHooked 😀.

This is an early time for Hooks, and there are definitely still patterns we need to work out and compare. Don’t rush to adopt Hooks if you’re used to following well-known “best practices”. There’s still a lot to try and discover.

@joepuzzo
Copy link

joepuzzo commented Feb 7, 2019

Ok so it seems like the solution to this is this:??

function useStateWithRef(initial) {
  const ref = useRef();
  const [state, setState] = useState(initial);
  ref.current = state;
  useEffect( () =>{
    ref.current = state;
  });
  return [state, setState, ref];
}

I def see the value in having a getter on hooks.

@joepuzzo
Copy link

joepuzzo commented Feb 7, 2019

On second thought.. why not just do this??

function useStateWithGetter(initial) {
  const ref = useRef();
  const [state, setState] = useState(initial);
  ref.current = state;
  const set = (value) => {
    ref.current = value;
    setState(value);
  };
  const get = () => {
    return ref.current;
  };
  return [state, set, get];
}

@murrayee
Copy link

mark

@joepuzzo
Copy link

@gaearon You mentioned this was discussed previously and left it open. Can we start this conversation back up? Or at the very least, verify that the solution above is an ok practice.

@liyuanqiu
Copy link
Author

https://overreacted.io/how-are-function-components-different-from-classes/
This article may help to understand the behavior of Hooks API(actually is Functional Component).
It's highly recommended to spend half an hour to read it.

@joepuzzo
Copy link

joepuzzo commented Mar 4, 2019

Just read that article... it basically reassures the solution above would work for adding a getter.

@danielkcz
Copy link

A Complete Guide to useEffect touches this subject quite a lot too. Really recommend reading it through, it was an eye opener for me.

@genshinw
Copy link

genshinw commented Jan 20, 2022

Until now, Do we still have to do use ref as escape hatch to resolve those problem which have “impedance mismatch” with the React programming model? @gaearon

@johnnysprinkles
Copy link

johnnysprinkles commented Jan 21, 2022

@joepuzzo I like the version with no useEffect! But I don't think your code snippet handles the setter being called with a function. Not hard to update it, looking at how React itself does it in

return typeof action === 'function' ? action(state) : action;

function useStateWithGetter(initial) {
  const ref = useRef(initial);
  const [state, setState] = useState(initial);
  const set = (valueOrUpdater) => {
    if (typeof valueOrUpdater === 'function') {
      setState((prev) => {
        ref.current = valueOrUpdater(prev);
        return ref.current;
      })
    } else {
      ref.current = value;
      setState(value);
    }
  };
  const get = () => {
    return ref.current;
  };
  return [state, set, get];
}

@joepuzzo
Copy link

Yup That would do the trick @johnnysprinkles ! I have done that in the past as well. For anyone here that is saying that a getter is not a good pattern I disagree. I have been developing an OS form library for years and this pattern becomes valuable. The key is some things in JS might have a reference to a value in state and they cant use the outdated version. You either pass around a ref or you use a getter.

@genshinw
Copy link

@joepuzzo I like the version with no useEffect! But I don't think your code snippet handles the setter being called with a function. Not hard to update it, looking at how React itself does it in

return typeof action === 'function' ? action(state) : action;

function useStateWithGetter(initial) {
  const ref = useRef(initial);
  const [state, setState] = useState(initial);
  const set = (valueOrUpdater) => {
    if (typeof valueOrUpdater === 'function') {
      setState((prev) => {
        ref.current = valueOrUpdater(prev);
        return ref.current;
      })
    } else {
      ref.current = value;
      setState(value);
    }
  };
  const get = () => {
    return ref.current;
  };
  return [state, set, get];
}

I think set ref.current when setState will cause some issue in concurrent mode.

@M4TH76
Copy link

M4TH76 commented Oct 21, 2023

Just for discussion, with setter, getter:

const useStateX = (init) =>{
	const [state, setState] = useState(init)
	const stateRef = useRef(state)
	stateRef.current = state

	const object = {
		set v(value) {
			setState(value)
		},
		get v(){
			return stateRef.current
		}
	}

	return object
}


export default () =>{
	let count = useStateX(1)

	

	return <button onClick={()=> count.v++ }>{ count.v }</button>
}

or with a Proxy:

import { useRef, useState } from "react"

export const useStates = (init) =>{
	const [state, setState] = useState(init)
	const stateRef = useRef(state)
	stateRef.current = state


	const proxy = new Proxy( {}, {
		set:(Void, prop, value) =>{
			setState({...stateRef.current, [prop]:value})
			return true
		}
		,
		get:(Void, prop) => stateRef.current?.[prop]
	})

	return proxy
}


export default () =>{
	let appState = useStates({count:1})

	

	return <button onClick={()=>appState.count++}>{appState.count}</button>
}

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

No branches or pull requests