Why is "Ctrl P" (Print) causing UseEffect to be triggered? - reactjs

I'm working on a webpage that shows the details for a request, but when I hit "ctrl p" (or select print from the dropdown menu) on Microsoft Edge browser, it's causing the page to call the api for the request data again. Because I have a loading spinner showing while the fetch is happening, the content that would be printed is just the spinner rather than the request details. How do I stop this from happening?
I'm using React with Typescript and am using React Context for data management. I've already determined that none of the higher components are responsible for causing a re-render.
const ViewRequest: React.FC = () => {
const location = useLocation<ILocation>();
const [isOpen, setOpen] = useState(false);
const { state, actions } = useRequestContext();
const id = Number(querystring.parse(location.search, '?').Request_ID) ?? 0;
const user = getUser();
const handleClose = () => {
setOpen(false);
actions.dataGET(id);
};
useEffect(() => {
actions.dataGET(id);
// This calls the API for the data, and should only be called once after the component mounts. Why is Printing making this happen again??
}, []);
const status = getRequestStatus(state, actionTypes.REQUEST_FETCH_DATA);
if (!state.data && status.error) {
return <Error404 />;
}
if (!state.data || (status.fetching && !isOpen)) {
return <ATMLoader active>Loading Request</ATMLoader>;
}
return (
[TSX for component...]
)

Related

Previous data showing even though cleaning up in useEffect

I have a component in my react native app that loads sessions related to a particular individual. In the useEffect() of that component I both load the sessions when the component comes into focus, and unload those sessions within the cleanup.
export const ClientScreen = (props) => {
const isFocused = useIsFocused();
const client = useSelector((state) => selectActiveClient(state));
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
return () => dispatch(unloadSessions()); // Cleaning up here...
};
loadSessions(props);
}
}, [isFocused, client?.id]);
const updatedProps = {
...props,
client,
};
return <ClientBottomTabNavigator {...updatedProps} />;
};
Generally the component is working as expected. However, I do notice that if I load the component with one client, then navigate away, and then come back to the component by loading a new client, that for a brief moment the sessions pertaining to the previous client show before being replaced the sessions relevant to the new client.
My question is, shouldn't the unloadVisits() that runs on cleanup -- which sets sessions to an empty array -- prevent this? Or is this some kind of react behavior that's holding onto the previous state of the component? How can I ensure this behavior doesn't occur?
Cleanup function should appear before the closing-brace of the useEffect hook
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
};
loadSessions(props);
}
return () => dispatch(unloadSessions()); // Cleaning up here... // <--- here
}, [isFocused, client?.id]);
as commented, your loadSessions returns a cleanup function, but you don't do anything with it. And the effect where you call loadSessions(props) does not return anything, that's why it does not clean up.
Edit:
I made a mistake, loadSessions returns a Promise of a cleanup function. And it is impossible to "unwrap" this Promise and get to the cleanup function itself in a way that you can return it in your effect. You have to move the cleaup function out of the async function loadSessions.
But you don't need async/await for everything:
useEffect(() => {
if (isFocused && client?.id) {
loadClientSessions(client.id).then(dispatch);
return () => dispatch(unloadSessions());
}
}, [isFocused, client?.id]);

How to prevent set sate in unmounted component in React?

I received a warning: "Can't perform a React state update on an unmounted component", so I try to determine when my component is unmounted, like below:
function ListStock() {
let mounted = true;
const [data, setData] = useState([]);
const [search, setSearch] = useState();
useEffect(() => {
async function fetchData() {
const {start_date, end_date} = search;
const result = await getDataStock(start_date, end_date);
if (result && mounted) {
setData(result.data); // only set a state when mounted = true
}
}
fetchData();
return () => {
mounted = false; // set false on clean up
}
}, [search])
const handleSearch = () => {
...
setSearch({
start_date: moment().subtract(1, 'month').format('YYYY-MM-DD'),
end_date: moment().format('YYYY-MM-DD')
});
}
return (
<div>
<input type="text" id="keyword">
<input type="button" onlick={handleSearch} value="Search">
{data}
</div>
)
}
By this way, it can resolve that warning message, however it shows another one:
"Assignments to the 'mounted' variable from inside React Hook
useEffect will be lost after each render. To preserve the value over
time, store it in a useRef Hook and keep the mutable value in the
'.current' property. Otherwise, you can move this variable directly
inside useEffect"
When I store the 'mounted' variable in a useRef hook, I cannot search anymore, since the 'mounted' is always set to "false".
My questions are:
Why a clean up code runs when User click a search button? I though it runs only when a component is unmounted?
What is the right way to implement a searching job with a remote api?
Is it fine if I config ESLint to ignore all this kind of warning messages?
Thanks all.
The problem is that you are doing the whole mounted/unmounted thing wrong. Here is a proper implementation:
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []); // Notice lack of dependencies
Before I go on, I should probably refer you to the awesome react-use library, which already comes with a useMountedState hook
Now back to your questions
Why a clean up code runs when User click a search button? I though it
runs only when a component is unmounted?
I didn't realize this was a thing until I read the docs:
When exactly does React clean up an effect? React performs the cleanup
when the component unmounts. However, as we learned earlier, effects
run for every render and not just once. This is why React also cleans
up effects from the previous render before running the effects next
time...
So there you have it: The cleanup function is run after every render which happens after state changes, thus when search changes, a re-render is required.
What is the right way to implement a searching job with a remote api?
The way you are doing it is fine, but if you are going to be checking for unmounted state every time, you might as well use the library I mentioned.
Is it fine if I config ESLint to ignore all this kind of warning
messages?
Nah. Just fix it. It is very easy
Instead of using a variable, you need to store mounted = true; in a useRef hook. UseRef can hold values and it won't re-render the page when the value changes.
function ListStock() {
const mounted = useRef(true);
const [data, setData] = useState([]);
const [search, setSearch] = useState();
useEffect(() => {
async function fetchData() {
const {start_date, end_date} = search;
const result = await getDataStock(start_date, end_date);
if (result && mounted,current) {
setData(result.data); // only set a state when mounted = true
}
}
fetchData();
return () => {
mounted.current = false; // set false on clean up
}
}, [search])
const handleSearch = () => {
...
setSearch({
start_date: moment().subtract(1, 'month').format('YYYY-MM-DD'),
end_date: moment().format('YYYY-MM-DD')
});
}
return (
<div>
<input type="text" id="keyword">
<input type="button" onlick={handleSearch} value="Search">
{data}
</div>
)
}
Hopefully, questions 1 and 2 will be solved by the above code. 3rd question, I would say it's better to keep it as it shows what's going wrong.
in this case, put mounted inside useEffect is better,
once search changed, previous request should be cancel,
const [data, setData] = useState([]);
const [search, setSearch] = useState();
useEffect(() => {
let cancel = true;
async function fetchData() {
const {start_date, end_date} = search;
const result = await getDataStock(start_date, end_date);
if (result && !cancel) {
setData(result.data); // only set a state when not canceled
}
}
fetchData();
return () => {
cancel = true; // to cancel setState
}
}, [search])

React native screens not re-rendering when custom hook state got changed

I am using a custom hook in app purchases.
const useInAppPurchase = () => {
const context = useContext(AuthGlobal)
const [isFullAppPurchased, setIsFullAppPurchased] = useState(false)
useEffect(() => {
console.log(`InAppPurchase useEffect is called`)
getProductsIAP()
return async () => {
try {
await disconnectAsync()
} catch (error) {}
}
}, [])
....
}
When I used this hook at AccountScreen (where I do the purchase) Account screen is getting re-rendered once the payment is done.
i.e. isFullAppPurchased is changing from false -> true
const AccountScreen = (props) => {
const width = useWindowDimensions().width
const {
isFullAppPurchased,
} = useInAppPurchase()
return (
// value is true after the purchase
<Text>{isFullAppPurchased}</Text>
)
}
But I am using the same hook in CategoryList screen and after the payment is done when I navigate to the CategoryList screen, The values (isFullAppPurchased) is not updated (still false).
But when I do the re-rendering manually then I get isFullAppPurchased as true.
const CategoryList = (props) => {
const navigation = useNavigation()
const { isFullAppPurchased } = useInAppPurchase()
return (
// still value is false
<Text>{isFullAppPurchased}</Text>
)
}
What is the reason for this behaviour ? How should I re-render CategoryList screen once the payment is done ?
Thank you.
I see hook make API request only on mount, if whole parent component didn't unmount and rendered a new, value of hook stays same.
E.g. dependencies array is empty - [] so hook doesn't request data again.
Probably better idea is to pass isFullAppPurchased via context or redux from top level.
And put state and function to update that state in same place.

React JS window.requestAnimationFrame(update)

I am using Flickity plugin on React JS and all works. However, I want to create slider like https://brand.uber.com (Best-in-class examples section).
The workaround example posted at https://github.com/metafizzy/flickity/issues/77 works, but i have issue with play and pause functions.
The issues is window.requestAnimationFrame(update); is re rendering component before pause is taken into account. I have tried with local state and also redux, but it re-renders before i can dispatch. I am using function comp and code below.
const carouselIsScrolling = useSelector(isServicesScrolling);
const servicesCategoriesSel = useSelector(getServiceCategories);
const carousel = useRef(null);
const dispatch = useDispatch();
const update = () => {
if (carousel.current.flkty.slides && carouselIsScrolling) {
carousel.current.flkty.x =
(carousel.current.flkty.x - tickerSpeed) %
carousel.current.flkty.slideableWidth;
carousel.current.flkty.selectedIndex = carousel.current.flkty.dragEndRestingSelect();
carousel.current.flkty.updateSelectedSlide();
carousel.current.flkty.settle(carousel.current.flkty.x);
window.requestAnimationFrame(update);
}
};
const pause = () => {
dispatch(app.toggleServiceScroller(false));
window.cancelAnimationFrame(window.requestAnimationFrame(update));
};
const play = () => {
if (!carouselIsScrolling) {
dispatch(app.toggleServiceScroller(true));
window.requestAnimationFrame(update);
}
};
useEffect(() => {
carousel.current.flkty.on("dragStart", () => {
dispatch(app.toggleServiceScroller(false));
});
dispatch(app.toggleServiceScroller(false));
update();
}, []);
The reason is that I was changing carouselIsScrolling with state, that was waiting for the re render to use, but the re render was causing it to reset to initial value.
I changed to using
const carouselIsScrolling = useRef(true);
and
if (!carouselIsScrolling.current) return;
and now it works.

React Hooks state always a step behind

Im having trouble resetting page = 1. In the handleSearchClick I have setPage(1) but its one click late, so after the second click page will reset to 1
This is my current code
const [page, setPage] = React.useState(1);
//Other states as well
/**
* Handles event on search click
*/
const handleSearchClick = async () => {
setItems([]);
const database = handleBaseId();
setPage(1);
const items = await fetchSearchPayload(page, value, option, database);
setIsLoading(false);
setItems(items);
};
I also tried resetting page in the button itself but was getting a react limits the number of renders to prevents an infinite loop error message
This is what I tried doing
<SearchBar
onClickButton={handleSearchClick}
onValueChange={handleChange}
value={value}
pageNum={page}
onEnter={handleSearchOnEnter}>
{setPage(1)}
</SearchBar>
I also tried using useEffect and added a count state that increments every time the searchBar is clicked, but I was having trouble with count itself being a step behind.
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setPage(1);
}, [count]);
/**
* Handles event on search click
*/
const handleSearchClick = async () => {
setCount(count+1);
setItems([]);
const database = handleBaseId();
setPage(1);
const items = await fetchSearchPayload(page, value, option, database);
setIsLoading(false);
setItems(items);
};
Any suggestions on how to reset page without any delays are appreciated!
It seems you're attempting to set the page (async) and immediately afterwards use the page state in your fetchSearchPayload call. Considering you're hardcoding always setting the page to 1 there, you could also hardcode the 1 in your fetchSearchPayload call.
Another route you could go is only do the async call when the page has updated by doing something like this:
React.useEffect(() => {
const database = handleBaseId();
const items = await fetchSearchPayload(page, value, option, database);
setIsLoading(false);
setItems(items);
}, [ page ]);
/**
* Handles event on search click
*/
const handleSearchClick = async () => {
setItems([]);
setPage(1);
};
The problem is that state updates are asynchronous with hooks state updater just like setState in react components. The solution is to pass the page manually to function instead of depending on state
/**
* Handles event on search click
*/
const handleSearchClick = async () => {
setItems([]);
const database = handleBaseId();
setPage(1);
const items = await fetchSearchPayload(1, value, option, database);
setIsLoading(false);
setItems(items);
};

Resources