Reputation: 414
I have a parent component, NavBar
, which has two children: NavItems
and Icon
. When either of those children are clicked, their appearance should both change. A very standard functionality.
What I want in addition is to be able to set the initial state based on the screen width. For this I have created a custom hook that I have called useMatchMedia
. Every time useMatchMedia
is called, I need the state to be overridden, regardless of what state it currently has and the child components to be re-rendered with this new state.
I believe the problem lies within NavBar
before the return statement. But just in case it doesn't, I have provided the code for my custom hook as well.
(Scroll to below my code to see my attempts to fix the issue).
Here is the parent:
function NavBar() {
const matchMedia = useMatchMedia(false, "tablet"); // returns true or false if the screen width is that of a tablet or not
const [hideNavItems, setHideNavItems] = useState(matchMedia);
let onHandleHideNavItems = () => {
setHideNavItems(!hideNavItems);
};
return (
<Nav>
<NavItems
hideNavItems={hideNavItems}
onHandleHideNavItems={onHandleHideNavItems} // onClick={() => onHandleHideNavItems(!hideNavItems)}
/>
<Icon
hideNavItems={hideNavItems}
onHandleHideNavItems={onHandleHideNavItems} // onClick={() => onHandleHideNavItems(!hideNavItems)}
/>
</Nav>
);
}
export default NavBar;
Here is my custom hook:
function useMatchMedia(initialState, device) {
const [passQuery, setPassQuery] = useState(initialState);
const matchMediaRef = useRef(null);
let widthType = null;
let chosenDevice = device;
switch (chosenDevice) {
case "phone":
chosenDevice = 425;
widthType = "max";
break;
case "tablet":
chosenDevice = 768;
widthType = "max";
break;
case "smallLaptop":
chosenDevice = 1024;
widthType = "max";
break;
case "largeLaptop":
chosenDevice = 1440;
widthType = "max";
break;
default:
chosenDevice = 1800;
widthType = "min";
}
useEffect(() => {
matchMediaRef.current = window.matchMedia(
`(${widthType}-width: ${chosenDevice}px)`
);
const initialMatch = matchMediaRef.current.matches;
if (initialMatch) {
setPassQuery(true);
} else {
setPassQuery(false);
}
const target = (event) => {
if (event.matches) {
setPassQuery(true);
} else {
setPassQuery(false);
}
};
matchMediaRef.current.addListener(target);
return () => {
matchMediaRef.current.removeListener(target);
};
}, [widthType, chosenDevice]);
return passQuery;
}
export default useMatchMedia;
I have tried to use useEffect
within my NavBar component, such as:
useEffect(() => {
setHideNavItems(!hideNavItems);
}, [matchMedia, hideNavItems]);
That way I should get a re-render whenever matchMedia changes and as the initial state of hideNavItems
is set to matchMedia
that should update my state and therefore the children. This fails because it creates an infinite loop. I understand why there is an infinite loop but I also believe the solution is close to something like this.
I have also tried using useCallback
on onHandleHideNavItems
but that doesn't work.
I have tried using useRef
to restrict the render to updates and not the initial render. This also does not work.
I'm a bit lost. Any help is much appreciated.
Upvotes: 2
Views: 198
Reputation: 4536
Just use useEffect
to reset the hideNavItems
state back to matchMedia
value whenever the screen width changes based on your useMatchMedia
hook logic:
useEffect(() => {
setHideNavItems(matchMedia);
}, [matchMedia]);
Upvotes: 1