user7148391
user7148391

Reputation:

react can't access state from document/window keypress

Given the example below

#app div {
  padding: 10px;
  border: 1px solid;
  margin: 10px 0;
}

div:focus {
  background: red;
}

div:focus:before {
  content: "focused";
  display: block;
}
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/babel.min.js"></script>

<script type="text/babel">
  let { useState, useRef, Fragment } = React; 
  let App = () => { 
    let [, forceUpdate] = useState(); 
    let [num, setNum] = useState(0); 
    let history = useRef('null'); 
    
    let keyPressHandler = () => { 
    history.current = num; 
    
    // just to force an update 
    forceUpdate(Date.now());
  }; 
  
  return (
  <Fragment>
    <button onClick={()=>{setNum(Date.now())}}>update state</button>
    <div tabindex="0" onKeyPress={keyPressHandler}>this div has a keypress listener. <br/>for now whatever key you press it will save the state in a ref, that changes on the button click. <br/>Click the button to change the state then focus this div then press any key to save the state in the ref</div>
    <div>State&nbsp;&nbsp;&nbsp;&nbsp;: {num}</div>
    <div>History: {history.current}</div>
  </Fragment>
  ); 
}; 

ReactDOM.render(<App />, document.querySelector("#app"));
</script>

Now i'm going to move the key press listener to the window, should do the same thing.

#app div {
  padding: 10px;
  border: 1px solid;
  margin: 10px 0;
}
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/babel.min.js"></script>

<script type="text/babel">
  let { useEffect, useState, useRef, Fragment } = React; 
  let App = () => { 
    let [, forceUpdate] = useState(); 
    let [num, setNum] = useState(0); 
    let history = useRef('null'); 
    
    useEffect(() => {
		  window.addEventListener("keypress", keyPressHandler);
	  }, []);
    
    let keyPressHandler = () => { 
    console.log('called keyPressHandler')
    history.current = num; 
    
    // just to force an update 
    forceUpdate(Date.now());
  }; 
  
  return (
  <Fragment>
    <button onClick={()=>{setNum(Date.now())}}>update state</button>
    <div>State&nbsp;&nbsp;&nbsp;&nbsp;: {num}</div>
    <div>History: {history.current}</div>
  </Fragment>
  ); 
}; 

ReactDOM.render(<App />, document.querySelector("#app"));
</script>

But it doesn't num(state) is always the initial value.

Upvotes: 0

Views: 388

Answers (2)

saurabh
saurabh

Reputation: 2713

The reason is that when the window rerender only accesses the val variable in the first render, it doesn't have access to the new val in the subsequent render.

state hooks also have a callback, in which the current state is passed in.

In this case, using a callback to read the latest state value and to ensure that you have the latest state value before add it will solve the problem.

example:

<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>

<script src="https://unpkg.com/[email protected]/babel.min.js"></script>


<script type="text/babel">
let { useEffect, useState } = React;
let App = () => {

	let [val, setVal] = useState('initial Value')
  
  useEffect(() => {
		window.addEventListener("keypress", keyPressHandler);
		// document.addEventListener("keypress", keyPressHandler);
	}, []);
  
  let keyPressHandler = ({ key }) => {
    console.log(val)
		setVal(val => val + "-" + key); /*<- change this line*/
	};
  
	return (
		<div tabindex="0" >
			<h1>{val}</h1>
		</div>
	);
};
ReactDOM.render(<App />, document.querySelector("#app"));

</script>

UPDATED

As for the question asked by Zohir Salak in the comment. I will recommend reading this blog by Dan Abramov. link

EDIT

As the question got updated

the best way to do that will be using the a state to store the history value

if you want to use useref then the way I can think of is

<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/[email protected]/babel.min.js"></script>

<script type="text/babel">
  let { useEffect, useState, useRef, Fragment } = React; 
  let App = () => { 
    let [,ForceUpdate] = useState(); 
    let [keyUpdate, setKeyUpdate] = useState(0); 
    let [num, setNum] = useState(0); 
    let history = useRef('null'); 
    
    useEffect(() => {
		  window.addEventListener("keypress", keyPressHandler);
	  }, []);
    
    let keyPressHandler = () => { 
    console.log('called keyPressHandler');
    setKeyUpdate(Date.now());
  }; 
    
  useEffect(() => {history.current = num; ForceUpdate(Date.now())  }, [keyUpdate]);
    
  
    
  return (
  <Fragment>
    <button onClick={()=>{setNum(Date.now())}}>update state</button>
    <div>State&nbsp;&nbsp;&nbsp;&nbsp;: {num}</div>
    <div >History: {history.current}</div>
  </Fragment>
  ); 
}; 

ReactDOM.render(<App />, document.querySelector("#app"));
</script>

Upvotes: 1

Luca
Luca

Reputation: 4273

I think you have to register listener in each update. Alternative you can store the state in a service or in a variable or using useRef hook.

Demo

<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>

<script src="https://unpkg.com/[email protected]/babel.min.js"></script>


<script type="text/babel">
let { useEffect, useState } = React;
let App = () => {

	let [val, setVal] = useState('initial Value')
  
  useEffect(() => {
		window.addEventListener("keypress", keyPressHandler);
		// document.addEventListener("keypress", keyPressHandler);
        return () => {
          window.removeEventListener("keypress", keyPressHandler);
        }
	});
  
  let keyPressHandler = ({ key }) => {
    console.log(val)
		setVal(val + "-" + key);
	};
  
	return (
		<div tabindex="0" >
			<h1>{val}</h1>
		</div>
	);
};
ReactDOM.render(<App />, document.querySelector("#app"));

</script>

Upvotes: 0

Related Questions