React hooks hover effect - reactjs

I am trying to make hover effect with react hooks
I wrote function to hover based on some tutorials
function useHover() {
const [hovered, setHovered] = useState(false);
const ref = useRef(null);
const handleMouseOver = () => setHovered(true);
const handleMouseOut = () => setHovered(false);
useEffect(() => {
const node = ref.current;
if (node) {
node.addEventListener("mouseover", handleMouseOver);
node.addEventListener("mouseout", handleMouseOut);
return () => {
node.removeEventListener("mouseover", handleMouseOver);
node.removeEventListener("mouseout", handleMouseOut);
};
}
}, [ref]);
return [ref, hovered];
}
but how to make it work in my App function
export default function App() {
const [ref, isHovered] = useHover();
const reactionItems = myObject.map(([key, value]) => (
<li key={key} ref={ref}>
{isHovered ? `${key} ${value.length > 1 ? "x " + value.length : ""}` : `${key} ${value.length > 1 ? "x " + value.length : ""} ${value}`}
</li>
));
return (
<div className="App">
<h1>{string}</h1>
<h2>Reactions</h2>
<ul>{reactionItems}</ul>
</div>
);
}
I can see it only in state false so second option and no hover effect

Use React's events' system, and not the DOM's. In addition, each item should have it's own event handlers, and state.
Create a hook that returns the hovered state, and the events' listeners of an item. Create an Item component, and use the hook in it's definition. Render the items.
const { useState, useMemo } = React;
const useHover = () => {
const [hovered, setHovered] = useState();
const eventHandlers = useMemo(() => ({
onMouseOver() { setHovered(true); },
onMouseOut() { setHovered(false); }
}), []);
return [hovered, eventHandlers];
}
const Item = ({ children }) => {
const [hovered, eventHandlers] = useHover();
return (
<li {...eventHandlers}>Item: {hovered && children}</li>
);
};
const myObject = {
a: 'A1',
b: 'B2',
c: 'C3',
}
function App() {
const reactionItems = Object.entries(myObject)
.map(([key, value]) => (
<Item key={key}>{value}</Item>
));
return (
<div className="App">
<h2>Reactions</h2>
<ul>{reactionItems}</ul>
</div>
);
}
ReactDOM.render(<App />, root);
<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>
<div id="root"></div>

A way to do this is to use React's events, and just make sure you let it be more generic.
One of the issues you were running into is that ref can only refer to a single node at a time. And ref never changes dependencies, so the useEffect only ever ran once.
const { useState, useRef, useEffect, useCallback } = React;
function useHover() {
const [hovered, setHovered] = useState({});
const mouseOver = useCallback((event) => {
const target = event.target;
const key = target.getAttribute('data-key');
setHovered((curState) => ({ ...curState, [key]: true }));
}, []);
const mouseOut = useCallback((event) => {
const target = event.target;
const key = target.getAttribute('data-key');
setHovered((curState) => ({ ...curState, [key]: false }));
}, []);
return { mouseOver, mouseOut, hovered };
}
const object = { key1: 'test', key2: 'test2', key3: 'test3' };
const myObject = Object.entries(object);
const string = 'Header';
function App() {
const { mouseOver, mouseOut, hovered } = useHover();
const reactionItems = myObject.map(([key, value]) => (
<li key={key} data-key={key} onMouseOver={mouseOver} onMouseOut={mouseOut}>
{hovered[key]
? `${key} ${value.length > 1 ? 'x ' + value.length : ''}`
: `${key} ${value.length > 1 ? 'x ' + value.length : ''} ${value}`}
</li>
));
return (
<div className="App">
<h1>{string}</h1>
<h2>Reactions</h2>
<ul>{reactionItems}</ul>
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
You can also separate the list items out to their own component which would enable you to work with the useHover more closely to how you had it.

Related

Costume Hook says array of elements is not iterable

I am trying to create a costume hook that calls IntersectionObserver for all of the li elements inside a ref.
When I just use it in the component, it works great:
const App = () => {
const array = [...Array(50).keys()];
const ref = React.useRef(null);
React.useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((element: any) => {
if (element.isIntersecting) {
element.target.style.color = 'green';
}
})
})
ref.current.querySelectorAll(`li`).forEach(element => {
observer.observe(element);
});
}, [ ]);
return (
<ul ref={ref}>
{array.map((_, i) => <li key={i}>{i}</li>)}
</ul>
)
}
ReactDOM.createRoot(document.getElementById(`root`))
.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script><div id="root"></div>
But When I separate it into a costume hook, it says it is not iterable:
const App = () => {
const array = [...Array(50).keys()];
const ref = React.useRef(null);
const useCostumeHook = (elements) => {
React.useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((element: any) => {
if (element.isIntersecting) {
element.target.style.color = 'green';
}
})
})
elements.forEach(element => {
observer.observe(element);
});
}, [ ]);
}
useCostumeHook(ref?.current.querySelectorAll(`li`))
return (
<ul ref={ref}>
{array.map((_, i) => <li key={i}>{i}</li>)}
</ul>
)
}
ReactDOM.createRoot(document.getElementById(`root`))
.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script> <div id="root"></div>
Is there a way to do this with just giving the costume hook an array of elements?
THANKS! 😊
When you call the hook, and pass ref.current?.querySelectorAll(li), the ref.current is still null, so the result is undefined which not iterable.
Instead pass a function that would allow the useEffect block to get the items when the ref is already set:
const useCostumeHook = (getElements) => {
React.useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((element: any) => {
if (element.isIntersecting) {
element.target.style.color = 'green';
}
})
})
const elements = getElements();
elements?.forEach(element => {
observer.observe(element);
});
}, []);
}
Usage:
useCostumeHook(() => ref.current?.querySelectorAll(`li`))

React setState returns function dispatch()

I have met issues setting the state of the const displayClipName in the following function which is blocking despite the fact that I passed the props from the parent element to the child.
const audioClips = [
{
keyCode: 67,
keyTrigger: "C",
id: "Closed-HH",
src: "https://s3.amazonaws.com/freecodecamp/drums/Cev_H2.mp3"
}
]
function App() {
const [displayClipName, setDisplayClipName] = React.useState('Click a key!')
return (
<div id="drum-machine" className="text-white text-center">
<div className="container bg-info">
<h1>FCC - Drum Machine</h1>
{audioClips.map((clip) => (
<Pad
key={clip.id}
clip={clip}
setDisplayClipName={setDisplayClipName}
/>
))}
<h2>{displayClipName}</h2>
</div>
</div>
)
}
const Pad = ({clip, setDisplayClipName}) => {
const [playing, setPlaying] = React.useState(false)
React.useEffect(() => {
document.addEventListener('keydown', handleKey);
return () => {
document.removeEventListener('keydown', handleKey)
}
}, [])
const handleKey = (e) => {
if(e.keyCode === clip.keyCode) {
playSound()
}
}
const playSound = () => {
const audioPlay = document.getElementById(clip.keyTrigger);
const clipName = document.getElementById(clip.id)
setPlaying(true);
setTimeout(() => setPlaying(false), 300);
audioPlay.currentTime = 0;
audioPlay.play();
setDisplayClipName(clipName);
console.log(setDisplayClipName)
}
return (
<div onClick={playSound} id={`drum-pad-${clip.keyTrigger}`}>
<audio src={clip.src} className="clip" id={clip.keyTrigger}/>
{clip.keyTrigger}
</div>
)
}
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(<App />);
The console returns the following message:
function dispatchSetState()
​
length: 1
​
name: "bound dispatchSetState"
​
<prototype>: function ()
As some have pointed out in comments to your post it'd be better if you used refs. Also you were logging a function that's why the console displayed that. I have taken the liberty to do some modifications to your code so I could understand it better, I would suggest you keep the ones you find reasonable:
The displayName variable has an undefined state when no song is playing. This could be set from any other part of the application but you wouldn't depend on rerendering the component for it to return to a default message ("Press a key!")
The playSound function could be bound to the id of the song and you would avoid having to check the HTML element that received the input.
Here is a working snippet.
const { useState, useEffect } = React;
const audioClips = [
{
keyCode: 67,
keyTrigger: "C",
id: "Closed-HH",
src: "https://s3.amazonaws.com/freecodecamp/drums/Cev_H2.mp3"
}
]
const Pad = ({ clip, setDisplayClipName }) => {
const [playing, setPlaying] = useState(false);
useEffect(() => {
document.addEventListener("keydown", handleKey);
return () => {
document.removeEventListener("keydown", handleKey);
};
}, []);
const handleKey = (e) => {
if (e.keyCode === clip.keyCode) {
playSound(clip.id);
}
};
const playSound = (clipId) => {
const audioPlay = document.getElementById(clip.keyTrigger);
setPlaying(true);
setTimeout(() => setPlaying(false), 300);
audioPlay.currentTime = 0;
audioPlay.play();
setDisplayClipName(clipId);
};
return (
<div onClick={() => playSound(clip.id)} id={`drum-pad-${clip.keyTrigger}`}>
<audio src={clip.src} className="clip" id={clip.keyTrigger} />
{clip.keyTrigger}
</div>
);
};
function App() {
const [clipName, setClipName] = useState(undefined);
return (
<div id="drum-machine" className="text-white text-center">
<div className="container bg-info">
<h1>FCC - Drum Machine</h1>
{audioClips.map((clip) => (
<Pad key={clip.id} clip={clip} setDisplayClipName={setClipName} />
))}
<h2>{clipName ? clipName : "Press a key!"}</h2>
</div>
</div>
);
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<App />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
To further refine the Pad component and avoid the missing dependencies on the useEffect hook I would suggest you model it like this, using useMemo:
export const Pad = ({ clip, setDisplayClipName }) => {
const [playing, setPlaying] = useState(false);
const playSound = useMemo(
() => () => {
const audioPlay = document.getElementById(clip.keyTrigger);
setPlaying(true);
setTimeout(() => setPlaying(false), 300);
if (audioPlay) {
audioPlay.currentTime = 0;
audioPlay.play();
}
setDisplayClipName(clip.id);
},
[setDisplayClipName, clip]
);
useEffect(() => {
const handleKey = (e) => {
if (e.keyCode === clip.keyCode) {
playSound();
}
};
document.addEventListener("keydown", handleKey);
return () => {
document.removeEventListener("keydown", handleKey);
};
}, [playSound, clip.keyCode]);
return (
<div onClick={playSound} id={`drum-pad-${clip.keyTrigger}`}>
<audio src={clip.src} className="clip" id={clip.keyTrigger} />
{clip.keyTrigger}
</div>
);
};

React useState, how to reset to initial value

I have a problem in this code. I need reset item array in line but don't work.
if (!(element.name in filter){setItem([])}
import React, { useState, useEffect, cloneElement } from 'react';
import {fetchData} from '../helpers/fetchData';
function MainElement() {
const [data, setData] = useState([]);
const [filter, setFilter] = useState({});
const [item, setItem] = useState([]);
useEffect( () => {
fetchData("http://localhost/cataleg/php/p3filtres.php")
.then( category => {
setData(category);
});
}, [])
return (
<div onChange={
(e) => {
const product = document.querySelectorAll('input[type="checkbox"]:checked');
[...product].map((element) => {
if (!(element.name in filter){
setItem([]);
}else
setItem([...item, element.value])
return filter[element.name]=item;
})
}
}
>
{
data && Object.entries(data).map((element,i) => {
return (
<div key={i}>
<h3 key={element[0]}>{element[0]}</h3>
{
element[1].map(item => {
return (
Object.entries(item).map((value,i) => {
if (i%2)
return <div key={i}>
<input type="checkbox" key={value[1]} name={element[0]} value={value[1]}/>
<label key={`${value[1]}-${i}`} htmlFor={value[1]}>{value[1]}</label>
</div>
})
)
})
}
</div>
)
})
}
</div>
)
}
export default MainElement;
enter image description here
When ckeckbox is checked I want to store in an object like:
{'processor': [I7,I5], 'RAM':['4GB','8GB']}
So I need reset item array for each key in the object. In may code item array is [I7,I5,'4GB','8GB']
There is a list of things worth fixing in your codes.
onChange has no effect on div
document.querySelector and document.querySelectorAll should not be used inside a React component
Include the <input> checkboxes as part of your app
See useRef if you must reference other elements
.map is being used like .forEach
setItem([...item, element.value]) is called for every element regardless of your condition
filtre is misspelled
item is derived state that should not have its own state
Use data.filter to compute item
See Single Source of Truth
Here is a minimal complete example you can run in your browser.
function fetchProducts() {
return new Promise(resolve =>
setTimeout(resolve, 1000, [
{"name": "carrot", type: "vegetable", quantity: 6 },
{"name": "potato", type: "vegetable", quantity: 0 },
{"name": "pretzels", type: "snack", quantity: 3 }
]))
}
function App() {
const [products, setProducts] = React.useState([])
const [isVegetable, veggieFilter] = useCheckbox(false)
const [isStocked, stockFilter] = useCheckbox(false)
React.useEffect(_ => {
fetchProducts().then(setProducts).catch(console.error)
}, [])
function filter() {
return products
.filter(p => !isVegetable || p.type == "vegetable")
.filter(p => !isStocked || p.quantity > 0)
}
return <div>
Vegetable:{veggieFilter} In-Stock:{stockFilter}
{filter().map(p => <pre>{JSON.stringify(p)}</pre>)}
</div>
}
function useCheckbox(initValue = false) {
const [checked, setChecked] = React.useState(initValue)
return [
checked,
<input type="checkbox" checked={checked} onClick={_ => setChecked(!checked)} />
]
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>

React State returning out of sync data

I have the below component which is using React hooks:
import React, {useState} from 'react';
// import components
import TagManagementRow from './TagManagementRow';
const TagManagementList = (props) => {
const [tagData, setTagData] = useState(props.data);
const deleteAction = (id) => {
// Call to backend to delete tag
const currentData = [];
for( var i = 0; i <= tagData.length; i++){
if(i < tagData.length && tagData[i].id !== id) {
currentData.push(tagData[i]);
}
if(i === tagData.length) setTagData(currentData);
};
};
return (
<ul className="tagManagement">
{tagData.map( (tag,i) => {
return <TagManagementRow name={tag.name} key={i} id={tag.id} delete={() => deleteAction(tag.id)} />
})}
</ul>
);
}
export default TagManagementList;
It renders 4 TagManagementRow child components, each have a delete button. When I click the delete button, everything looks good if I log out the changed state to the console, however, in the actual browser the last item in the list is removed. I feel like its some kind of render/timing issue but I can't seem to figure it out. Any assistance from those who better understand hooks would be greatly appreciated.
By the way, here is the code for the TagManagementRow component:
import React, { useState } from 'react';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
const TagManagementRow = (props) => {
const [editTag, setEdit] = useState(false);
const [tagName, setTagName] = useState(props.name);
const [tempName, setTempName] = useState('');
const handleEdit = (e) => {
setTempName(e.target.value);
};
const switchToEdit = () => {
setEdit(!editTag);
}
const saveEdit = () => {
setTagName(tempName);
setTempName('');
switchToEdit();
}
return (
<li>
<span>
{tagName}
<FontAwesomeIcon icon={["fas","pen"]} onClick={switchToEdit} />
</span>
<span>
<FontAwesomeIcon icon={["fas","trash-alt"]} onClick={props.delete} />
</span>
</li>
);
}
export default TagManagementRow;
Instead of updating the state inside the loop, you could use filter to filter out the object with the matching id.
Also make sure you use tag.id as key instead of the array index, since that will change when you remove an element.
const { useState } = React;
const TagManagementList = props => {
const [tagData, setTagData] = useState(props.data);
const deleteAction = id => {
setTagData(prevTagData => prevTagData.filter(tag => tag.id !== id));
};
return (
<ul className="tagManagement">
{tagData.map((tag, i) => {
return (
<TagManagementRow
name={tag.name}
key={tag.id}
id={tag.id}
delete={() => deleteAction(tag.id)}
/>
);
})}
</ul>
);
};
const TagManagementRow = props => {
const [editTag, setEdit] = useState(false);
const [tagName, setTagName] = useState(props.name);
const [tempName, setTempName] = useState("");
const handleEdit = e => {
setTempName(e.target.value);
};
const switchToEdit = () => {
setEdit(!editTag);
};
const saveEdit = () => {
setTagName(tempName);
setTempName("");
switchToEdit();
};
return (
<li>
{tagName}
<button onClick={props.delete}>Delete</button>
</li>
);
};
ReactDOM.render(
<TagManagementList data={[{ id: 1, name: "foo" }, { id: 2, name: "bar" }]} />,
document.getElementById("root")
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>

How to avoid extra renders in my component to use react hooks

I try to use react hooks instead of class-based components and have some problem with performance.
Code:
import React, { memo, useCallback, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
let counter = -1;
function useToggle(initialValue) {
const [toggleValue, setToggleValue] = useState(initialValue);
const toggler = useCallback(() => setToggleValue(!toggleValue), [
toggleValue,
setToggleValue
]);
return [toggleValue, toggler];
}
const Header = memo(({ onClick }) => {
counter = counter + 1;
return (
<div>
<h1>HEADER</h1>
<button onClick={onClick}>Toggle Menu</button>
<div>Extra Render: {counter}</div>
</div>
);
});
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
const handleMenu = useCallback(
() => {
toggle(!visible);
},
[toggle, visible]
);
return (
<>
<Header onClick={handleMenu} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
export default Dashboard;
Here is an example of what I wanna do: Example.
As you see, there are extra renders in my Header component.
My question: Is it possible to avoid extra renders to use react-hooks?
Change your custom hook useToggle to use functional state setter, like this
function useToggle(initialValue) {
const [toggleValue, setToggleValue] = useState(initialValue);
const toggler = useCallback(() => setToggleValue(toggleValue => !toggleValue));
return [toggleValue, toggler];
}
and use it like this :
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
const handleMenu = useCallback(
() => {
toggle();
}, []
);
return (
<>
<Header onClick={handleMenu} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
Complete example : https://codesandbox.io/s/z251qjvpw4
Edit
This can be simpler (thanks to #DoXicK)
function useToggle(initialValue) {
const [toggleValue, setToggleValue] = useState(initialValue);
const toggler = useCallback(() => setToggleValue(toggleValue => !toggleValue), [setToggleValue]);
return [toggleValue, toggler];
}
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
return (
<>
<Header onClick={toggle} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
This is an issue with useCallback get invalidate too often. (there is a conversation about this on React repo here: https://github.com/facebook/react/issues/14099)
since useCallback will be invalidated every time toggle value change and return a new function, then passing a new handleMenu function to <Header /> cause it re-render.
A workaround solution is to create a custom useCallback hook:
(Copied from https://github.com/facebook/react/issues/14099#issuecomment-457885333)
function useEventCallback(fn) {
let ref = useRef();
useLayoutEffect(() => {
ref.current = fn;
});
return useMemo(() => (...args) => (0, ref.current)(...args), []);
}
Example: https://codesandbox.io/s/1o87xrnj37
If you use the callback pattern to update state, you would be able to avoid extra re-renders since the function need not be created again and again and you use just create handleMenu on first render
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
const handleMenu = useCallback(() => {
toggle(visible => !visible);
}, []);
return (
<>
<Header onClick={handleMenu} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
Working Demo

Resources