Here is some code to illustrate what I am trying to accomplish:
import React, { useState } from "react";
export default function App() {
const [list, setList] = useState(["a", "b", "c"]);
const [text, setText] = useState("");
let listBefore;
const addItem = () => {
listBefore = list;
setList([text, ...list]);
//simulate api call "addNewItem(newItem, confirmAdd)"
setTimeout(() => confirmAdd(false), 2000);
};
const confirmAdd = success => {
if (!success) {
setList(listBefore);
}
};
return (
<div className="App">
{list.toString()}
<input onChange={e => setText(e.target.value)} />
<button onClick={addItem}>Add</button>
</div>
);
}
The goal is to immediately update the UI before I know that the new list item was successfully added to the database. In the off chance that it failed, I revert back using my listBefore variable. I have been trying to figure out why this works and stumbled across closure. My guess is that, even though many rerenders may occur before the request succeeds (and listBefore is recreated as undefined), listBefore is "remembered" inside confirmAdd() because it had the value at the time of being called.
So I am wondering:
Am I correct in understanding how closure is being applied?
Is this encouraged?
Yes your thought of closure is correct.
It works but it is not encouraged, because this is hard to understand, and tracking when reading or debugging code. You should use useRef to sotre beforeList in this case.
Related
I have an Input and when the user submits a value which does not exist a not found message is shown for example setError("Not found") in the Toast (Snackbar) which closes after timeout. I am using Material UI and i am using it as its shown in this example Consecutive Snackbars in the docs. Only difference i made is i put this in a seperate component and added useEffect like this
const Toast = ({message}) => {
React.useEffect(() => {
if(message) {
setSnackPack((prev) => [...prev, { message, key: new Date().getTime() }]);
}
}, [message])
... // Rest of the code from the docs
}
And this works when the error is set but if the same errors occurs (same value) it wont show the value (Snackbar)like in the example as it will not rerender because it has the same value.
My question is how would i cause the rerender so that the same string appears again, or is there a different way I could do something like this as I feel this is an anti-pattern?
I'm not 100% sure I understood your problem, but I suspect you might not be managing the Snackbar's open state properly. I suggest making sure that it's always set (or inherited from the parent) correctly (e.g. not just set once upon state initialization).
The code below allows displaying a Snackbar with identical parameters multiple times. See this codesandbox for a fully working example.
import { useState } from "react";
import Snackbar from "#material-ui/core/Snackbar";
export default function App() {
const [text, setText] = useState();
const [show, setShow] = useState(false);
return (
<>
<input onChange={(e) => setText(e.target.value)} />
<button onClick={() => setShow(true)}>Show it</button>
<Toast show={show} hide={() => setShow(false)} message={text} />
</>
);
}
const Toast = ({ message, show, hide }) => {
return (
<Snackbar
open={show}
autoHideDuration={1000}
onClose={hide}
message={message}
/>
);
};
You could change message to an object
let message = {
messageText: "A bad thing happened...",
timestamp: 1626698144204
}
Then in your handler, update the timestamp. Then the useEffect should fire when the timestamp gets updated.
I cannot understand the difference between useState and useEffect. Specifically, the difference between state and lifecycle methods. For instance, I have watched tutorials and seen this example for useEffect:
const UseEffectBasics = () => {
const [value, setVal] = useState(0);
const add = () => {
setVal((x) => { return x + 1 })
}
useEffect(() => {
if (value > 0) {
document.title = `Title: ${value}`
}
},[value])
return <>
<h1>{value}</h1>
<button className="btn" onClick={add}>add</button>
</>;
};
When we click the button, the title of the document shows us increasing numbers by one. When I removed the useEffect method and did this instead:
const UseEffectBasics = () => {
const [value, setVal] = useState(0);
document.title = `Title: ${value}`
const add = () => {
setVal((x) => { return x + 1 })
}
return <>
<h1>{value}</h1>
<button className="btn" onClick={add}>add</button>
</>;
};
It worked same as the previous code.
So, how does useEffect actually work? What is the purpose of this method?
P.S. Do not send me links of documentation or basic tutorials, please. I have done my research. I know what I am missing is very simple, but I do not know what is it or where to focus to solve it.
Using useEffect to track stateful variable changes is more efficient - it avoids unnecessary calls by only executing the code in the callback when the value changes, rather than on every render.
In the case of document.title, it doesn't really matter, since that's an inexpensive operation. But if it was runExpensiveFunction, then this approach:
const UseEffectBasics = () => {
const [value, setVal] = useState(0);
runExpensiveOperation(value);
would be problematic, since the expensive operation would run every time the component re-renders. Putting the code inside a useEffect with a [value] dependency array ensures it only runs when needed - when the value changes.
This is especially important when API calls that result from state changes are involved, which is pretty common. You don't want to call the API every time the component re-renders - you only want to call it when you need to request new data, so putting the API call in a useEffect is a better approach than putting the API call in the main component function body.
According to the React docs:
every value referenced inside the effect function should also appear in the dependencies array
If my effect function references a few variables from the outer scope but I only want it to be executed when one of them changes, why do I need to specify all the other variables in the dependency array? Yes, the closure will become stale if the other variables change but I don't care because I don't need the function to be called yet. When the variable that I care about changes then the new closure with the values at that moment can be called. What am I missing?
Here's a working example (to my knowledge) where the useEffect dependency arrays are not exhaustive:
import React, { useEffect, useState } from "react";
const allCars = {
toyota: ["camry", "corolla", "mirai"],
ford: ["mustang", "cortina", "model T"],
nissan: ["murano", "micra", "maxima"],
};
function CarList() {
const [cars, setCars] = useState([]);
const [brand, setBrand] = useState("toyota");
const [filterKey, setFilterKey] = useState("");
useEffect(() => {
// I don't want to run this effect when filterKey changes because I wanna wrap that case in a timeout to throttle it.
setCars(allCars[brand].filter(model => model.startsWith(filterKey)));
}, [brand]);
useEffect(() => {
// This effect is only called when filterKey changes but still picks up the current value of 'brand' at the time the function is called.
const timeoutId = setTimeout(() => {
setCars(allCars[brand].filter(model => model.startsWith(filterKey)));
}, 500);
return () => clearTimeout(timeoutId);
}, [filterKey]);
const handleChangeFilterKey = event => {
setFilterKey(event.target.value);
};
return (
<div>
{`${brand} cars`}
<div>Select brand</div>
<input type="radio" value="toyota" checked={brand === "toyota"} onChange={() => setBrand("toyota")} />
<input type="radio" value="ford" checked={brand === "ford"} onChange={() => setBrand("ford")} />
<input type="radio" value="nissan" checked={brand === "nissan"} onChange={() => setBrand("nissan")} />
<div>Filter</div>
<input label="search" value={filterKey} onChange={handleChangeFilterKey} />
<ul>
{cars.map(car => (
<li>{car}</li>
))}
</ul>
</div>
);
}
Are there any pitfalls to the above example?
Yes, you should follow this rule all the time, when you find your code break with following it, that means good practice is not followed. That is the meaning of this rule, make sure you design code well.
I imagine in your case the code looks like this:
const Test = () => {
const [wantToSync] = useState(0)
const [notWantToSync] = useState(0) // notWantToSync also might come from props, i'll use state as example here
useEffect(() => {
fetch('google.com', {
body: JSON.stringify({wantToSync, notWantToSync})
}).then(result => {
// ...
})
}, [wantToSync]) // component is supposed to be reactive to notWantToSync, missing notWantToSync in dep is dangerous
}
If notWantToSync was defined as a state of the component, the component is supposed to be reactive to it, including useEffect. If that is not what you want, notWantToSync shouldn't be state from start.
const Test = () => {
const [wantToSync] = useState(0)
const notWantToSyncRef = useRef(0) // hey I don't want notWantToSync to be reactive, so i put it in useRef
useEffect(() => {
fetch('google.com', {
body: JSON.stringify({wantToSync, notWantToSync: notWantToSyncRef.current})
}).then(result => {
// ...
})
}, [wantToSync, notWantToSyncRef]) // yeah, now eslint doesn't bother me, and notWantToSync doesn't trigger useEffect anymore
}
Normally you don't need to do if else in useEffect to stop re-rendering, there're other approaches like useMemo or useCallback to do similar things in addition to useRef although they have different using context.
I see your struggle in the new example, so you want to do a throttle, and if filterKey is added to the first useEffect dep, the throttle will be broken. My opinion is that when you find yourself in a situation like this, that often means there's better practice(eslint exhaustive help to identify it), for example, extract throttle logic to a hook: https://github.com/streamich/react-use/blob/master/src/useThrottle.ts.
Exhaustive deps is not something vital to follow, it's just a good practice to make sure your code is designed well. The above example proves it that
In your case there is no side-effect, it's just data derived from state, so you should use different tools instead of useEffect.
First filter could be implemented with just getting value by key:
const carsByBrand = allCars[brand];
Complexity of this operation is constant, so memoization will be too expensive.
And second filter is debounced, but you might want to memoize it because of filtering with linear complexity. For these purposes you can use for example useDebouncedMemo hook:
const filteredCars = useDebouncedMemo(() => {
return carsByBrand.filter(model => model.startsWith(filterKey)
}, [filterKey, carsByBrand], 500)
In this case dependencies of hook are exhaustive and logic is much more declarative.
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).
I'm having trouble deciding how to trigger an API call imperatively, for example, on a button click.
I'm unsure what is the proper approach with hooks, because there seems to be more than one method, but I don't understand which is the "best" approach and the eventual implications.
I've found the following examples that are simple enough and do what I want:
Using useEffect() with a trigger value
function SomeFunctionComponent() {
const [fakeData, setFakeData] = useState(0);
const [trigger, setTrigger] = useState(false);
async function fetchData() {
if (!trigger) return;
const newData = await someAPI.fetch();
setTrigger(false);
setFakeData(newData);
}
useEffect(() => {
fetchData();
}, [trigger]);
return (
<React.Fragment>
<p>{fakeData}</p>
<button onClick={() => setTrigger(!trigger)}>Refresh</button>
</React.Fragment>
);
}
Example
Just calling the API and then setState()
function SomeFunctionComponent() {
const [fakeData, setFakeData] = useState(0);
async function fetchData() {
const newData = await someAPI.fetch();
setFakeData(newData);
}
return (
<React.Fragment>
<p>{fakeData}</p>
<button onClick={fetchData}>Refresh</button>
</React.Fragment>
);
}
Example
There are also other approaches that leverage useCallback() but as far as I understood they are useful to avoid re-rendering child components when passing callbacks down and are equivalent to the second example.
I think that the useEffect approach is useful only when something has to run on component mount and programmatically, but having what essentially is a dummy value to trigger a side-effect looks verbose.
Just calling the function looks pragmatic and simple enough but I'm not sure if a function component is allowed to perform side-effects during render.
Which approach is the most idiomatic and correct to have imperative calls using hooks in React ?
The first thing I do when I try to figure out the best way to write something is to look at how I would like to use it. In your case this code:
<React.Fragment>
<p>{fakeData}</p>
<button onClick={fetchData}>Refresh</button>
</React.Fragment>
seems the most straightforward and simple. Something like <button onClick={() => setTrigger(!trigger)}>Refresh</button> hides your intention with details of the implementation.
As to your question remark that "I'm not sure if a function component is allowed to perform side-effects during render." , the function component isn't doing side-effects during render, since when you click on the button a render does not occur. Only when you call setFakeData does a render actually happen. There is no practical difference between implementation 1 and implementation 2 in this regard since in both only when you call setFakeData does a render occur.
When you start generalizing this further you'll probably want to change this implementation all together to something even more generic, something like:
function useApi(action,initial){
const [data,setData] = useState({
value:initial,
loading:false
});
async function doLoad(...args){
setData({
value:data.value,
loading:true
});
const res = await action(...args);
setData({
value:res,
loading:false
})
}
return [data.value,doLoad,data.loading]
}
function SomeFunctionComponent() {
const [data,doLoad,loading] = useApi(someAPI.fetch,0)
return <React.Fragment>
<p>{data}</p>
<button onClick={doLoad}>Refresh</button>
</React.Fragment>
}
The accepted answer does actually break the rules of hooks. As the click is Asynchronous, which means other renders might occur during the fetch call which would create SideEffects and possibly the dreaded Invalid Hook Call Warning.
We can fix it by checking if the component is mounted before calling setState() functions. Below is my solution, which is fairly easy to use.
Hook function
function useApi(actionAsync, initialResult) {
const [loading, setLoading] = React.useState(false);
const [result, setResult] = React.useState(initialResult);
const [fetchFlag, setFetchFlag] = React.useState(0);
React.useEffect(() => {
if (fetchFlag == 0) {
// Run only after triggerFetch is called
return;
}
let mounted = true;
setLoading(true);
actionAsync().then(res => {
if (mounted) {
// Only modify state if component is still mounted
setLoading(false);
setResult(res);
}
})
// Signal that compnoent has been 'cleaned up'
return () => mounted = false;
}, [fetchFlag])
function triggerFetch() {
// Set fetchFlag to indirectly trigger the useEffect above
setFetchFlag(Math.random());
}
return [result, triggerFetch, loading];
}
Usage in React Hooks
function MyComponent() {
async function fetchUsers() {
const data = await fetch("myapi").then((r) => r.json());
return data;
}
const [fetchResult, fetchTrigger, fetchLoading] = useApi(fetchUsers, null);
return (
<div>
<button onClick={fetchTrigger}>Refresh Users</button>
<p>{fetchLoading ? "Is Loading" : "Done"}</p>
<pre>{JSON.stringify(fetchResult)}</pre>
</div>
);
}