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>
Related
Let's say I have a state called count.
const [count, setCount] = useState();
Now let's say I would like to increase count with 1 every time some key in the keyboard is being pressed.
So I can use useEffect hook and add an event listener to it.
useEffect(() => {
document.addEventListener("keydown", increaseCount);
}, []);
The function increaseCount, increasing count with 1
const increaseCount = () => {
setCount(prevCount => prevCount + 1);
};
Well everything is working great. BUT, if I want to get the current value of count inside increaseCount, I can't do this! The event listener is only called once when the component is mounting (because the useEffect has an empty dependency array).
And if I add count to the dependency array, I have a new problem - I'm creating a kind of loop, because useEffect will call increaseCount, that will call setCount(), that will cause the component to re-render, which will call useEffect again and so on and so on.
I have this kind of problem on a few projects I'm currently working on, and it is very frustrating. So if you know how to answer this - thanks! :)
snippets
When using an empty dependency array and login count inside increaseCount, count will always be 0:
// Get a hook function
const {useState, useEffect} = React;
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.addEventListener("keydown", increaseCount);
}, []);
const increaseCount = () => {
setCount((prevCount) => prevCount + 1);
console.log(count);
};
return (
<div>
count = {count}
</div>
);
};
// Render it
ReactDOM.render(
<Counter />,
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>
and when adding count to the dependency array, we see this "loop" thing happening:
// Get a hook function
const {useState, useEffect} = React;
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.addEventListener("keydown", increaseCount);
}, [count]);
const increaseCount = () => {
setCount((prevCount) => prevCount + 1);
console.log(count);
};
return (
<div>
count = {count}
</div>
);
};
// Render it
ReactDOM.render(
<Counter />,
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>
What you should do really depends on what you need to do. See X Y problem.
As mentioned there are multiple use cases you are trying for.
const { useState, useEffect, useMemo, Fragment } = React;
const App = () => {
const [count, setCount] = useState(0);
function increaseCount(ev) {
setCount((count) => {
const newCount = count + 1;
// You can use the newCount here for something basic if needed...
console.log(newCount);
// I'm not positive, but I'm farily sure that setting other state
// from within a set state function might be problematic.
// If you feel the need to do something with side effects here,
// Consider another useEffect as below.
return newCount;
});
}
useEffect(() => {
document.addEventListener("keydown", increaseCount);
// Always clean up after your effect!
return () => document.removeEventListener("keydown", increaseCount);
// If you use `increaseCount` here as a dependency (which you should), the function will get recreated every time.
// Or you could set it up by creating the `increase count function inside the effect itself
}, [increaseCount]);
useEffect(() => {
// If you need to use newCount for something more complicated, do it here...
// Or for side effects
}, [count]);
const arrayCountSized = useMemo(() => {
// You can use the count here in a useMemo for things that are
// derived, but not stateful in-of-themselves
return new Array(count).fill(null).map((_, idx) => idx);
}, [count]);
return (
<Fragment>
<div>{count}</div>
<ul>
{arrayCountSized.map((row) => (
<li key={row}>{row}</li>
))}
</ul>
</Fragment>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"/>
As for the situation you mentioned in particular, here's one solution.
You can get the "current Index" from the array length normally, which is what I'm doing here.
If the "currentIndex" is more complicated than that and inexorably tied together with the array state, you should useReducer to set up that tied state in a pure fashion.
You could also use a reference to the array in your listener function.
const arrayRef = useRef(array);
useEffect(()=>{arrayRef.current=array},[array]);
// arrayRef.current is safe to use in any effect after this.
const { useState, useEffect, useMemo, Fragment } = React;
const App = () => {
const [array, setArray] = useState([]);
const curIndex = array.length - 1;
useEffect(() => {
function addKey(ev) {
if (ev.key.length === 1 && ev.key >= "a" && ev.key <= "z") {
setArray((arr) => [...arr, ev.key]);
}
}
document.addEventListener("keydown", addKey);
// Always clean up after your effect!
return () => document.removeEventListener("keydown", addKey);
}, []);
return (
<Fragment>
<div>Current Index: {curIndex}</div>
<div>Keys pressed: </div>
<ul>
{array.map((row, index) => (
<li key={index}>{row}</li>
))}
</ul>
</Fragment>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"/>
useReducer Approach:
As I don't have enough details, this is the same solution as above, just in a reducer.
const { useState, useEffect, useReducer, useMemo, Fragment } = React;
function reducer(state, key) {
if (!key) {
return state;
}
const array = [...state.array, key];
return { array, currentIndex: array.length - 1 };
}
const App = () => {
const [{ array, currentIndex }, addKey] = useReducer(reducer, {
array: [],
currentIndex: -1,
});
useEffect(() => {
function keydownListener(ev) {
if (ev.key.length === 1 && ev.key >= "a" && ev.key <= "z") {
addKey(ev.key);
}
}
document.addEventListener("keydown", keydownListener);
// Always clean up after your effect!
return () => document.removeEventListener("keydown", keydownListener);
}, []);
return (
<Fragment>
<div>Current Index: {currentIndex}</div>
<div>Keys pressed: </div>
<ul>
{array.map((row, index) => (
<li key={index}>{row}</li>
))}
</ul>
</Fragment>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"/>
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.
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.
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>
A very simple list with inputs that are supposed to be addable and updatable.
The problem occurs after I add one or more inputs and then try to type inside of one of the inputs - all inputs after the one being typed in disappear.
It has something to do with memo-ing the Item component and I'm looking to understand what exactly is happening there (valueChanged getting cached?). I can't wrap my head around.
Without the memo function the code works as expected but of course, all inputs get updated on every change.
Here's a gif of what's happening: https://streamable.com/gsgvi
To replicate paste the code below into an HTML file or take a look here: https://codesandbox.io/s/81y3wnl142
<style>
ul {
list-style-type:none;
padding:0;
}
input[type=text] {
margin:0 10px;
}
</style>
<div id="app"></div>
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/babel-standalone#6.15.0/babel.min.js"></script>
<script type="text/babel">
const randomStr = () => Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
const { useState, memo, Fragment } = React
const Item = memo(({ value, i, valueChanged }) => {
console.log('item rendering...');
return <li>
<input type='text' value={value} onChange={e => valueChanged(i, e.target.value)}/>
</li>
}, (o, n) => o.value === n.value)
const ItemList = ({ items, valueChanged }) => {
return <ul>{
items.map(({ key, title }, i) => (
<Item
key={key}
i={i}
value={title}
valueChanged={valueChanged}
/>
))
}</ul>
}
const App = () => {
const [items, setItems] = useState([
{ key: randomStr(), title: 'abc' },
{ key: randomStr(), title: 'def' },
])
const valueChanged = (i, newVal) => {
let updatedItems = [...items]
updatedItems[i].title = newVal
setItems(updatedItems)
}
const addNew = () => {
let newItems = [...items]
newItems.push({ key: randomStr(), title:'' })
setItems(newItems)
}
return <Fragment>
<ItemList items={items} valueChanged={valueChanged}/>
<button onClick={addNew}>Add new</button>
</Fragment>
}
ReactDOM.render(<App/>, document.querySelector('#app'))
</script>
valueChanged is a closure which gets a fresh items every render. But your memo doesn't trigger an update because it only checks (o, n) => o.value === n.value. The event handler inside Item uses the old items value.
It can be fixed with functional updates:
const valueChanged = (i, newVal) => {
setItems(oldItems => {
let updatedItems = [...oldItems];
updatedItems[i].title = newVal;
return updatedItems;
});
}
So valueChanged doesn't depend on items and doesn't need to be checked by memo.
Your code might have similar problems with other handlers. It is better to use functional updates whenener new value is based on the old one.
You can try initializing the state this way, I tried it on codesandbox and it may be the behavior you are looking for.
https://codesandbox.io/s/q8l6m2lj64
const [items, setItems] = useState(() => [
{ key: randomStr(), title: 'abc' },
{ key: randomStr(), title: 'def' },
]);