Responsive sidebar using react hooks - reactjs

I'm trying to debug an issue that I'm having with my sidebar but can't figure out what I'm doing wrong. The sidebar is correctly collapsing based on the innerWidth but when I am on mobile view on the first load the sidebar is expanded rather than collapse as suppose to be.
Any help that explains to me what is wrong would be great.
Thanks a lot
Here is my snippet:
export default function Sidebar() {
const location = useLocation();
let { pathname } = location;
const [isNavOpen, setIsNavOpen] = useState(true);
useEffect(() => {
window.addEventListener("resize", () => {
if (window.innerWidth <= 767) {
setIsNavOpen(false);
}
else if (window.innerWidth >= 767) {
setIsNavOpen(true);
}
});
});
return (
<div className="menu-bar">
<Menu
width={210}
isOpen={isNavOpen}
noOverlay
pageWrapId={"page-wrap"}
outerContainerId={"outer-container"}
disableAutoFocus
disableCloseOnEsc
>

Your initial state is true, so when the app starts, the sidebar is open. As long as you don't resize, the event handler is not called.
Extract the logic that defines the state of isNavOpen to a function, and call it to create the initial value, and then when the window is resized:
const { useState, useEffect } = React;
const shouldBeOpen = () => window.innerWidth > 767
function Sidebar() {
const [isNavOpen, setIsNavOpen] = useState(shouldBeOpen);
useEffect(() => {
window.addEventListener("resize", () => {
setIsNavOpen(shouldBeOpen());
});
}, []);
return isNavOpen ? 'open' : 'close'
}
ReactDOM.render(
<Sidebar />,
root
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
In addition, in this case I prefer the use of window.matchMedia(), which is the JS equivalent to CSS media queries. I've created a useMatchMedia hook which you can see in this answer.

Related

Need to update React component on window resize

I am trying to access the window object inside of a React.js component as I want to create a state which holds the dynamic innerWidth value of the window object. I was able to make it work when the page gets refreshed but not when I resize the page with the dev tools dynamically.
Here is the code that works for me on refresh:
const About = () => {
const [bioType, setBioType] = useState("");
const truncateText = () =>
window.innerWidth > 1024 ? setBioType("desktop") : setBioType("mobile");
useEffect(() => {
truncateText();
});
return ({
bioType === 'desktop' ? ... : ....
})
}
However, when I resize the web page with Dev Tools, it doesn't work. Could someone give me a hint? Thanks.`
Changing the windows width doesn't cause React to react to the change, and re-render. You need to use an event handler to listen to the resize event, use a ResizeObserver or use MatchMedia, and listen to the changes.
Example with MatchMedia:
const { useState, useEffect } = React;
const MIN_WIDTH = 600;
const getBioType = matches => matches ? 'desktop' : 'mobile';
const About = () => {
const [bioType, setBioType] = useState(() => getBioType(window.innerWidth > MIN_WIDTH));
useEffect(() => {
const mql = window.matchMedia(`(min-width: ${MIN_WIDTH}px)`);
const handler = e => setBioType(getBioType(e.matches));
mql.addEventListener('change', handler);
return () => {
mql.removeEventListener('change', handler);
};
}, []);
return bioType;
}
ReactDOM
.createRoot(root)
.render(<About />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>

How to capture "any event" on HTML object?

I have several HTML img element I want to manipulate on the following events:
onMouseEnter
onMouseLeave
onMouseDownCapture
onMouseUp
A naive solution (that works) is to implement these listeners for each event manually as such:
<img
src={taskbarImage}
onMouseEnter={(e) =>setTaskbarImage(taskbarAppImageHover)}
onMouseLeave={(e) => setTaskbarImage(taskbarAppImage)}
onMouseUp={(e) => setTaskbarImage(taskbarAppImageHover)}
onMouseDownCapture={(e) => setTaskbarImage(taskbarAppImageFocus)}
className="taskbar-application-img">
</img>
This code is kind of messy and I would much rather simply attach one function that triggers any time any event happens on the tag. After this, the function would then analyze for what event it is and act appropriately. Something like this:
const taskBarManipulation = (e) => {
switch (e.type) {
case "mouseenter":
setTaskbarImage(taskbarAppImageHover);
case "mouseleave":
setTaskbarImage(taskbarAppImageHover);
case "mouseup":
setTaskbarImage(taskbarAppImage);
case "mousedowncapture":
setTaskbarImage(taskbarAppImageFocus);
}
};
The snippet above works for detecting the type of event and changing the variable. However, I don't know how to make the function trigger on any event happening in the tag. Any suggestions?
There are many events, listening tho all of those will slow down your component, and is not recommended.
I'd use a function that returns the eventListeners you wish to add, and then apply that to the component using spreading:
const { useState } = React;
const getEvents = () => {
return {
onClick: () => console.log('onClick'),
onMouseEnter: () => console.log('onMouseEnter'),
onMouseLeave: () => console.log('onMouseLeave'),
// ...etc
};
}
const Example = () => {
return (
<div>
<h1 {...getEvents()}>{'Test me!'}</h1>
</div>
)
}
ReactDOM.render(<Example />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
If all those event have the same handler, we can create a more fancy getEvents function like so:
const eventHandler = (e) => console.log(e.type);
const getEvents = (events = [ 'onClick', 'onMouseEnter', 'onMouseLeave' ]) => {
return events.reduce((c, p) => ({ ...c, [p]: eventHandler }), {});
}
const { useState } = React;
const eventHandler = (e) => console.log(e.type);
const getEvents = (events = [ 'onClick', 'onMouseEnter', 'onMouseLeave' ]) => {
return events.reduce((c, p) => ({ ...c, [p]: eventHandler }), {});
}
const Example = () => {
return (
<div>
<h1 {...getEvents()}>{'Test me!'}</h1>
</div>
)
}
ReactDOM.render(<Example />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Next.JS - Too many re-renders. React limits the number of renders to prevent an infinite loop

I think i've been tried all possible possibilities but its not working. I wanna make dynamic menu (i know its not correct database fiction for this but i wrote for learn).
I'll create json for this plan. I can't edit json file coz i wanna try regenerate json data from json.
Main menu
-- Sub Menu
-- Sub Menu
I can say it Next.js is forced me a little.
This is my code block:
import {useEffect, useState } from "react";
export default async function MenuCreator() {
const [menu,setMenu] = useState([]);
const [subMenu,setSubMenu] = useState([]);
const [menu_f,setMenu_F] = useState([]);
useEffect(()=>{
fetch("http://localhost:8000/menus").then((res)=>{
if(!res.ok) throw Error("error");
return res.json();
}).then((data)=>{
setMenu_F(data);
});
},[])
menu_f.forEach((main)=>{
if(main.main_menu === null)
{
menu_f.forEach((sub)=>{
if(sub.main_menu === main.id)
setSubMenu([...subMenu,{id:sub.id,menu_name:sub.menu_name,main_menu:sub.main_menu,menu_link:sub.menu_link}]);
})
setMenu([...menu,{
id:main.id,
menu_name:main.menu_name,
menu_icon:main.menu_icon,
menu_link:main.menu_link,
subMenus:subMenu
}]);
setSubMenu([]);
}
})
return {menu};
}
and json file
{
"quick_links":[
{
"id":1,
"menu_id":1
},
{
"id":2,
"menu_id":2
}
],
"menus":[
{
"id":1,
"menu_name":"dashboard",
"menu_icon":"fa-gauge",
"main_menu":null,
"menu_link":"/"
},
{
"id":2,
"menu_name":"receipt",
"menu_icon":"fa-file-lines",
"main_menu":null,
"menu_link":"/dashboard"
},
{
"id":3,
"menu_name":"buy",
"menu_icon":"fa-cart-plus",
"main_menu":null,
"menu_link":"/buy"
},
{
"id":4,
"menu_name":"users",
"menu_icon":"fa-user",
"main_menu":null,
"menu_link":"/users"
},
{
"id":5,
"menu_name":"user list",
"menu_icon":null,
"main_menu":4,
"menu_link":"/user-list"
},
{
"id":6,
"menu_name":"add user",
"menu_icon":null,
"main_menu":4,
"menu_link":"/add-user"
}
]
}
You're updating the state at every render inside your menu_f.forEach, as a quick fix you can put everything inside a useEffect that listen on menu_f change like this:
useEffect(() => {
menu_f.forEach((main)=>{
if(main.main_menu === null)
{
menu_f.forEach((sub)=>{
if(sub.main_menu === main.id)
setSubMenu([...subMenu,{id:sub.id,menu_name:sub.menu_name,main_menu:sub.main_menu,menu_link:sub.menu_link}]);
})
setMenu([...menu,{
id:main.id,
menu_name:main.menu_name,
menu_icon:main.menu_icon,
menu_link:main.menu_link,
subMenus:subMenu
}]);
setSubMenu([]);
}
})
}, [menu_f])
When you do a setMenu/setSubMenu it triggers a rerender so it'll go again in your forEach loop and do the setMenu/setSubMenu again and again doing an infinite loop.
EDIT: I guess your code will not work as expected, in the if (main.main_menu === null) you're doing a lot of unexpected state manipulation. setSubMenu will not change the subMenu variable instantly so the setMenu that is using subMenu will have an undefined value instead of what you want.
What you can do to avoid this:
if (main.main_menu === null) {
const subMenuNotState = menu_f.find(sub => sub.main_menu === main.id);
if (subMenuNotState) {
setMenu([...menu,{
id:main.id,
menu_name:main.menu_name,
menu_icon:main.menu_icon,
menu_link:main.menu_link,
subMenus:subMenuNotState
]);
}
}
Summary
This is because you are in a infinite loop
When the code carry out setState, it will re-render again
So you need tell the react component only carry out setState when state has changed.
Move your forEach loop into a useEffect(), and add a second parameter [menu_f] means that execute only when the menu_f state mutated
like
useEffect(()=>{} ,[menu_f])
a modified example from question
useEffect(() => {
menu_f.forEach((main)=>{
if(main.main_menu === null)
{
menu_f.forEach((sub)=>{
if(sub.main_menu === main.id)
setSubMenu([...subMenu,{id:sub.id,menu_name:sub.menu_name,main_menu:sub.main_menu,menu_link:sub.menu_link}]);
})
setMenu([...menu,{
id:main.id,
menu_name:main.menu_name,
menu_icon:main.menu_icon,
menu_link:main.menu_link,
subMenus:subMenu
}]);
setSubMenu([]);
}
})
}, [menu_f]);
Other Example
a fail example in your case
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
function App () {
const [state, setState] = React.useState("No");
// ***
// Error happened here
// Need change this by React.useEffect() with parameter [state]
// And setState in other function or event
setState("Yes");
// ***
const handleClick = () = {}
return <div>
<button onClick={handleClick}>Click to setState</button>
{state}
</div>
}
</script>
<script type="text/babel">
ReactDOM.render(
<App></App>
, document.getElementById("root"));
</script>
a successful example
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
function App () {
const [state, setState] = React.useState("No");
React.useEffect(() => {
// other stuff...
}, [state])
const handleClick = () => {
if (state !== "Yes") {
setState("Yes")
}
else {
setState("No")
}
}
return <div>
<button onClick={handleClick}>Click to setState</button>
{state}
</div>
}
</script>
<script type="text/babel">
ReactDOM.render(
<App></App>
, document.getElementById("root"));
</script>
Other
And your main_f.forEach's code block is weird, please consider #Nicolas Menettrier's suggestion

How to register only once in react useEffect and get useState changes

I have a react hook component which needs to register to an outside event as follows:
const DefaultRateSection: React.FC<{
registerOnSaveEvent: (fn: () => void) => void;
}> = ({registerOnSaveEvent}) => {
const [serviceName, setServiceName] = useState('the name');
const serviceNameInput = useRef<input>(null);
useEffect(
() => {
return registerOnSaveEvent(() => {
if (!serviceName) {
serviceNameInput.current?.focus();
}
});
},
[registerOnSaveEvent]
);
return (
<input
ref={serviceNameInput}
value={serviceName}
onChange={(event) => {
const newValue = event.target.value;
setServiceName(newValue)
}}
/>
);
};
The registerOnSaveEvent is an API that i cannot change and i do not have an unsubscribe method, therefore i need to register to it only once. However, when it fires (from outside the component) i'm receiving the initial value of serviceName and not the updated one. I know it happens because i'm not calling useEffect after the change, but I need to avoid multiple registrations.
How can i achieve this?
TL-TR: The short answer is to use another ref so that the arrow function can access the latest rendered value of service name.
const serviceNameRef = useRef();
serviceNameRef.current = serviceName;
// use serviceNameRef.current in the arrow function
This code will NOT work
// Get a hook function - only needed for this Stack Snippet
const {useState, useRef, useEffect} = React;
const DefaultRateSection = ({ registerOnSaveEvent }) => {
const [serviceName, setServiceName] = useState("the name");
const serviceNameInput = useRef();
useEffect(() => {
return registerOnSaveEvent(() => {
console.log(serviceName)
});
}, [registerOnSaveEvent]);
return (
<input
ref={serviceNameInput}
value={serviceName}
onChange={(event) => {
const newValue = event.target.value;
setServiceName(newValue);
}}
/>
);
};
const App = () => {
const callbackRef = useRef();
function registerOnSaveEvent(callback) {
callbackRef.current=callback
}
function execCallback() {
callbackRef.current();
}
return <div>
<h2>This will not work</h2>
<p>Try to change the input field and click 'RegisterOnSaveEvent'.</p>
<p>The callback will not see the new value of the input</p>
<DefaultRateSection registerOnSaveEvent={registerOnSaveEvent}/>
<button onClick={execCallback}>RegisterOnSaveEvent</button>
</div>;
};
ReactDOM.render(
<App/>,
document.getElementById("react")
);
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
This code will work
// Get a hook function - only needed for this Stack Snippet
const {useState, useRef, useEffect} = React;
const DefaultRateSection = ({ registerOnSaveEvent }) => {
const [serviceName, setServiceName] = useState("the name");
const serviceNameInput = useRef();
// use a ref for the service name and
// update it with the serviceName state on every render
const serviceNameRef = useRef();
serviceNameRef.current = serviceName;
useEffect(() => {
return registerOnSaveEvent(() => {
console.log(serviceNameRef.current)
});
}, [registerOnSaveEvent]);
return (
<input
ref={serviceNameInput}
value={serviceName}
onChange={(event) => {
const newValue = event.target.value;
setServiceName(newValue);
}}
/>
);
};
const App = () => {
const callbackRef = useRef();
function registerOnSaveEvent(callback) {
callbackRef.current=callback
}
function execCallback() {
callbackRef.current();
}
return <div>
<h2>This will work</h2>
<p>Try to change the input field and click 'RegisterOnSaveEvent'.</p>
<p>The callback will not see the new value of the input</p>
<DefaultRateSection registerOnSaveEvent={registerOnSaveEvent}/>
<button onClick={execCallback}>RegisterOnSaveEvent</button>
</div>;
};
ReactDOM.render(
<App/>,
document.getElementById("react")
);
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Explanation
When the useEffect is executed it creates an arrow function. This arrow function references the const serviceName which is the initial value. This is the value that the arrow function sees. When you enter something in the input field you call the setServiceName which changes the state and triggers a rerender. The rendering itself is nothing but a function call. So when the component is rerendered the useState returns the state and you assign it to a brand new local const named serviceName. This is NOT the same as the one that the arrow function references. Thus the arrow function will always see the value of serviceName when it was created.
To solve this problem I use another ref for the serviceName called serviceNameRef and update that ref with the serviceName state on every rendering. Since useRef returns the same instance of serviceRefName on each call, it is the same instance as the one the arrow function uses. That's how it works.

Why useEffect doesn't trigger on document change?

I am writing custom hook to set new locale every time when the HTML lang attribute changes, but it seems the useEffect hook doesn't fire when the document.documentElement.lang is changed with javascript. I know how to solve this, my question is why does it behave like this?
export const useLocale = (): LocaleObject => {
const [lang, setLang] = useState<string>(document.documentElement.lang);
useEffect(() => {
setLang(document.documentElement.lang);
}, [document.documentElement.lang]); // useEffect is not triggered when document.documentElement.lang changes
return locale[lang];
};
As #DennisVash written in his comment:
Why? Because the change to document.documentElement.lang won't trigger
the hook. Only a render will trigger the hook, and if lang changed the
callback will be executed.
However, since changing the property actually changes the lang attribute value in the DOM, you can use a MutationObserver to track the lang attribute values.
I've created a custom useMutationObserver hook to track mutations in the DOM, and based useLocale on it.
const { useRef, useEffect, useState, useCallback } = React;
const useMutationObserver = (domNodeSelector, observerOptions, cb) => {
useEffect(() => {
const targetNode = document.querySelector(domNodeSelector);
const observer = new MutationObserver(cb);
observer.observe(targetNode, observerOptions);
return () => {
observer.disconnect();
};
}, [domNodeSelector, observerOptions, cb]);
}
const options = { attributes: true };
const useLocale = () => {
const [lang, setLang] = useState(document.documentElement.lang);
const handler = useCallback(mutationList => {
mutationList.forEach(mutation => {
if(mutation.type !== 'attributes' || mutation.attributeName !== 'lang') return;
setLang(document.documentElement.lang);
});
}, []);
useMutationObserver('html', options, handler);
return lang; // locale[lang]
};
const Demo = () => {
const locale = useLocale();
return <div>{locale}</div>;
};
document.documentElement.lang = 'en'; // base lang
ReactDOM.render(
<Demo />,
root
);
// example - changing the lang
setTimeout(() => document.documentElement.lang = 'fr', 1000);
setTimeout(() => document.documentElement.lang = 'ru', 3000);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>

Resources