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>
);
};
Related
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`))
As Dan Abramov introduced, I wrote an interval using useRef so it does not get created on every render. My code below saves the redux store into local storage every 20 seconds. However, the store inside of setInterval does not get updated which leads to saving the same initial store every 20 seconds.
import { useRef, useState } from "react";
import { useSelector } from "react-redux";
import styles from "./AutoSaveButton.module.scss";
import LOCAL_STORAGE_KEY from "../../../constants/localStorage";
const TWO_SECONDS = 2000;
const TWENTY_SECONDS = 20000;
function AutoSaveButton() {
const store = useSelector((state) => state);
const interval = useRef(null);
const [isAutoSaveOn, setIsAutoSaveOn] = useState(false);
const [isSavedMsgDisplayed, setIsSavedMsgDisplayed] = useState(false);
const toggleAutoSave = () => {
if (isAutoSaveOn) {
setIsAutoSaveOn(false);
clearInterval(interval.current);
return;
}
setIsAutoSaveOn(true);
interval.current = setInterval(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(store)); <-- how to update?
setIsSavedMsgDisplayed(true);
setTimeout(() => {
setIsSavedMsgDisplayed(false);
}, TWO_SECONDS);
}, TWENTY_SECONDS);
};
return (
<span
className={isAutoSaveOn ? styles.active : styles.stale}
onClick={toggleAutoSave}
>
{isAutoSaveOn ? "auto save off" : "auto save on"}
{isAutoSaveOn && (
<span
className={`material-symbols-outlined ${
isSavedMsgDisplayed ? styles["icon-visible"] : styles["icon-greyed"]
}`}
>
done
</span>
)}
</span>
);
}
export default AutoSaveButton;
since closure will always capture the variable store each time it's reinitialized. use [YOUR STORE].getState().[your reducer] instead
const { useRef, useState } = React
const { useSelector,Provider,useDispatch } = ReactRedux
const { configureStore,createSlice } = window.RTK
//import styles from "./AutoSaveButton.module.scss";
const LOCAL_STORAGE_KEY = "XXX"
const slice = createSlice({
name: 'data',
initialState: {
value: 0,
},
reducers: {
setData: (state, action) => {
state.value += action.payload
},
},
})
const mystore=configureStore({
reducer: {
data: slice.reducer
},
})
const TWO_SECONDS = 2000;
const TWENTY_SECONDS = 20000;
function AutoSaveButton() {
const store = useSelector((state) => state.data);
const interval = useRef(null);
const [isAutoSaveOn, setIsAutoSaveOn] = useState(false);
const [isSavedMsgDisplayed, setIsSavedMsgDisplayed] = useState(false);
const toggleAutoSave = (store) => {
if (isAutoSaveOn) {
setIsAutoSaveOn(false);
clearInterval(interval.current);
return;
}
setIsAutoSaveOn(true);
console.log("store outside", store);
interval.current = setInterval(() => {
setIsSavedMsgDisplayed(true);
console.log("store inside", mystore.getState().data);
setTimeout(() => {
setIsSavedMsgDisplayed(false);
}, TWO_SECONDS);
//localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(mystore.getState().data)); //<-- how to update?
}, 1000);
};
return (
<span
className=""
onClick={()=>toggleAutoSave(store)}
>
{isAutoSaveOn ? "auto save off" : "auto save on"}
{isAutoSaveOn && (
<span
className={`material-symbols-outlined `}
>
done
</span>
)}
</span>
);
}
const App=()=>{
return <Provider store={mystore}>
<AutoSaveButton />
<B/>
</Provider>
}
const B=()=>{
const dispatch=useDispatch()
return <button onClick={()=>dispatch(slice.actions.setData(12))}>
setData
</button>
}
ReactDOM.render(<App/>,document.getElementById("App"))
<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.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/8.0.2/react-redux.min.js"></script>
<script src="https://unpkg.com/#reduxjs/toolkit#1.8.3/dist/redux-toolkit.umd.js"></script>
<div id="App">
</div>
second way watchc store with useEffect and put it to ref
const { useRef, useState, useEffect} = React
const { useSelector,Provider,useDispatch } = ReactRedux
const { configureStore,createSlice } = window.RTK
//import styles from "./AutoSaveButton.module.scss";
const LOCAL_STORAGE_KEY = "XXX"
const slice = createSlice({
name: 'data',
initialState: {
value: 0,
},
reducers: {
setData: (state, action) => {
state.value += action.payload
},
},
})
const mystore=configureStore({
reducer: {
data: slice.reducer
},
})
const TWO_SECONDS = 2000;
const TWENTY_SECONDS = 20000;
function AutoSaveButton() {
const store = useSelector((state) => state.data);
const interval = useRef(null);
const mystore = useRef(store);
useEffect(()=>{
mystore.current=store
},[store])
const [isAutoSaveOn, setIsAutoSaveOn] = useState(false);
const [isSavedMsgDisplayed, setIsSavedMsgDisplayed] = useState(false);
const toggleAutoSave = () => {
if (isAutoSaveOn) {
setIsAutoSaveOn(false);
clearInterval(interval.current);
return;
}
setIsAutoSaveOn(true);
console.log("store outside", store);
interval.current = setInterval(() => {
setIsSavedMsgDisplayed(true);
console.log("store inside", mystore.current);
setTimeout(() => {
setIsSavedMsgDisplayed(false);
}, TWO_SECONDS);
//localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(mystore.current); //<-- how to update?
}, 1000);
};
return (
<span
className=""
onClick={toggleAutoSave}
>
{isAutoSaveOn ? "auto save off" : "auto save on"}
{isAutoSaveOn && (
<span
className={`material-symbols-outlined `}
>
done
</span>
)}
</span>
);
}
const App=()=>{
return <Provider store={mystore}>
<AutoSaveButton />
<B/>
</Provider>
}
const B=()=>{
const dispatch=useDispatch()
return <button onClick={()=>dispatch(slice.actions.setData(12))}>
setData
</button>
}
ReactDOM.render(<App/>,document.getElementById("App"))
<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.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/8.0.2/react-redux.min.js"></script>
<script src="https://unpkg.com/#reduxjs/toolkit#1.8.3/dist/redux-toolkit.umd.js"></script>
<div id="App">
</div>
I think I found an answer myself, but if any of you find this dumb, please leave a comment. I am open to discussions!
import { useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import styles from "./AutoSaveButton.module.scss";
import LOCAL_STORAGE_KEY from "../../../constants/localStorage";
const TWO_SECONDS = 2000;
const TWENTY_SECONDS = 5000;
function AutoSaveButton() {
const store = useSelector((state) => state);
const interval = useRef(null);
const [isAutoSaveOn, setIsAutoSaveOn] = useState(false);
const [isSavedMsgDisplayed, setIsSavedMsgDisplayed] = useState(false);
const toggleAutoSave = () => {
if (isAutoSaveOn) {
setIsAutoSaveOn(false);
clearInterval(interval.current);
return;
}
setIsAutoSaveOn(true);
interval.current = setInterval(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(store));
setIsSavedMsgDisplayed(true);
setTimeout(() => {
setIsSavedMsgDisplayed(false);
}, TWO_SECONDS);
}, TWENTY_SECONDS);
};
/* I added below useEffect and got desired result! */
useEffect(() => {
if (!interval.current) return;
clearInterval(interval.current);
interval.current = setInterval(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(store));
setIsSavedMsgDisplayed(true);
setTimeout(() => {
setIsSavedMsgDisplayed(false);
}, TWO_SECONDS);
}, TWENTY_SECONDS);
}, [store]);
return (
<span
className={isAutoSaveOn ? styles.active : styles.stale}
onClick={toggleAutoSave}
>
{isAutoSaveOn ? "auto save off" : "auto save on"}
{isAutoSaveOn && (
<span
className={`material-symbols-outlined ${
isSavedMsgDisplayed ? styles["icon-visible"] : styles["icon-greyed"]
}`}
>
done
</span>
)}
</span>
);
}
export default AutoSaveButton;
I have a parent/child component where when there is a swipe event occurring in the child the parent component should fetch a new profile. The problem is the useEffect in the child component to set up the eventListeneners currently is not running, only occasionally on hot-reload which in reality should run basically every time.
Child component
function Profile(props: any) {
const [name] = useState(`${props.profile.name.title} ${props.profile.name.first} ${props.profile.name.last}`);
const [swiped, setSwiped] = useState(0)
const backgroundImage = {
backgroundImage: `url(${props.profile.picture.large})`
};
const cardRef = useRef<HTMLDivElement>(null);
const card = cardRef.current
let startX:any = null;
function unify (e:any) { return e.changedTouches ? e.changedTouches[0] : e };
function lock (e:any) { if (card) {startX = unify(e).clientX; console.log(startX)} }
function move (e: any) {
console.log('move')
if(startX) {
let differenceX = unify(e).clientX - startX, sign = Math.sign(differenceX);
if(sign < 0 || sign > 0) {
setSwiped((swiped) => swiped +1)
props.parentCallback(swiped);
startX = null
}
}
}
// Following code block does not work
useEffect(() => {
if (card) {
console.log(card)
card.addEventListener('mousedown', lock, false);
card.addEventListener('touchstart', lock, false);
card.addEventListener('mouseup', move, false);
card.addEventListener('touchend', move, false);
}
})
return (
<div>
<h1 className="heading-1">{name}</h1>
<div ref={cardRef} className="card" style={backgroundImage}>
</div>
</div>
);
}
Parent component
function Profiles() {
const [error, setError] = useState<any>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [profiles, setProfiles] = useState<any[]>([]);
const [swiped, setSwiped] = useState(0)
useEffect(() => {
getProfiles()
}, [swiped])
const callback = useCallback((swiped) => {
setSwiped(swiped);
console.log(swiped);
}, []);
const getProfiles = () => {
fetch("https://randomuser.me/api/")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setProfiles(result.results);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}
if (error) {
return <h1 className="heading-1">Error: {error.message}</h1>;
} else if (!isLoaded) {
return <h1 className="heading-1">Loading...</h1>;
} else {
return (
<div id="board">
{profiles.map(profile => (
<Profile key={profile.id.value} profile={profile} parentCallback={callback}/>
))}
</div>
);
}
}
If you want the parent components swiped state to change, you need to pass "setSwiped" from the parent to the child compenent. You will also need to pass "swiped" to the child to use its current value to calculate the new value. I'm going to assume you declared the useState in the child component trying to set the parents state of the same name, so I'm going to remove that useState Declaration in the child altogether.
Here's an example of passing the setSwiped method and swiped value to the child:
PARENT
import React, {useState, useEffect, useCallback} from 'react';
import './Index.css';
import Profile from './Profile'
function Profiles() {
const [error, setError] = useState<any>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [profiles, setProfiles] = useState<any[]>([]);
const [swiped, setSwiped] = useState(0)
useEffect(() => {
getProfiles()
}, [swiped])
const callback = useCallback((swiped) => {
setSwiped(swiped);
console.log(swiped);
}, []);
const getProfiles = () => {
fetch("https://randomuser.me/api/")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setProfiles(result.results);
},
(error) => {
setIsLoaded(true);
setError(error);
}
)
}
if (error) {
return <h1 className="heading-1">Error: {error.message}</h1>;
} else if (!isLoaded) {
return <h1 className="heading-1">Loading...</h1>;
} else {
return (
<div id="board">
{profiles.map(profile => (
<Profile key={profile.id.value} profile={profile} parentCallback={callback} setSwiped={setSwiped} swiped={swiped}/>
))}
</div>
);
}
}
export default Profiles;
CHILD
import React, {useState, useRef, useEffect } from 'react';
import './Index.css';
function Profile(props: any) {
const [name] = useState(`${props.profile.name.title} ${props.profile.name.first} ${props.profile.name.last}`);
const backgroundImage = {
backgroundImage: `url(${props.profile.picture.large})`
};
const cardRef = useRef<HTMLDivElement>(null);
const card = cardRef.current
let startX:any = null;
function unify (e:any) { return e.changedTouches ? e.changedTouches[0] : e };
function lock (e:any) { if (card) {startX = unify(e).clientX; console.log(startX)} }
function move (e: any) {
console.log('move')
if(startX) {
let differenceX = unify(e).clientX - startX, sign = Math.sign(differenceX);
if(sign < 0 || sign > 0) {
props.setSwiped((props.swiped) => props.swiped +1)
props.parentCallback(props.swiped);
startX = null
}
}
}
useEffect(() => {
if (card) {
console.log(card)
card.addEventListener('mousedown', lock, false);
card.addEventListener('touchstart', lock, false);
card.addEventListener('mouseup', move, false);
card.addEventListener('touchend', move, false);
}
})
return (
<div>
<h1 className="heading-1">{name}</h1>
<div ref={cardRef} className="card" style={backgroundImage}>
</div>
</div>
);
}
export default Profile;
I'm hoping I didn't miss anything here.
Best of luck.
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.
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>