So I'm currently working with React Hooks, and I'm trying to use useEffect. It supposed whenever that dependencies changed, the useEffect would re-render right? But it doesn't work for me. Here's my code :
const [slidesPerView, setSlidesPerView] = React.useState(0)
React.useEffect(() => {
setSlidesPerView(() => (window.innerWidth <= 375 ? 1 : 2))
console.log("rerender?", slidesPerView)
}, [window.innerWidth])
Everytime I changed the screen size, useEffect won't re-render. I wonder what did I do wrong?
useEffect will respond to either props changes or state changes.
Every time screen size changes component has no idea, if window.innerWidth is changed or not, because it is not in a state or props.
To get it working you need to store window.innerWidth into state, and attach a event listener to your window, whenever window size changes it will get the window.innerWidth and store it into the state, and as state changes your useEffect will get re-run, and finally your component will get re-render.
const [size, setSize] = React.useState(window.innerWidth)
React.useEffect(() => {
//Attach event on window which will track window size changes and store the width in state
window.addEventListener("resize", updateWidth);
setSlidesPerView(() => (size <= 375 ? 1 : 2));
console.log("rerender?", slidesPerView);
//It is important to remove EventListener attached on window.
return () => window.removeEventListener("resize", updateWidth);
}, [size])
const updateWidth = () => {
setSize(window.innerWidth)
}
Demo
Related
The gap variable should just give a difference once, but it gives a diff every second. I am not even updating its state. Even if i use settime in useEffect still the other variable that are changing in background are still effecting the page and are updating.
const ftime = dayjs('Dec 31,2021').unix();
const dateVar = dayjs().unix();
const gap = ftime - dateVar;
const [time, settime] = useState(dayjs().format('DD/MM/YYYY'));
useEffect(() => {
setInterval(() => settime(dayjs().second()), 1000);
}, []);
return (
<div className="App">
<div>{time}</div>
<div>{gap}</div>
</div>
)
useEffect(() => {
setInterval(() => settime(dayjs().second()), 1000)
}, [])
settime is invoked for every 1000ms. This will update the state (time) and trigger a rerender.
const ftime = dayjs('Dec 31,2021').unix();
const dateVar = dayjs().unix();
const gap = ftime - dateVar;
As these three variables are initialised at the top level of the function, the rerender will initialise them every time.
If you want to prevent this, you can move the variables outside the function component.
It is updating every second because you change time every second, which the component renders.
This rerendering will also cause the constants ftime and dateVar to reinitialise every second. If this is not intended, you need to put them outside the function or wrap them in a hook, such as useState or useMemo.
You can solve the rerendering issue by making the time a React component and placing the interval effect in that component. A child's rerendering normally doesn't trigger a rerender in the parent.
This is because in useEffect you are updating the state every second. When a state updates in a react function component the variables will be re-initialized and react doesn't track the values of the variables. It only keeps track on state and props.
Here you can make use of useRef hook since its is mutable and react does not re-initialize its value, which guarantees its value to be same during state update
import { useState, useEffect, useRef } from "react";
import dayjs from "dayjs";
import ReactDOM from "react-dom";
function App() {
const gap = useRef(dayjs("Dec 31,2021").unix() - dayjs().unix());
const [time, settime] = useState(dayjs().format("DD/MM/YYYY"));
useEffect(() => {
setInterval(() => settime(dayjs().second()), 1000);
}, []);
return (
<div className="App">
<div> {time}</div>
**<div>{gap.current}</div>**
</div>
);
}
I'm just playing around with ReactJS and trying to figure out some strange behavior with the useState hook.
A component should not re-rendered if the state is set with the same primitive value (Boolean) as it was before
const useScroll = ({positionToCross = 10}) => {
const window = useWindow();
const [isPositionCrossed, setIsPositionCrossed] = useState(window.scrollY > positionToCross);
useEffect(() => {
const onScroll = function (e) {
window.requestAnimationFrame(function () {
const lastKnownScrollPosition = window.scrollY;
setIsPositionCrossed(lastKnownScrollPosition > positionToCross);
});
}
window.addEventListener('scroll', onScroll);
return () => {
window.removeEventListener("scroll", onScroll)
}
}, []);
console.log(`useScroll - render window.scrollY = ${window.scrollY.toFixed(0)} isPositionCrossed = `, isPositionCrossed)
return {isPositionCrossed}
}
here is the console output - you can see the component and the hook are both rendered two times with "true" (after scrolled over 100px)
"useScroll - render window.scrollY = 101 isPositionCrossed = ", true
"useScroll - render window.scrollY = 103 isPositionCrossed = ", true
If you try simple code that on click handler setState and if you click two times and in each update state with same value the component again re-render.
As react doc says:
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.
I hope the answers from this post and this github discussion help you to understand why this happens
and there are another related topics like this post and this one
I want to update state with the inner window height as I resize the screen. When I log the state height within the useEffect hook I get 0 each time however, when I log inside the updateWindowDimensions function the height value is updated as expected.
How can I update state with the new value each time?
const [height, setHeight] = useState(0);
const updateWindowDimensions = () => {
const newHeight = window.innerHeight;
setHeight(newHeight);
console.log('updating height');
};
useEffect(() => {
window.addEventListener('resize', updateWindowDimensions);
console.log("give height", height);
}, []);
Your useEffect is only being run one time, when the component mounts (because of the empty array [] you passed as the second argument)
If you simply log outside of it, you'll see your state value is being updated correctly
const [height, setHeight] = useState(0);
useEffect(() => {
const updateWindowDimensions = () => {
const newHeight = window.innerHeight;
setHeight(newHeight);
console.log("updating height");
};
window.addEventListener("resize", updateWindowDimensions);
return () => window.removeEventListener("resize", updateWindowDimensions)
}, []);
console.log("give height", height);
Also you should move the declaration of that function inside the useEffect so it's not redeclared on every render
The above Answer by #Galupuf won't work ideally, until you add updateWindowDimensions call below addEventListener:
window.addEventListener("resize", updateWindowDimensions);
updateWindowDimensions()
Because eventListener won't run this function until you resize.
so height will remain 0 until you resize.
I know this is too late for answer, but we can define a hook for this purpose. In here you can find a custom hook that update width and height of window on every resize.
So I've got this hook to return the windowWidth for my App components. I'll call this Option #1.
import {useEffect, useState} from 'react';
function useWindowWidth() {
const [windowWidth,setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWindowWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowWidth;
}
export default useWindowWidth;
And right now I'm basically using it on every component that depends on the window width to render, like:
function Component(props) {
const windowWidth = useWindowWidth();
return(
// RETURN SOMETHING BASED ON WINDOW WIDTH
);
}
And since the hook has an event listener for the resize events, the component stays responsive even after window resizes.
But I'm worried that I'm attaching a new listener for every component that uses that hook and it might slow things down at some point. And I've though of other approach:
Option #2
I use the useWindowWidth() hook only one time, inside a top level component like <App/> and I'll provide the windowWidth value down the chain via context.
Like:
function App() {
const windowWidth = useWindowWidth();
return(
<WindowWidthContext.Provider value={windowWidth}>
<Rest_of_the_app/>
</WindowWidthContext.Provider>
);
}
And then, every component that needs it could get it via:
function Component() {
const windowWidth = useContext(WindowWidthContext);
return(
// SOMETHING BASED ON WINDOW WIDTH
);
}
QUESTION
Am I right in being bothered by that fact that I'm setting up multiple resize listeners with Option #1 ? Is Option #2 a good way to optmize that flow?
If your window with is used by so many components as you mentioned, you must prefer using context. As it reads below:
Context is for global scope of application.
So, #2 is perfect choice here per react.
First approach #1 might be good for components in same hierarchy but only up-to 2-3 levels.
I'm not sure if adding and removing event listeners is a more expensive operation than setting and deleting map keys but maybe the following would optimize it:
const changeTracker = (debounceTime => {
const listeners = new Map();
const add = fn => {
listeners.set(fn, fn);
return () => listeners.delete(fn);
};
let debounceTimeout;
window.addEventListener('resize', () => {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(
() => {
const width=window.innerWidth;
listeners.forEach(l => l(width))
},
debounceTime
);
});
return add;
})(200);
function useWindowWidth() {
const [windowWidth, setWindowWidth] = useState(
() => window.innerWidth
);
useEffect(
() =>//changeTracker returns a remove function
changeTracker((width) =>
setWindowWidth(width)
),
[]
);
return windowWidth;
}
As HMR said in an above thread, my solution was to use redux to hold the width value. With this strategy you only need one listener and you can restrict how often you update with whatever tool you like. You could check if the width value is within the range of a new breakpoint and only update redux when that is true. This only works if your components dont need a steady stream of the window width, in that case just debounce.
I have 2 different kinds of NavBar components in my project: a NavBarTransparent.jsx that's only used in index.jsx (home page), and a NavBar.jsx that's used on all other pages.
Now, I have a function set to trigger each time the window scrolls in the NavBarTransparent.jsx component:
// components/NavBarTransparent.jsx
useEffect(() => {
window.removeEventListener('scroll', scrollFunction);
});
const scrollFunction = () => {
const nav = document.getElementById('nav');
const overlay = document.getElementById('nav-overlay');
const travel = document.documentElement.scrollTop;
const travelRem = travel / 16;
const navHeight = 7 - (travelRem / 6);
if (navHeight <= 4) {
nav.style.lineHeight = `${4}rem`;
overlay.style.opacity = 1;
} else if (navHeight >= 7) {
nav.style.lineHeight = `${7}rem`;
overlay.style.opacity = 0;
} else {
nav.style.lineHeight = `${navHeight}rem`;
overlay.style.opacity = (7 - navHeight) / 3;
}
};
The other navbar component (NavBar.jsx) does not have this event listener. However, the event listener still gets triggered when I visit, say, /about from / (client-side routing). Why is the listener getting hit when the other page doesn't even use that component?
The repo is up at https://github.com/amitschandillia/proost/tree/master/web
Fixed it with the following:
useLayoutEffect(() => {
if(transparent) { window.addEventListener('scroll', scrollFunction); }
// returned function will be called on component unmount
return () => {
window.removeEventListener('scroll', scrollFunction);
}
}, []);
Looks like window event listeners added before a component mounts must be explicitly removed before the component unmounts. I was adding the listener via the useEffect hook, but not unmounting it, hence the issue of persistent triggers. Now, I have used the return() snippet of useEffect (the Hooks equivalent of componentWillUnmount) to remove the listener and it works fine.
Also, am using useLayoutEffect instead of useEffect because if you want to manipulate DOM and do so before browser paint, as is the case here, useLayoutEffect` is preferable.