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`))
Related
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>
);
};
I want to create a ref to an element, save it in state and use it somewhere else, down the line. Here is what I have so far:
const Header = () => {
const topElement = useRef();
const { setRootElement } = useScrollToTop();
useEffect(() => {
setRootElement(topElement);
}, []);
return (
<div ref={topElement}>
...
</div>
)
}
The useScrollToTop hook:
export const useScrollToTop = () => {
const [rootElement, setRootElement] = useState();
const scrollToTop = () => {
rootElement.current.scrollIntoView();
};
return {
scrollToTop: scrollToTop,
setRootElement: setRootElement
};
};
And in a different component:
const LongList = () => {
const { scrollToTop } = useScrollToTop();
return (
<div>
....
<button onClick={() => scrollToTop()} />
</div>
);
}
The setRootElemet works okay, it saves the element that I pass to it but when I call scrollToTop() the element is undefined. What am I missing here?
As hooks are essentially just functions, there is no state shared between calls. Each time you call useScrollToTop you are getting a new object with its own scrollToTop and setRootElement. When you call useScrollToTop in LongList, the returned setRootElement is never used and therefore that instance rootElement will never have a value.
What you need to do is have one call to useScrollToTop and pass the returned items to their respective components. Also, instead of using a state in the hook for the element, you can use a ref directly and return it.
Putting these together, assuming you have an App structure something like:
App
Header
LongList
Hook:
export const useScrollToTop = () => {
const rootElement = useRef();
const scrollToTop = () => {
rootElement.current.scrollIntoView();
};
return {
scrollToTop,
rootElement,
};
};
App:
...
const { scrollToTop, rootElement } = useScrollToTop();
return (
...
<Header rootElementRef={rootElement} />
<LongList scrollToTop={scrollToTop} />
...
);
Header:
const Header = ({ rootElementRef }) => {
return (
<div ref={rootElementRef}>
...
</div>
);
}
LongList:
const LongList = ({ scrollToTop }) => {
return (
<div>
...
<button onClick={() => scrollToTop()} />
</div>
);
}
The issue probably is topElement would be null initially and useEffect would trigger setRootElement with null. You would need to keep topElement in state variable and check when it changes and set the value inside your JSX as
const [topElement, setTopElement] = useState(null);
useEffect(() => {topElement && setRootElement(topElement);}, [topElement])
return (
<div ref={(ref) => setTopElement(ref)}>
...
</div>
);
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.
function TypeArticleOne(props) {
let apiData = props.apiData;
const [ therapists, setTherapists ] = useState(apiData.topProfiles.therapists);
const [speciality, setSpeciality]= useState('ABA');
const [pageLoading, setPageLoading]= useState(true);
const topProfilesUrl = 'therapists/top/profiles'
useEffect(() => {
console.log(speciality);
getTopTherapists();
window.scrollTo(0, 0);
}, []);
const getTopTherapists = () => {
setPageLoading(true);
loadTopTherapists();
};
const loadTopTherapists = () => {
console.log("second");
props.actions.reqGetTherapistsTopProfiles({
body: {},
headers: null,
resource: `${topProfilesUrl}`
})
};
useEffect(() => {
if (apiData.topProfiles && apiData.topProfiles.success) {
const therapistsLoad = apiData.topProfiles.therapists;
setPageLoading(false);
setTherapists([therapists].concat(therapistsLoad));
}
}, []);
How to map an array in a functional component? I want to map the therapists array from the functional component above. React is suggesting me to use UseRef(), because I have a functional component and I am using hooks, but it's not clear for me.
I call the therapists in an array from an database and I need to map them to render in a card, inside a functional component. For now I can access the array elements, but I need to access some specific parameters of the objects. Could you help me guys?
const renderTherapists = (props) => {
const items = props.therapists.map( (t, idx) => (
<TherapistCard therapist={t} key={idx} />
))
return (
<div ref={0} className="therapist-list">
{ items }
</div>
)
}
Rather than declaring a const, it is better to directly map the props like this:
const RenderTherapists = props => {
return (
<div className="therapist-list">
{ props.therapists.map((t, idx) => {
return <TherapistCard therapist={t} key={idx} />
})}
</div>
)
}
export default RenderTherapists;
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>