Sometimes I have to use some native js libaray api, So I may have a component like this:
function App() {
const [state, setState] = useState(0)
useEffect(() => {
const container = document.querySelector('#container')
const h1 = document.createElement('h1')
h1.innerHTML = 'h1h1h1h1h1h1'
container.append(h1)
h1.onclick = () => {
console.log(state)
}
}, [])
return (
<div>
<button onClick={() => setState(state => state + 1)}>{state}</button>
<div id="container"></div>
</div>
)
}
Above is a simple example. I should init the lib after react is mounted, and bind some event handlers. And the problem is coming here: As the above shown, if I use useEffect() without state as the item in dependencies array, the value state in handler of onclick may never change. But if I add state to dependencies array, the effect function will execute every time once state changed. Above is a easy example, but the initialization of real library may be very expensive, so that way is out of the question.
Now I find 3 ways to reslove this, but none of them satisfy me.
Create a ref to keep state, and add a effect to change it current every time once state changed. (A extra variable and effect)
Like the first, but define a variable out of the function instead of a ref. (Some as the first)
Use class component. (Too many this)
So is there some resolutions that solve problems and makes code better?
I think you've summarised the options pretty well. There's only one option i'd like to add, which is that you could split your code up into one effect that initializes, and one effect that just changes the onclick. The initialization logic can run just once, and the onclick can run every render:
const [state, setState] = useState(0)
const h1Ref = useRef();
useEffect(() => {
const container = document.querySelector('#container')
const h1 = document.createElement('h1')
h1Ref.current = h1;
// Do expensive initialization logic here
}, [])
useEffect(() => {
// If you don't want to use a ref, you could also have the second effect query the dom to find the h1
h1ref.current.onClick = () => {
console.log(state);
}
}, [state]);
Also, you can simplify your option #1 a bit. You don't need to create a useEffect to change ref.current, you can just do that in the body of the component:
const [state, setState] = useState(0);
const ref = useRef();
ref.current = state;
useEffect(() => {
const container = document.querySelector('#container');
// ...
h1.onClick = () => {
console.log(ref.current);
}
}, []);
I'll show below the simplified version of the SSE part of my React project.
For some reason, I am not being able to change the state of a variable inside the EventSource.onmessage function.
function App() {
const [variable, setVariable] = useState("qwerty");
const mockFunc = () => {
let eventSource = new EventSource("http://localhost:5000/stream");
eventSource.onmessage = function (e) {
console.log("variable:", variable);
setVariable("abc");
};
};
useEffect(() => {
console.log("UseEffect: ",variable);
}, [variable]);
return (
<div>
<button onClick={mockFunc}>updateVariable</button>
</div>
);
}
export default App;
From the Flask side I am sending a message every 5 seconds and successfully receiving it on the React side. But, for some reason, setVariable is not working properly, since the value of variable is not actually being set. Here are the resulting logs from the program.
variable: qwerty
UseEffect: abc
variable: qwerty
variable: qwerty
variable: qwerty
.... indefinetely
Which means, setVariable not only does not trigger useEffect more than once, it actually resets the value to the original setState (?).
This does not make any sense to me. If I make a button that changes the variable value with setVariable it works fine, so I don't get why it's not working this way.
Thanks in advance
It's a stale enclosure over the variable state value in the onmessage callback handler.
You could use a React ref and an useEffect hook to cache the variable state value and access the cached value in the onmessage callback.
Example:
function App() {
const [variable, setVariable] = useState("qwerty");
const variableRef = useRef();
useEffect(() => {
variableRef.current = variable;
}, [variable]);
const mockFunc = () => {
let eventSource = new EventSource("http://localhost:5000/stream");
eventSource.onmessage = function (e) {
console.log("variable:", variableRef.current);
setVariable("abc");
};
};
useEffect(() => {
console.log("UseEffect: ", variable);
}, [variable]);
return (
<div className="App">
<button onClick={mockFunc}>updateVariable</button>
</div>
);
}
Log output:
Here i have a example hooks
const useGPS = () => {
const [gps, setGps] = useState({})
useEffect(() => {
setGps({ a: 1 })
}, [])
return [gps]
}
it's pretty simple, but when i use it inside another Component
const Foo = () => {
const location = useGPS()
useEffect(() => {
console.log(location);
}, [])
return (
<div>1</div>
)
}
console always log empty object for first time. Can someone explain what happened and how can i fix it? Sorry about my bad english. Thanks a lot!
To add to Tushar's answer, if you want to fix the behaviour without leaving useEffect running on every update (which can cause bugs in some more complex examples), you can add location to useEffect's dependencies:
const Foo = () => {
const location = useGPS();
useEffect(() => {
console.log(location);
}, [location]);
return <div>1</div>;
};
That way the effect will run only when a new value for location has been generated. You'll still see an empty object the very first time you call console.log, but it will be immediately updated to the generated value.
The value of GPS is only set after the first useEffect is run (inside the custom hook). It is initially empty and when the useEffect(foo component) runs, that empty value is shown.
The value is set successfully, and you can check this if you remove the [] array from the Foo component's useEffect. [] means that it will only run once after mounting, acting as componentDidMount.
export default function App() {
const location = useGPS()
useEffect(() => {
console.log(location);
});
return (
<div>1</div>
)
}
const [location] = useGPS();
you need to destructor location state array
I am using React context to pass down state. When my state changes, it also changes in the child Component (console logs shows new value), but when this state is used in a function, it doesnt update there (console.log shows old value).
Do I need to rerender the function? How?
const {user, userInfo, ref} = useSession(); <-- wrapper for useContext
console.log(userInfo); <--- correct, updated value
const haalDataOp = async () => {
console.log(userInfo.enelogic); <--- old value displaying
...
}
I am using the function haalDataOp from a button (onClick)
As someone already mentioned, I could use useRef, but I dont understand why. Why does this simple example work (extracted from https://dev.to/anpos231/react-hooks-the-closure-hell-71m), and my code doesnt:
const [value, setValue] = useState(1);
const handleClick = useCallback(
() => {
setValue(value + 1)
},
[value],
);
I also tried using useCallback (with userInfo in the dep array) in my example but that doesnt do the trick.
const ... userInfo ... is a constant, so in a Component like following:
console.log('render', userInfo.enelogic) // different value in each render
const haalDataOp = async () => {
console.log('before', userInfo.enelogic) // correct old value
await update()
console.log('after', userInfo.enelogic) // still the same old value
}
return <button onClick={haalDataOp} />
...it would log:
render old
before old
after old
render new
...because the userInfo inside haalDataOp is a closure referencing to the value from the original render. If you need to access a mutable reference that would point to the up-to-date value from a future render instead, you can useRef:
const userInfoRef = useRef()
userInfoRef.current = userInfo
console.log('render', userInfo.enelogic) // different value in each render
const haalDataOp = async () => {
console.log('before', userInfoRef.current.enelogic) // old value
await update()
console.log('after', userInfoRef.current.enelogic) // should be new value
}
return <button onClick={haalDataOp} />
However, there might be a race condition and/or the execution of the 'after' code happens deterministically BEFORE the next render, in which case you will need to use some other trick...
I suspect that the const {ref} = useSession() is needed for exactly this situation, so please read the documentation.
I am reading about React useState() and useRef() at "Hooks FAQ" and I got confused about some of the use cases that seem to have a solution with useRef and useState at the same time, and I'm not sure which way it the right way.
From the "Hooks FAQ" about useRef():
"The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class."
With useRef():
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
}
With useState():
function Timer() {
const [intervalId, setIntervalId] = useState(null);
useEffect(() => {
const id = setInterval(() => {
// ...
});
setIntervalId(id);
return () => {
clearInterval(intervalId);
};
});
// ...
}
Both examples will have the same result, but which one it better - and why?
The main difference between both is :
useState causes re-render, useRef does not.
The common between them is, both useState and useRef can remember their data after re-renders. So if your variable is something that decides a view layer render, go with useState. Else use useRef
I would suggest reading this article.
useRef is useful when you want to track value change, but don't want to trigger re-render or useEffect by it.
Most use case is when you have a function that depends on value, but the value needs to be updated by the function result itself.
For example, let's assume you want to paginate some API result:
const [filter, setFilter] = useState({});
const [rows, setRows] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const fetchData = useCallback(async () => {
const nextPage = currentPage + 1;
const response = await fetchApi({...filter, page: nextPage});
setRows(response.data);
if (response.data.length) {
setCurrentPage(nextPage);
}
}, [filter, currentPage]);
fetchData is using currentPage state, but it needs to update currentPage after successful response. This is inevitable process, but it is prone to cause infinite loop aka Maximum update depth exceeded error in React. For example, if you want to fetch rows when component is loaded, you want to do something like this:
useEffect(() => {
fetchData();
}, [fetchData]);
This is buggy because we use state and update it in the same function.
We want to track currentPage but don't want to trigger useCallback or useEffect by its change.
We can solve this problem easily with useRef:
const currentPageRef = useRef(0);
const fetchData = useCallback(async () => {
const nextPage = currentPageRef.current + 1;
const response = await fetchApi({...filter, page: nextPage});
setRows(response.data);
if (response.data.length) {
currentPageRef.current = nextPage;
}
}, [filter]);
We can remove currentPage dependency from useCallback deps array with the help of useRef, so our component is saved from infinite loop.
The main difference between useState and useRef are -
The value of the reference is persisted (stays the same) between component re-rendering,
Updating a reference using useRefdoesn't trigger component re-rendering.
However, updating a state causes component re-rendering
The reference update is synchronous, the updated referenced value is immediately available, but the state update is asynchronous - the value is updated after re-rendering.
To view using codes:
import { useState } from 'react';
function LogButtonClicks() {
const [count, setCount] = useState(0);
const handle = () => {
const updatedCount = count + 1;
console.log(`Clicked ${updatedCount} times`);
setCount(updatedCount);
};
console.log('I rendered!');
return <button onClick={handle}>Click me</button>;
}
Each time you click the button, it will show I rendered!
However, with useRef
import { useRef } from 'react';
function LogButtonClicks() {
const countRef = useRef(0);
const handle = () => {
countRef.current++;
console.log(`Clicked ${countRef.current} times`);
};
console.log('I rendered!');
return <button onClick={handle}>Click me</button>;
}
I am rendered will be console logged just once.
Basically, We use UseState in those cases, in which the value of state should be updated with re-rendering.
when you want your information persists for the lifetime of the component you will go with UseRef because it's just not for work with re-rendering.
If you store the interval id, the only thing you can do is end the interval. What's better is to store the state timerActive, so you can stop/start the timer when needed.
function Timer() {
const [timerActive, setTimerActive] = useState(true);
useEffect(() => {
if (!timerActive) return;
const id = setInterval(() => {
// ...
});
return () => {
clearInterval(intervalId);
};
}, [timerActive]);
// ...
}
If you want the callback to change on every render, you can use a ref to update an inner callback on each render.
function Timer() {
const [timerActive, setTimerActive] = useState(true);
const callbackRef = useRef();
useEffect(() => {
callbackRef.current = () => {
// Will always be up to date
};
});
useEffect(() => {
if (!timerActive) return;
const id = setInterval(() => {
callbackRef.current()
});
return () => {
clearInterval(intervalId);
};
}, [timerActive]);
// ...
}
Counter App to see useRef does not rerender
If you create a simple counter app using useRef to store the state:
import { useRef } from "react";
const App = () => {
const count = useRef(0);
return (
<div>
<h2>count: {count.current}</h2>
<button
onClick={() => {
count.current = count.current + 1;
console.log(count.current);
}}
>
increase count
</button>
</div>
);
};
If you click on the button, <h2>count: {count.current}</h2> this value will not change because component is NOT RE-RENDERING. If you check the console console.log(count.current), you will see that value is actually increasing but since the component is not rerendering, UI does not get updated.
If you set the state with useState, clicking on the button would rerender the component so UI would get updated.
Prevent unnecessary re-renderings while typing into input.
Rerendering is an expensive operation. In some cases, you do not want to keep rerendering the app. For example, when you store the input value in the state to create a controlled component. In this case for each keystroke, you would rerender the app. If you use the ref to get a reference to the DOM element, with useState you would rerender the component only once:
import { useState, useRef } from "react";
const App = () => {
const [value, setValue] = useState("");
const valueRef = useRef();
const handleClick = () => {
console.log(valueRef);
setValue(valueRef.current.value);
};
return (
<div>
<h4>Input Value: {value}</h4>
<input ref={valueRef} />
<button onClick={handleClick}>click</button>
</div>
);
};
Prevent the infinite loop inside useEffect
to create a simple flipping animation, we need to 2 state values. one is a boolean value to flip or not in an interval, another one is to clear the subscription when we leave the component:
const [isFlipping, setIsFlipping] = useState(false);
let flipInterval = useRef<ReturnType<typeof setInterval>>();
useEffect(() => {
startAnimation();
return () => flipInterval.current && clearInterval(flipInterval.current);
}, []);
const startAnimation = () => {
flipInterval.current = setInterval(() => {
setIsFlipping((prevFlipping) => !prevFlipping);
}, 10000);
};
setInterval returns an id and we pass it to clearInterval to end the subscription when we leave the component. flipInterval.current is either null or this id. If we did not use ref here, everytime we switched from null to id or from id to null, this component would rerender and this would create an infinite loop.
If you do not need to update UI, use useRef to store state variables.
Let's say in react native app, we set the sound for certain actions which have no effect on UI. For one state variable it might not be that much performance savings but If you play a game and you need to set different sound based on game status.
const popSoundRef = useRef<Audio.Sound | null>(null);
const pop2SoundRef = useRef<Audio.Sound | null>(null);
const winSoundRef = useRef<Audio.Sound | null>(null);
const lossSoundRef = useRef<Audio.Sound | null>(null);
const drawSoundRef = useRef<Audio.Sound | null>(null);
If I used useState, I would keep rerendering every time I change a state value.
You can also use useRef to ref a dom element (default HTML attribute)
eg: assigning a button to focus on the input field.
whereas useState only updates the value and re-renders the component.
It really depends mostly on what you are using the timer for, which is not clear since you didn't show what the component renders.
If you want to show the value of your timer in the rendering of your component, you need to use useState. Otherwise, the changing value of your ref will not cause a re-render and the timer will not update on the screen.
If something else must happen which should change the UI visually at each tick of the timer, you use useState and either put the timer variable in the dependency array of a useEffect hook (where you do whatever is needed for the UI updates), or do your logic in the render method (component return value) based on the timer value.
SetState calls will cause a re-render and then call your useEffect hooks (depending on the dependency array).
With a ref, no updates will happen, and no useEffect will be called.
If you only want to use the timer internally, you could use useRef instead. Whenever something must happen which should cause a re-render (ie. after a certain time has passed), you could then call another state variable with setState from within your setInterval callback. This will then cause the component to re-render.
Using refs for local state should be done only when really necessary (ie. in case of a flow or performance issue) as it doesn't follow "the React way".
useRef() only updates the value not re-render your UI if you want to re-render UI then you have to use useState() instead of useRe. let me know if any correction needed.
As noted in many different places useState updates trigger a render of the component while useRef updates do not.
For the most part having a few guiding principles would help:.
for useState
anything used with input / TextInput should have a state that gets updated with the value that you are setting.
when you need a trigger to recompute values that are in useMemo or trigger effects using useEffect
when you need data that would be consumed by a render that is only available after an async operation done on a useEffect or other event handler. E.g. FlatList data that would need to be provided.
for useRef
use these to store data that would not be visible to the user such as event subscribers.
for contexts or custom hooks, use this to pass props that are updated by useMemo or useEffect that are triggered by useState/useReducer. The mistake I tend to make is placing something like authState as a state and then when I update that it triggers a whole rerender when that state is actually the final result of a chain.
when you need to pass a ref
The difference is that useState returns the current state and has an updater function that updates the state. While useRef returns an object, doesn’t cause components to re-render, and it’s used to reference DOM elements.
Therefore,
If you want to have state in your components, which triggers a rerendered view when changed, useState or useReducer. Go with useRef if you don't want state to trigger a render.
look at this example,
import { useEffect, useRef } from "react";
import { Form } from "./FormStyle";
const ExampleDemoUseRef = () => {
const emailRef = useRef("");
const passwordRef = useRef("");
useEffect(() => {
emailRef.current.focus();
}, []);
useEffect(() => {
console.log("render everytime.");
});
const handleSubmit = (event) => {
event.preventDefault();
const email = emailRef.current.value;
const password = passwordRef.current.value;
console.log({ email, password });
};
return (
<div>
<h1>useRef</h1>
<Form onSubmit={handleSubmit}>
<label htmlFor="email">Email: </label>
<input type="email" name="email" ref={emailRef} />
<label htmlFor="password">Password: </label>
<input type="password" name="password" ref={passwordRef} />
<button>Submit</button>
</Form>
</div>
);
};
export default ExampleDemoUseRef;
and this useState example,
import { useEffect, useState, useRef } from "react";
import { Form } from "./FormStyle";
const ExampleDemoUseState = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const emailRef = useRef("");
useEffect(() => {
console.log("render everytime.");
});
useEffect(() => {
emailRef.current.focus();
}, []);
const onChange = (e) => {
const { type, value } = e.target;
switch (type) {
case "email":
setEmail(value);
break;
case "password":
setPassword(value);
break;
default:
break;
}
};
const handleSubmit = (event) => {
event.preventDefault();
console.log({ email, password });
};
return (
<div>
<h1>useState</h1>
<Form onSubmit={handleSubmit}>
<label htmlFor="email">Email: </label>
<input type="email" name="email" onChange={onChange} ref={emailRef} />
<label htmlFor="password">Password: </label>
<input type="password" name="password" onChange={onChange} />
<button>Submit</button>
</Form>
</div>
);
};
export default ExampleDemoUseState;
so basically,
UseRef is an alternative to useState if you do not want to update DOM elements and want to get a value (having a state in component).