Better use of useEffect hooks as to not need use Callbacks - reactjs

I have written the following React component and it's messy with the the dependencies and need for
useCallback
My question is: What about hooks am I misunderstanding to where I need to use this useCallback all the time?
const TagPicker = ({ value, defaultValue, tags, updateTags }) => {
const thisPicker = useRef(null);
const thisInput = useRef(null);
// Managing Tags
const [ selectedTags, setSelectedTags ] = useState(value || defaultValue);
const getIds = useCallback(() => {
let ids = [];
for(let x in selectedTags) {
ids.push(selectedTags[x].id);
}
return ids;
}, [ selectedTags ]);
const addTag = useCallback(tag => {
let ids = getIds();
if(!ids.includes(tag.id)) setSelectedTags([ ...selectedTags, tag ]);
}, [ selectedTags, getIds ]);
const removeTag = tag => {
let ids = getIds();
if(ids.includes(tag.id)) setSelectedTags([ ...selectedTags].filter(t => (t.id !== tag.id)));
}
// Typed text / Filtered List
const [ text, setText ] = useState('');
const [ filteredList, setFilteredList ] = useState([]);
useEffect(() => {
let ids = getIds();
if(text.length) {
let filteredTags = tags.filter(tag => (!ids.includes(tag.id)));
setOpen(true);
let list = [];
for(let x in filteredTags) {
if(filteredTags[x].title.toLowerCase().includes(text.toLowerCase())) list.push(filteredTags[x]);
}
setFilteredList(list);
} else {
setOpen(false);
setFilteredList([])
}
}, [ selectedTags, tags, text, getIds ]);
useEffect(() => {
updateTags(selectedTags);
}, [ updateTags, selectedTags ]);
// Toggling dropdown menu
const [ open, setOpen ] = useState(false);
useEffect(() => {
if(open) getCoordinates(thisPicker);
}, [ open ]);
// Handling Coordinates
const [ coords, setCoords ] = useState(null);
const getCoordinates = ({ current }) => {
if(current) {
const rect = current.getBoundingClientRect();
setCoords({
left: rect.x,
top: rect.y + window.scrollY + 40
});
}
}
useEffect(() => {
window.addEventListener('resize', () => getCoordinates(thisPicker));
return () => window.removeEventListener('resize', () => getCoordinates(thisPicker));
}, []);
// Handle Enter
const handleEnter = useCallback(e => {
if(e.keyCode === 13 && text) {
if(filteredList.length) addTag(filteredList[0]);
else addTag({
id: Math.random(),
title: text
});
setText('');
setOpen(false);
}
}, [ addTag, text, filteredList ]);
useEffect(() => {
let el = thisInput.current;
el.addEventListener('keyup', e => handleEnter(e));
return () => el.removeEventListener('keyup', e => handleEnter(e));
}, [ handleEnter ]);
return(
<div className='tag-picker' ref={thisPicker}>
<input type='text' value={text} onChange={e => setText(e.target.value)} ref={thisInput} />
<div className='selected-tags'>
{ selectedTags.map(tag => (
<Tag tag={tag} key={tag.id} removeTag={removeTag} />
)) }
</div>
{ open ?
<Portal>
<List text={text} items={filteredList} onSelect={addTag} setOpen={setOpen} coords={coords} />
</Portal>
: null }
</div>
);
};

There are some issues with your first useEffect. It has an extra dependency selectedTags.
If you don’t use useCallback to wrap the callback, the effect indeed depends on selectedTags. If you use useCallback, the effect only depends on the callback.
Effectively their meaning are the same. If the dependencies of useCallback change, the callback will also be recalculated. Hence the change of selectedTags triggers the changes of the callback (your getIds) and this triggers the effect: Without useCallback you put the dependencies directly inside the dependencies array of useEffect so their changes directly trigger the effect.

Related

React | Collect State Values of Children Array and update Per Object and Save to PouchDB

Stackoverflow
problem
I have separate components that house Tiptap Editor tables. At first I had a save button for each Child Component which worked fine, but was not user friendly. I want to have a unified save button that will iterate through each child Table component and funnel all their editor.getJSON() data into an array of sections for the single doc object . Then finish it off by saving the whole object to PouchDB
What did I try?
link to the repo → wchorski/Next-Planner: a CRM for planning events built on NextJS (github.com)
Try #1
I tried to use the useRef hook and the useImperativeHandle to call and return the editor.getJSON(). But working with an Array Ref went over my head. I'll post some code of what I was going for
// Parent.jsx
const childrenRef = useRef([]);
childrenRef.current = []
const handleRef = (el) => {
if(el && !childrenRef.current.includes(el)){
childrenRef.current.push(el)
}
}
useEffect(() =>{
childrenRef.current[0].childFunction1() // I know this doesn't work, because this is where I gave up
})
// Child.jsx
useImperativeHandle(ref, () => ({
childFunction1() {
console.log('child function 1 called');
},
childFunction2() {
console.log('child function 2 called');
},
}))
Try #2
I set a state counter and passed it down as a prop to the Child Component . Then I update the counter to trigger a child function
// Parent.jsx
export const Planner = ({id, doc, rev, getById, handleSave, db, alive, error}) => {
const [saveCount, setSaveCount] = useState(0)
const handleUpdate = () =>{
setSaveCount(prev => prev + 1)
}
const isSections = () => {
if(sectionsState[0]) handleSave(sectionsState)
if(sectionsState[0] === undefined) console.log('sec 0 is undefined', sectionsState)
}
function updateSections(newSec) {
setsectionsState(prev => {
const newState = sectionsState.map(obj => {
if(!obj) return
if (obj.header === newSec.header) {
return {...obj, ...newSec}
}
// 👇️ otherwise return object as is
return obj;
});
console.log('newState', newState);
return newState;
});
}
useEffect(() => {
setsectionsState(doc.sections)
}, [doc])
return (<>
<button
title='save'
className='save'
onPointerUp={handleUpdate}>
Save to State <FiSave />
</button>
<button
style={{right: "0", width: 'auto'}}
title='save'
className='save'
onClick={isSections}>
Save to DB <FiSave />
</button>
{doc.sections.map((sec, i) => {
if(!sec) return
return (
<TiptapTable
key={i}
id={id}
rev={doc.rev}
getById={getById}
updateSections={updateSections}
saveCount={saveCount}
section={sec}
db={db}
alive={alive}
error={error}
/>
)
})}
</>)
// Child.jsx
export const TiptapTable = ((props, ref) => {
const {id, section, updateSections, saveCount} = props
const [currTimeStart, setTimeStart] = useState()
const [defTemplate, setdefTemplate] = useState('<p>loading<p>')
const [isLoaded, setIsLoaded] = useState(false)
const [notesState, setnotesState] = useState('')
const editor = useEditor({
extensions: [
History,
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow.extend({
content: '(tableCell | tableHeader)*',
}),
TableHeader,
TableCell,
],
// i wish it was this easy
content: (section.data) ? section.data : defTemplate,
}, [])
const pickTemplate = async (name) => {
try{
const res = await fetch(`/templates/${name}.json`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json()
setIsLoaded(true)
setdefTemplate(data)
console.log('defTemplate, ', defTemplate);
// return data
} catch (err){
console.warn('template error: ', err);
}
}
function saveData(){
console.log(' **** SAVE MEEEE ', section.header);
try{
const newSection = {
header: section.header,
timeStart: currTimeStart,
notes: notesState,
data: editor.getJSON(),
}
updateSections(newSection)
} catch (err){
console.warn('table update error: ', id, err);
}
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) saveData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
useEffect(() => {
setTimeStart(section.timeStart)
setnotesState(section.notes)
if(!section.data) pickTemplate(section.header).catch(console.warn)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, section, isLoaded])
useEffect(() => {
if (editor && !editor.isDestroyed) {
if(section.data) editor.chain().focus().setContent(section.data).run()
if(!section.data) editor.chain().focus().setContent(defTemplate).run()
setIsLoaded(true)
}
}, [section, defTemplate, editor]);
if (!editor) {
return null
}
return isLoaded ? (<>
<StyledTableEditor>
<div className="title">
<input type="time" label='Start Time' className='time'
onChange={(e) => setTimeStart(e.target.value)}
defaultValue={currTimeStart}
/>
<h2>{section.header}</h2>
</div>
<EditorContent editor={editor} className="tiptap-table" ></EditorContent>
// ... non relavent editor controls
<button
title='save'
className='save2'
onPointerUp={() => saveData()}>
Save <FiSave />
</button>
</div>
</nav>
</StyledTableEditor>
</>)
: null
})
TiptapTable.displayName = 'MyTiptapTable';
What I Expected
What I expected was the parent state to update in place, but instead it overwrites the previous tables. Also, once it writes to PouchDB it doesn't write a single piece of new data, just resolved back to the previous, yet with an updated _rev revision number.
In theory I think i'd prefer the useRef hook with useImperativeHandle to pass up the data from child to parent.
It looks like this question is similar but doesn't programmatically comb through the children
I realize I could have asked a more refined question, but instead of starting a new question I'll just answer my own question from what I've learned.
The problem being
I wasn't utilizing React's setState hook as I iterated and updated the main Doc Object
Thanks to this article for helping me through this problem.
// Parent.jsx
import React, {useState} from 'react'
import { Child } from '../components/Child'
export const Parent = () => {
const masterDoc = {
_id: "123",
date: "2023-12-1",
sections: [
{header: 'green', status: 'old'},
{header: 'cyan', status: 'old'},
{header: 'purple', status: 'old'},
]
}
const [saveCount, setSaveCount] = useState(0)
const [sectionsState, setsectionsState] = useState(masterDoc.sections)
function updateSections(inputObj) {
setsectionsState(prev => {
const newState = prev.map(obj => {
// 👇️ if id equals 2, update country property
if (obj.header === inputObj.header)
return {...obj, ...inputObj}
return obj;
});
return newState;
});
}
return (<>
<h1>Parent</h1>
{sectionsState.map((sec, i) => {
if(!sec) return
return (
<Child
key={i}
section={sec}
updateSections={updateSections}
saveCount={saveCount}
/>
)
})}
<button
onClick={() => setSaveCount(prev => prev + 1)}
>State dependant update {saveCount}</button>
</>)
}
// Child.jsx
import React, {useEffect, useState, forwardRef, useImperativeHandle} from 'react'
export const Child = forwardRef((props, ref) => {
const {section, updateSections, saveCount} = props
const [statusState, setStatusState] = useState(section.status)
function modData() {
const obj = {
header: section.header,
status: statusState
}
updateSections(obj)
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) modData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
return (<>
<span style={{color: section.header}}>
header: {section.header}
</span>
<span>status: {section.status}</span>
<input
defaultValue={section.status}
onChange={(e) => setStatusState(e.target.value)}
/>
________________________________________
</>)
})
Child.displayName = 'MyChild';

React replace state array using hooks

I am trying to replace a state array using a separately given array. I am unable to get my state to update to hold the values of the separate array. I've tried several things
const [userFriendsList, setUserFriendsList] = useState([]);
window.chatAPI.recvFriendsList(message => {
if (message.length > userFriendsList.length)
{
console.log(message)
setUserFriendsList(message);
console.log(userFriendsList)
}
});
const [userFriendsList, setUserFriendsList] = useState({friendsList: []});
window.chatAPI.recvFriendsList(message => {
if (message.length > userFriendsList['friendsList'].length)
{
console.log(message)
setUserFriendsList({friendsList : message});
console.log(userFriendsList)
}
});
const [userFriendsList, setUserFriendsList] = useState([]);
window.chatAPI.recvFriendsList(message => {
if (message.length > userFriendsList.length)
{
console.log(message)
setUserFriendsList([ ... userFriendsList, message]);
console.log(userFriendsList)
}
});
None of these are updating the state.
Edit:
Component -
const FriendsList = () =>
{
const [checkFriendsList, setCheckFriendsList] = useState(true)
const [userFriendsList, setUserFriendsList] = useState([]);
window.chatAPI.recvFriendsList(message => {
console.log(message)
setUserFriendsList(oldFriendList => [ ...oldFriendList, ...message]);
console.log(userFriendsList)
});
useEffect ( () => {
if (checkFriendsList)
{
setCheckFriendsList(false);
window.chatAPI.getFriendsList();
}
}, [checkFriendsList])
return (
<div className="friends-list-container">
<List className="friends-list">
<ListItem className="friends-list-title"> Friends List </ListItem>
</List>
</div>
);
}
output:
The problem is in the condition if (message.length > userFriendsList.length).
If message is a non empty-string it will always be longer that your empty userFriendsList state, remove the condition and update the array with:
const [userFriendsList, setUserFriendsList] = useState([]);
window.chatAPI.recvFriendsList(message => {
setUserFriendsList(oldFriendList => [ ...oldFriendList, message]);
});
If message is an array just do:
const [userFriendsList, setUserFriendsList] = useState([]);
window.chatAPI.recvFriendsList(message => {
setUserFriendsList(oldFriendList => [ ...oldFriendList, ...message]);
});
Try disabling the if statement.
// if( your condition)
{
setUserFriendsList([ ... userFriendsList, ...message]);
}

React life cycles and Intersection observer

I am building a image slider in React, based on CSS vertical snapping. There are 2 ways to interact with it, either throught scroll vertically or click the navigation buttons. I am using the Intersection Observer API in a React useEffect() to detect the active item. However, I can't seem to get it right without any useEffect lint errors. Whenever I include the functions in the dependecy array as suggested by the lint, the active item isn't set when scrolling.
Am I using a React anti pattern or am I just missing something?
Live demo
Code:
const Slider = ({images}) => {
const [currentSlide, SetCurrentSlide] = React.useState(0);
const setSlide = (id) => {
SetCurrentSlide(id);
};
const moveToSlide = (id) => {
if(id > -1 && id < images.length) {
SetCurrentSlide(id);
}
}
return (
<StyledSlider id="slider">
<SliderWrapper items={images} setSlide={setSlide} currentSlide={currentSlide} />
<SliderNav currentSlide={currentSlide} moveToSlide={moveToSlide} maxItems={images.length}/>
</StyledSlider>
)
}
const SliderWrapper = ({items, setSlide, currentSlide}) => {
const containerRef = React.useRef(null);
const { ref, inView, entry } = useInView({
/* Optional options */
threshold: 0,
});
const handleSetSlide = (id) => {
setSlide(id);
};
const handleIntersection = (entries) => {
const [entry] = entries;
const activeSlide = Number(entry.target.dataset.slide);
if (!entry.isIntersecting || activeSlide === "NaN") return;
handleSetSlide(activeSlide);
};
React.useEffect(() => {
const observer = new IntersectionObserver(
handleIntersection,
{
root: containerRef.current,
threshold: 0.45
}
);
Array.from(containerRef.current.children).forEach((item) => {
observer.observe(item);
});
return function() {
observer.disconnect();
}
}, [items]);
return (
<StyledSliderWrapper ref={containerRef} >
{items.map((item, index) => {
return <SliderItem key={index} index={index} image={item} isActive={currentSlide === index} />
})}
</StyledSliderWrapper>
)
};
const SliderItem = ({index, image, isActive}) => {
const imageContent = getImage(image.url);
const imageRef = React.useRef()
React.useEffect(() => {
if(!isActive) return;
imageRef.current.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
},[isActive]);
return (
<StyledSliderItem data-slide={index} ref={imageRef}>
<GatsbyImage image={imageContent} alt={image.description} />
</StyledSliderItem>
)
}
So you've missing dependencies in the useEffect of SliderWrapper. You can simplify the code a bit as well.
SliderWrapper
Since nothing else calls handleIntersection callback other than the Observer you can safely move it into the useEffect callback body. This makes the only dependency the setSlide callback that's passed as a prop from the parent component.
const SliderWrapper = ({ items, setSlide, currentSlide }) => {
const containerRef = React.useRef(null);
React.useEffect(() => {
const handleIntersection = (entries) => {
const [entry] = entries;
const activeSlide = Number(entry.target.dataset.slide);
if (!entry.isIntersecting || activeSlide === "NaN") return;
setSlide(activeSlide);
};
const observer = new IntersectionObserver(handleIntersection, {
root: containerRef.current,
threshold: 0.45
});
Array.from(containerRef.current.children).forEach((item) => {
observer.observe(item);
});
return function () {
observer.disconnect();
};
}, [setSlide]);
return (
<StyledSliderWrapper ref={containerRef}>
{items.map((item, index) => (
<SliderItem
key={index}
index={index}
image={item}
isActive={currentSlide === index}
/>
))}
</StyledSliderWrapper>
);
};
Slider
The other issue what that you were memoizing the setSlide prop in the child instead of the parent where it's being passed down. This caused the setSlide prop to be a new reference each render and re-memoized via useCallback in the child. React useState updater functions are stable however, so you can directly pass them to children.
const Slider = ({ images }) => {
const [currentSlide, setCurrentSlide] = React.useState(0);
const moveToSlide = (id) => {
setCurrentSlide(id);
};
return (
<StyledSlider id="slider">
<SliderWrapper
items={images}
setSlide={setCurrentSlide} // <-- pass directly to child
currentSlide={currentSlide}
/>
<SliderNav
currentSlide={currentSlide}
moveToSlide={moveToSlide}
maxItems={images.length}
/>
</StyledSlider>
);
};
If you wanted to remain with the setSlide handler in the parent, here is where you'd memoize the callback so the parent is providing a stable reference. Note that this is only useful if memoizing non-useState functions.
const setSlide = React.useCallback(
(id) => {
setCurrentSlide(id);
},
[setCurrentSlide]
);

Updating React state in nested setTimeout callbacks

Can someone please tell me what's wrong with this and why the state of the 'video variable' remains false? So, even after the h2 element has rendered and is visible (i.e. the state of the video variable has been updated to true), when I click and call the hideVideo function, the video state remains false? Many thanks.
export default function App() {
const [message, showMessage] = useState(false);
const [video, setVideo] = useState(false);
let modalTimeout, videoTimeout;
useEffect(() => {
window.addEventListener("click", hideVideo);
setupTimeouts();
return () => {
clearTimeout(modalTimeout);
clearTimeout(videoTimeout);
};
}, []);
const setupTimeouts = () => {
modalTimeout = setTimeout(() => {
showMessage(true);
videoTimeout = setTimeout(() => {
showMessage(false);
setVideo(true);
}, 4000);
}, 2000);
};
const hideVideo = () => {
console.log(video);
showMessage(false);
if (video === true) {
setVideo(false);
}
};
return (
<div className="App">
{message && <h1>Message</h1>}
{video && <h2>Video</h2>}
</div>
);
}
When you call useEffect the window listener attach the default video value that is false to the function hideVideo() so it will be always false, I created a button to show you that the video state value does change. check the last test function
export default function App() {
const [message, showMessage] = useState(false);
const [video, setVideo] = useState(false);
let modalTimeout, videoTimeout;
useEffect(() => {
window.addEventListener("click", hideVideo);
setupTimeouts();
return () => {
clearTimeout(modalTimeout);
clearTimeout(videoTimeout);
};
}, []);
const setupTimeouts = () => {
modalTimeout = setTimeout(() => {
showMessage(true);
videoTimeout = setTimeout(() => {
showMessage(false);
setVideo(true);
}, 4000);
}, 2000);
};
const hideVideo = () => {
console.log(video);
showMessage(false);
if (video) {
setVideo(false);
}
};
const test = (event) => {
event.stopPropagation();
console.log(video)
}
return (
<>
{message && <h1>Message</h1>}
{video && <h2>Video</h2>}
<button onClick={test}>test</button>
</>
);
}

Props in react seems to not be usable right away

I have a small issue with a really simple component that doesn't display what I want.
const UserCards = (props) => {
const [retrievedData, setRetrievedData] = useState();
useEffect(() => {
const data = [];
props.users.map((user) => {
data.push(<UserCard key={user.username} user={user} />);
});
setRetrievedData(data);
}, []);
return (
<div className={styles.userCards}>{retrievedData && retrievedData}</div>
);
};
When I refresh the page it will not display my User cards. But If I had a timeout on useEffect like this :
const UserCards = (props) => {
const [retrievedData, setRetrievedData] = useState();
useEffect(() => {
const data = [];
setTimeout(function () {
props.users.map((user) => {
data.push(<UserCard key={user.username} user={user} />);
});
setRetrievedData(data);
}, 3000);
}, []);
return (
<div className={styles.userCards}>{retrievedData && retrievedData}</div>
);
};
Everything's fine!
I thought props were usable immediately but it seems I was wrong.
I tried to add [props] at the end of useEffect to be sure my state will be updated if props changed, but nothing...
I'm sure it's nothing but I've been struggling since yesterday!
Thank you!
Just add useEffect dependency, which will call your useEffect content every time, when dependency changed:
const UserCards = (props) => {
const [retrievedData, setRetrievedData] = useState();
useEffect(() => {
const data = [];
props.users.map((user) => {
data.push(<UserCard key={user.username} user={user} />);
});
setRetrievedData(data);
}, [props]);
return (
<div className={styles.userCards}>{retrievedData && retrievedData}</div>
);
};

Resources