eslint warning for missing dependency in useEffect - reactjs

I am making a searchable Dropdown and getting following eslint warning:
React Hook useEffect has missing dependencies: 'filterDropDown' and 'restoreDropDown'. Either include them or remove the dependency array.
import React, { useState, useEffect, useCallback } from "react";
const SearchableDropDown2 = () => {
const [searchText, setSearchText] = useState("");
const [dropdownOptions, setDropdownOptions] = useState([
"React",
"Angular",
"Vue",
"jQuery",
"Nextjs",
]);
const [copyOfdropdownOptions, setCopyOfDropdownOptions] = useState([
...dropdownOptions,
]);
const [isExpand, setIsExpand] = useState(false);
useEffect(() => {
searchText.length > 0 ? filterDropDown() : restoreDropDown();
}, [searchText]);
const onClickHandler = (e) => {
setSearchText(e.target.dataset.myoptions);
setIsExpand(false);
};
const onSearchDropDown = () => setIsExpand(true);
const closeDropDownHandler = () => setIsExpand(false);
const filterDropDown = useCallback(() => {
const filteredDropdown = dropdownOptions.filter((_) =>
_.toLowerCase().includes(searchText.toLowerCase())
);
setDropdownOptions([...filteredDropdown]);
}, [dropdownOptions]);
const restoreDropDown = () => {
if (dropdownOptions.length !== copyOfdropdownOptions.length) {
setDropdownOptions([...copyOfdropdownOptions]);
}
};
const onSearchHandler = (e) => setSearchText(e.target.value.trim());
return (
<div style={styles.mainContainer}>
<input
type="search"
value={searchText}
onClick={onSearchDropDown}
onChange={onSearchHandler}
style={styles.search}
placeholder="search"
/>
<button disabled={!isExpand} onClick={closeDropDownHandler}>
-
</button>
<div
style={
isExpand
? styles.dropdownContainer
: {
...styles.dropdownContainer,
height: "0vh",
}
}
>
{dropdownOptions.map((_, idx) => (
<span
onClick={onClickHandler}
style={styles.dropdownOptions}
data-myoptions={_}
value={_}
>
{_}
</span>
))}
</div>
</div>
);
};
const styles = {
mainContainer: {
padding: "1vh 1vw",
width: "28vw",
margin: "auto auto",
},
dropdownContainer: {
width: "25vw",
background: "grey",
height: "10vh",
overflow: "scroll",
},
dropdownOptions: {
display: "block",
height: "2vh",
color: "white",
padding: "0.2vh 0.5vw",
cursor: "pointer",
},
search: {
width: "25vw",
},
};
I tried wrapping the filterDropDown in useCallback but then the searchable dropdown stopped working.
Following are the changes incorporating useCallback:
const filterDropDown = useCallback(() => {
const filteredDropdown = dropdownOptions.filter((_) =>
_.toLowerCase().includes(searchText.toLowerCase())
);
setDropdownOptions([...filteredDropdown]);
}, [dropdownOptions, searchText]);

My suggestion would be to not use useEffect at all in the context of what you are achieving.
import React, {useState} from 'react';
const SearchableDropDown = () => {
const [searchText, setSearchText] = useState('');
const dropdownOptions = ['React','Angular','Vue','jQuery','Nextjs'];
const [isExpand, setIsExpand] = useState(false);
const onClickHandler = e => {
setSearchText(e.target.dataset.myoptions);
setIsExpand(false);
}
const onSearchDropDown = () => setIsExpand(true);
const closeDropDownHandler = () => setIsExpand(false);
const onSearchHandler = e => setSearchText(e.target.value.trim());
return (
<div style={styles.mainContainer}>
<input
type="search"
value={searchText}
onClick={onSearchDropDown}
onChange={onSearchHandler}
style={styles.search}
placeholder="search"
/>
<button disabled={!isExpand} onClick={closeDropDownHandler}>
-
</button>
<div
style={
isExpand
? styles.dropdownContainer
: {
...styles.dropdownContainer,
height: '0vh',
}
}
>
{dropdownOptions
.filter(opt => opt.toLowerCase().includes(searchText.toLowerCase()))
.map((option, idx) =>
<span key={idx} onClick={onClickHandler} style={styles.dropdownOptions} data-myoptions={option} value={option}>
{option}
</span>
)}
</div>
</div>
);
};
const styles = {
mainContainer: {
padding: '1vh 1vw',
width: '28vw',
margin: 'auto auto'
},
dropdownContainer: {
width: '25vw',
background: 'grey',
height: '10vh',
overflow: 'scroll'
},
dropdownOptions: {
display: 'block',
height: '2vh',
color: 'white',
padding: '0.2vh 0.5vw',
cursor: 'pointer',
},
search: {
width: '25vw',
},
};
Just filter the dropDownOptions before mapping them to the span elements.
also, if the list of dropDownOptions is ever going to change then use setState else just leave it as a simple list. If dropDownOptions comes from an api call, then use setState within the useEffect hook.

Your current code:
// 1. Filter
const filterDropDown = useCallback(() => {
const filteredDropdown = dropdownOptions.filter((_) =>
_.toLowerCase().includes(searchText.toLowerCase())
);
setDropdownOptions([...filteredDropdown]);
}, [dropdownOptions]);
// 2. Restore
const restoreDropDown = () => {
if (dropdownOptions.length !== copyOfdropdownOptions.length) {
setDropdownOptions([...copyOfdropdownOptions]);
}
};
// 3. searchText change effect
useEffect(() => {
searchText.length > 0 ? filterDropDown() : restoreDropDown();
}, [searchText]);
Above code produces eslint warning that:
React Hook useEffect has missing dependencies: 'filterDropDown' and 'restoreDropDown'. Either include them or remove the dependency array
BUT after doing what the warning says, our code finally looks like:
// 1. Filter
const filterDropDown = useCallback(() => {
const filteredDropdown = dropdownOptions.filter((_) =>
_.toLowerCase().includes(searchText.toLowerCase())
);
setDropdownOptions(filteredDropdown);
}, [dropdownOptions, searchText]);
// 2. Restore
const restoreDropDown = useCallback(() => {
if (dropdownOptions.length !== copyOfdropdownOptions.length) {
setDropdownOptions([...copyOfdropdownOptions]);
}
}, [copyOfdropdownOptions, dropdownOptions.length]);
// 3. searchText change effect
useEffect(() => {
searchText.length > 0 ? filterDropDown() : restoreDropDown();
}, [filterDropDown, restoreDropDown, searchText]);
But the above code has formed an infinite loop because:
"Filter" function is OK. (It will be recreated when searchText or dropdownOptions change. And that makes sense.)
"Restore" function is OK. (It will be recreated when copyOfdropdownOptions or dropdownOptions.length change. And this too makes sense.)
searchText change effect looks bad. This effect will run whenever:
=> (a). searchText is changed (OK), or
=> (b). "Filter" function is changed (Not OK because this function itself will change when this hook is run. Point 1), or
=> (c). "Restore" function is changed (Not OK because this function itself will change when this hook is run. Point 2)
We can clearly see an infinite loop in Point 3 (a, b, c).
How to fix it?
There can be few ways. One could be:
The below copy is constant, so either keep it in a Ref or move it outside the component definition:
const copyOfdropdownOptions = useRef([...dropdownOptions]);
And, move the "Filter" and "Restore" functions inside the hook (i.e. no need to define it outside and put it as a dependency), like so:
useEffect(() => {
if (searchText.length) {
// 1. Filter
setDropdownOptions((prev) =>
prev.filter((_) => _.toLowerCase().includes(searchText.toLowerCase()))
);
} else {
// 2. Restore
setDropdownOptions([...copyOfdropdownOptions.current]);
}
}, [searchText]);
As you can see the above effect will run only when searchText is changed.

Related

useRef underline current is changed but it will not paint to dom

Why it is not re-rendering the component even though current is changed. I know it can be done with useState.
const onDecrement = () => {
counter.current -= 1;
};
const onIncrement = () => {
counter.current += 1;
};
const onReset = () => {
counter.current = 0;
};
useEffect(() => {
// It should trigger this
console.log('re-render');
}, [counter.current]);
Playground: https://stackblitz.com/edit/react-ts-xtamop?file=useFetch.tsx,App.tsx
useRef are used to avoid re-render as opposed to useState which causes re-render when change happens.
As for your context, you can assign the ref to the div element and use the inntertext to assign values
const counter = useRef<HTMLDivElement>(null);
const onDecrement = () => {
counter.current.innerText = `${Number(counter.current.innerText) - 1}`;
};
const onIncrement = () => {
counter.current.innerText = `${Number(counter.current.innerText) + 1}`;
};
const onReset = () => {
counter.current.innerText = '0';
};
// only executes in the initial render
useEffect(() => {
console.log('re-render');
counter.current.innerText = '0';
}, [counter.current]);
return (
<div>
<button onClick={onDecrement}>-</button>
<div
ref={counter}
style={{ display: 'inline-block', margin: '0 10px' }}
></div>
<button onClick={onIncrement}>+</button>
<div style={{ marginTop: '10px' }}>
<button onClick={onReset}>Reset</button>
</div>
</div>
Demo

In React I get the warning: Function component cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Here's are a few snippets from my code. I have a function component defined as:
function AssociationViewer(props) {
const core = React.useRef<GraphCore | null>(null);
const dataService = React.useRef<DataService>(new DataService(props.graphApi));
const selectionBoxDimensions = React.useRef(null);
const initialGraphRender = React.useRef<boolean>(false);
const filterRef = React.useRef(null);
const getElementRef = React.useCallback((el) => {
if (el && !core.current) {
core.current = new GraphCore({ container: el });
// TODO: Change data service to accept core as an argument and initialize it here?
dataService.current.addGraphDataFn = (opts) => getRksGraph().addData(opts);
dataService.current.setGraphDataFn = (opts) => getRksGraph().setData(opts);
onMount();
return onUnmount;
}
}, []);
.
.
.
return (
<>
{props.enableSearch && <div style={{zIndex: 10000, position: 'absolute', marginTop: 10, right: 15}}>
<button onClick={flashSearchedNodes}>Search</button>
<input
value={searchText}
placeholder='Find node by text'
onKeyDown={(e) => e.key == 'Enter' && flashSearchedNodes()}
onChange={(e) => setSearchText(e.target.value)}
/>
<input readOnly style={{width: '60px', textAlign: 'center'}} type="text" value={searchedElesDisplayText} />
<button onClick={prevFoundNode}>Prev</button>
<button onClick={nextFoundNode}>Next</button>
<button onClick={cancelFlashNodes}>Clear</button>
</div>}
<div
style={{ height: '100%', width: '100%' }}
id={props.componentId || 'kms-graph-core-component'}
ref={getElementRef}
></div>
{core.current && (
<GraphTooltip
tooltip={props.tooltipCallback}
tooltipHoverHideDelay={props.tooltipHoverHideDelay}
tooltipHoverShowDelay={props.tooltipHoverShowDelay}
tippyOptions={props.tippyOptions}
core={core.current}
/>
)}
{props.loadingMask && !hasCustomLoadMask() && (
<DefaultLoadMask
active={showLoading}
loadingClass={getLoadingClass()}
onClick={() => {
setShowLoading(false);
}}
/>
)}
{props.loadingMask && showLoading && hasCustomLoadMask() && props.customLoadingMask()}
</>
);
}
export default AssociationViewer;
I have an angular app that uses a service to call this component as follows:
ReactService.render(AssociationViewer,
{
ref: function (el) {
reactElement = el;
},
component: 'association-viewer-1',
getImageUrl: getImageUrl,
graphApi: graphApi,
pairElements: $ctrl.pairElements,
theme: theme,
view: 'testView'
}
'miniGraphContainer',
() => {
if ($ctrl.pairElements.edges) {
reactElement.core.select($ctls.pairElements.edges($ctrl.currEdge).id);
}
}
Here's GraphTooltip:
import React from 'react';
import GraphCore from '../GraphCore';
function useForceUpdate() {
const [value, setValue] = React.useState(0);
return () => setValue((value) => ++value);
}
interface IGraphTooltipProps{
tooltipHoverShowDelay: number;
tooltipHoverHideDelay: number;
core: GraphCore;
tooltip: Function;
tippyOptions: any;
}
const GraphTooltip = (props: IGraphTooltipProps) => {
const tooltipRef = React.useRef<React.Element>(null);
const forceUpdate = useForceUpdate();
const setRef = (ref) => {
tooltipRef.current = ref;
};
const [currentElement, setCurrentElement] = React.useState<React.Element>();
React.useEffect(() => {
props.core.getAllSubGraphs().forEach((subgraph) => {
subgraph.setOptions({
tooltip: {
domElementCallback: (e) => {
// this isn't changing so it's not picking up a render loop
setCurrentElement(e);
forceUpdate();
return tooltipRef.current;
},
hoverShowDelay: props.tooltipHoverShowDelay,
hoverHideDelay: props.tooltipHoverHideDelay,
options: props.tippyOptions
}
});
});
}, []);
return <>{props.tooltip(setRef, currentElement, props.core)}</>;
};
export default GraphTooltip;
When triggering the event that causes this ReactService to render the AssociationViewer component, I get the warning: Function component cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? Also, reactElement is undefined since the ref cannot be accessed. How can I use React.forwardRef() in the AssociationViewer component to forward the ref to the calling component?
So, here you just have to wrap your functional component inside React.forwardRef()
<GraphTooltip
tooltip={props.tooltipCallback}
tooltipHoverHideDelay={props.tooltipHoverHideDelay}
tooltipHoverShowDelay={props.tooltipHoverShowDelay}
tippyOptions={props.tippyOptions}
core={core.current} // Due to this you are getting warning
/>
Go to your functional component "GraphTooltip" and wrap your export statement in forwardRef().
export const React.forwardRef(GraphTooltip)
Hope it helps!

React MUI ProgressBar not rendering on value change

I have a functional component where I am using the MUI progress bar that I want to display but when the progress bar loads its still at the progress I set at the first step.
Also I am calling an API and processing the results in one of the functions. What am I doing wrong ?
function LinearProgressWithLabel(props: LinearProgressProps & { value: number }) {
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" {...props} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(
props.value,
)}%`}</Typography>
</Box>
</Box>
);
}
export const Search = (props) => {
const { onSearchComplete } = props;
const [msgBox, setMsgBox] = useState(null);
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(10);
const onSearch = async () => {
setLoading(true);
const emails = contacts
.filter(x => x.isChecked)
.map(item => item.emailAddress);
setProgress(30); //this is where I am manually setting the progress.
try {
const searchResults = await AppApi.search(emails);
let userList = [];
setProgress(70); // I want to manually set the percentage here
for (let i = 0; i < searchResults.length; i++) {
//processing the list here
}
onSearchComplete(userList); //passing on the results to another component
} catch (err) {
console.log({ err });
setMsgBox({ message: `${err.message}`, type: 'error' });
}
setLoading(false);
}
useEffect(() => {
onSearch();
}, [progress]);
return (
<Box>
{loading ? <LinearProgressWithLabel value={progress} />:
<Box>{msgBox && (<a style={{ cursor: 'pointer' }} onClick={() => setMsgBox(null)} title="Click to dismiss"><MessageBox type={msgBox.type || 'info'}>{msgBox.message}</MessageBox></a>)}</Box>}
</Box>
);
}
At the moment, your useEffect hook has the wrong dependencies. onSearch looks like it has two dependencies that could change - contacts and onSearchComplete, so the effect hook should actually be written as:
useEffect(() => {
onSearch();
}, [contacts, onSearchComplete]);
Depending on how onSearchComplete is defined, you might find that your effect re-runs more frequently than it should; you can either solve this by making onSearchComplete a callback:
const OtherComponent = () => {
const onSearchComplete = useCallback(userList => {
// ----- 8< -----
}, [...]);
}
Or wrapping the callback in a ref -
const Search = ({ onSearchComplete }) => {
const onSearchCompleteRef = useRef();
onSearchCompleteRef.current = onSearchComplete;
const onSearch = async () => {
// ----- 8< -----
onSearchCompleteRef.current(userList);
}
// Now you don't need onSearchComplete as a dependency
useEffect(() => {
onSearch();
}, [contacts]);
};
Edit
The reason you're not seeing the "updated" progress is because the processing of the results happens on the same render cycle as you updating the progress bar. The only way to get around that would be to introduce an artificial delay when you're processing the results:
setTimeout(() => {
onSearchCompleteRef.current();
setLoading(false);
}, 100);
I created a CodeSandbox demo to show this.

How to make data persist on refresh React JS?

I have a code where I mount a table with some firebase data but for some reason the values disappear and I been struggling for the next 2 weeks trying to solve this issue I haven't found a solution to this and I have asked twice already and I have try everything so far but it keeps disappearing.
Important Update
I just want to clarify the following apparently I was wrong the issue wasn't because it was a nested collection as someone mentioned in another question. The issue is because my "user" is getting lost in the process when I refresh.
I bring the user from the login to the app like this:
<Estudiantes user={user} />
and then I receive it as a props
function ListadoPedidos({user})
but is getting lost and because is getting lost when I try to use my firebase as:
estudiantesRef = db.collection("usuarios").doc(user.uid).collection("estudiantes")
since the user is "lost" then the uid will be null. Since is null it will never reach the collection and the docs.
I have a simple solution for you. Simply raise the parsing of localStorage up one level, passing the preloadedState into your component as a prop, and then using that to initialize your state variable.
const ListadoEstudiantes = (props) => {
const estData = JSON.parse(window.localStorage.getItem('estudiantes'));
return <Listado preloadedState={estData} {...props} />;
};
Then initialize state with the prop
const initialState = props.preloadedState || [];
const [estudiantesData, setEstudiantesData] = useState(initialState);
And finally, update the useEffect hook to persist state any time it changes.
useEffect(() => {
window.localStorage.setItem('estudiantes', JSON.stringify(estudiantes));
}, [estudiantes]);
Full Code
import React, { useState, useEffect } from 'react';
import { db } from './firebase';
import { useHistory } from 'react-router-dom';
import './ListadoEstudiantes.css';
import {
DataGrid,
GridToolbarContainer,
GridToolbarFilterButton,
GridToolbarDensitySelector,
} from '#mui/x-data-grid';
import { Button, Container } from '#material-ui/core';
import { IconButton } from '#mui/material';
import PersonAddIcon from '#mui/icons-material/PersonAddSharp';
import ShoppingCartSharpIcon from '#mui/icons-material/ShoppingCartSharp';
import DeleteOutlinedIcon from '#mui/icons-material/DeleteOutlined';
import { Box } from '#mui/system';
const ListadoEstudiantes = (props) => {
const estData = JSON.parse(window.localStorage.getItem('estudiantes'));
return <Listado preloadedState={estData} {...props} />;
};
const Listado = ({ user, preloadedState }) => {
const history = useHistory('');
const crearEstudiante = () => {
history.push('/Crear_Estudiante');
};
const initialState = preloadedState || [];
const [estudiantesData, setEstudiantesData] = useState(initialState);
const parseData = {
pathname: '/Crear_Pedidos',
data: estudiantesData,
};
const realizarPedidos = () => {
if (estudiantesData == 0) {
window.alert('Seleccione al menos un estudiante');
} else {
history.push(parseData);
}
};
function CustomToolbar() {
return (
<GridToolbarContainer>
<GridToolbarFilterButton />
<GridToolbarDensitySelector />
</GridToolbarContainer>
);
}
const [estudiantes, setEstudiantes] = useState([]);
const [selectionModel, setSelectionModel] = useState([]);
const columns = [
{ field: 'id', headerName: 'ID', width: 100 },
{ field: 'nombre', headerName: 'Nombre', width: 200 },
{ field: 'colegio', headerName: 'Colegio', width: 250 },
{ field: 'grado', headerName: 'Grado', width: 150 },
{
field: 'delete',
width: 75,
sortable: false,
disableColumnMenu: true,
renderHeader: () => {
return (
<IconButton
onClick={() => {
const selectedIDs = new Set(selectionModel);
estudiantes
.filter((x) => selectedIDs.has(x.id))
.map((x) => {
db.collection('usuarios')
.doc(user.uid)
.collection('estudiantes')
.doc(x.uid)
.delete();
});
}}
>
<DeleteOutlinedIcon />
</IconButton>
);
},
},
];
const deleteProduct = (estudiante) => {
if (window.confirm('Quiere borrar este estudiante ?')) {
db.collection('usuarios').doc(user.uid).collection('estudiantes').doc(estudiante).delete();
}
};
useEffect(() => {}, [estudiantesData]);
const estudiantesRef = db.collection('usuarios').doc(user.uid).collection('estudiantes');
useEffect(() => {
estudiantesRef.onSnapshot((snapshot) => {
const tempData = [];
snapshot.forEach((doc) => {
const data = doc.data();
tempData.push(data);
});
setEstudiantes(tempData);
console.log(estudiantes);
});
}, []);
useEffect(() => {
window.localStorage.setItem('estudiantes', JSON.stringify(estudiantes));
}, [estudiantes]);
return (
<Container fixed>
<Box mb={5} pt={2} sx={{ textAlign: 'center' }}>
<Button
startIcon={<PersonAddIcon />}
variant="contained"
color="primary"
size="medium"
onClick={crearEstudiante}
>
Crear Estudiantes
</Button>
<Box pl={25} pt={2} mb={2} sx={{ height: '390px', width: '850px', textAlign: 'center' }}>
<DataGrid
rows={estudiantes}
columns={columns}
pageSize={5}
rowsPerPageOptions={[5]}
components={{
Toolbar: CustomToolbar,
}}
checkboxSelection
//Store Data from the row in another variable
onSelectionModelChange={(id) => {
setSelectionModel(id);
const selectedIDs = new Set(id);
const selectedRowData = estudiantes.filter((row) => selectedIDs.has(row.id));
setEstudiantesData(selectedRowData);
}}
{...estudiantes}
/>
</Box>
<Button
startIcon={<ShoppingCartSharpIcon />}
variant="contained"
color="primary"
size="medium"
onClick={realizarPedidos}
>
Crear pedido
</Button>
</Box>
</Container>
);
};
I suspect that it's because this useEffect does not have a dependency array and is bring run on every render.
useEffect (() => {
window.localStorage.setItem("estudiantes", JSON.stringify(estudiantes))
})
Try adding a dependency array as follows:
useEffect (() => {
if (estudiantes && estudiantes.length>0)
window.localStorage.setItem("estudiantes", JSON.stringify(estudiantes))
},[estudiantes])
This will still set the localStorage to [] when it runs on the first render. But when the data is fetched and estudiantes is set, the localStorage value will be updated. So I've added a check to check if it's not the empty array.
Change the dependency array of this useEffect to []:
estudiantesRef.onSnapshot(snapshot => {
const tempData = [];
snapshot.forEach((doc) => {
const data = doc.data();
tempData.push(data);
});
setEstudiantes(tempData);
console.log(estudiantes)
})
}, []);
The data flow in your code is somewhat contradictory, so I modify your code, and it works fine.
You can also try delete or add button, it will modify firebase collection, then update local data.
You can click refresh button in codesandbox previewer (not browser) to observe the status of data update.
Here is the code fargment :
// Set value of `localStorage` to component state if it exist.
useEffect(() => {
const localStorageEstData = window.localStorage.getItem("estudiantes");
localStorageEstData && setEstudiantes(JSON.parse(localStorageEstData));
}, []);
// Sync remote data from firebase to local component data state.
useEffect(() => {
// Subscribe onSnapshot
const unSubscribe = onSnapshot(
collection(db, "usuarios", user.id, "estudiantes"),
(snapshot) => {
const remoteDataSource = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data()
}));
console.info(remoteDataSource);
setEstudiantes(remoteDataSource);
}
);
return () => {
//unSubscribe when component unmount.
unSubscribe();
};
}, [user.id]);
// when `estudiantes` state update, `localStorage` will update too.
useEffect(() => {
window.localStorage.setItem("estudiantes", JSON.stringify(estudiantes));
}, [estudiantes]);
Here is the full code sample :
Hope to help you :)

I am trying to figure out how to create a clean up function as I keep getting an error

I am trying to figure out how to create a clean up function as I keep getting an error, if I remove "comments" from the useEffect dependencies, the error goes away, but then the app doesn't update in realtime, which is a problem. If anyone has worked with React and the realtime database or even Firestore and have any ideas on what I should do please let me know.
import React, { useContext, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'react-toastify';
import User from '../assets/images/user.svg';
import { AuthContext } from '../helpers/firebaseAuth';
import firebase from '../helpers/Firebase';
import Loading from '../helpers/Loading';
export const Comments = ({ match, history }) => {
const { register, handleSubmit, reset } = useForm();
const slug = match.params.slug;
const {...currentUser} = useContext(AuthContext);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = () => {
const data = firebase.database().ref(`/posts/${slug}/comments`)
data.once('value')
.then((snapshot) => {
if (snapshot) {
let comments = [];
const snapshotVal = snapshot.val();
for (let comment in snapshotVal) {
comments.push(snapshotVal[comment]);
}
setComments(comments);
setLoading(false);
}
});
}
fetchData();
}, [slug, comments])
if (loading) {
return <Loading />;
};
const postComment = (values) => {
console.log(!!currentUser.currentUser)
if (!!currentUser.currentUser) {
const comment = {
commentText: values.commentText,
commentCreator: currentUser.currentUser.displayName,
currentUserId: currentUser.currentUser.uid,
}
const postRef = firebase.database().ref(`posts/${slug}/comments`);
postRef.push(comment);
reset();
} else {
toast.error('You are not authenticated 😕');
}
};
const deleteComment = () => {
console.log(comments[0].commentUserId);
console.log(currentUser.currentUser.uid);
if (currentUser.currentUser.uid === comments[0].commentUserId) {
console.log('correct');
}
const key = firebase.database().ref(`posts/${slug}/comments`).once('value');
key.then(snapshot => {
console.log(snapshot.val());
}).catch((error) => {
console.log(error);
});
};
const back = () => {
history.push('./');
};
return (
<div className='main' style={{ maxWidth: '600px' }}>
<div className='see-user-comments' onClick={back} style={{ cursor: 'pointer', height: '50px' }}>
Commenting on the post: {slug}
</div>
<div className='see-user-comments' style={{ padding: '10px 0' }}>
<div>
<img src={User} alt='Profile' style={{ width: '30px' }} />
<span className='usertag-span'>{currentUser.displayName}</span>
</div>
<div>
<form onSubmit={handleSubmit(postComment)}>
<textarea
name='commentText'
rows='3'
style={{ margin: '10px 0' }}
placeholder='Add to the conversation!'
ref={register}
/>
<span style={{ width: '90%' }}>
<button>Comment</button>
</span>
</form>
</div>
</div>
{comments.map((comment, index) =>
<div key={index} className='see-user-comments' style={{ padding: '15px 0' }}>
<div style={{ height: '30px' }}>
<img src={User} alt='Profile' style={{ width: '30px' }} />
<div style={{ flexDirection: 'column', alignItems: 'flex-start', justifyItems: 'center' }}>
<span className='usertag-span'>{comment.commentCreator}</span>
</div>
</div>
<span className='commentText-span'>{comment.commentText}
{ !!currentUser?.currentUser?.uid === comments[0].commentUserId ?
(<button onClick={deleteComment}>Delete</button>) : null
}
</span>
</div>
)}
</div>
)
}
export default Comments;
Without seeing the error in question, I can only assume it's because using the following pattern causes an infinite loop because the effect is re-triggered every time count changes:
const [count, setCount] = useState(0);
useEffect(() => setCount(count + 1), [count]);
When you add comments to your effect, you are doing the same thing.
To solve this, you must change your effect to rely on Firebase's realtime events to update your comments array instead. This can be as simple as changing once('value').then((snap) => {...}) to on('value', (snap) => {...});. Because this is now a realtime listener, you must also return a function that unsubscribes the listener from inside your useEffect call. The least amount of code to do this correctly is:
const [postId, setPostId] = useState('post001');
useEffect(() => {
const postRef = firebase.database().ref('posts').child(postId);
const listener = postRef.on(
'value',
postSnapshot => {
const postData = postSnapshot.val();
// ... update UI ...
},
err => {
console.log('Failed to get post', err);
// ... update UI ...
}
)
return () => postRef.off('value', listener);
}, [postId]);
Applying these changes to your code (as well as some QoL improvements) yields:
const { register, handleSubmit, reset } = useForm();
const slug = match.params.slug;
const { ...authContext } = useContext(AuthContext); // renamed: currentUser -> authContext (misleading & ambiguous)
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
let _postCommentHandler, _deleteCommentHandler;
useEffect(() => {
// don't call this data - it's not the data but a reference to it - always call it `somethingRef` instead
const postCommentsRef = firebase.database().ref(`/posts/${slug}/comments`);
// create realtime listener
const listener = postCommentsRef.on(
'value',
querySnapshot => {
let _comments = [];
querySnapshot.forEach(commentSnapshot => {
const thisComment = commentSnapshot.val();
thisComment.key = commentSnapshot.key; // store the key for delete/edit operations
_comments.push(thisComment);
});
setComments(_comments);
setLoading(false);
},
err => {
console.log(`Error whilst getting comments for post #${slug}`, err);
// TODO: handle error
});
// update new comment handler
_postCommentHandler = (formData) => {
console.log({
isLoggedIn: !!authContext.currentUser
});
if (!authContext.currentUser) {
toast.error('You are not authenticated 😕');
return;
}
const newComment = {
commentText: formData.commentText, // suggested: commentText -> content
commentCreator: authContext.currentUser.displayName, // suggested: commentCreator -> author
currentUserId: authContext.currentUser.uid, // suggested: commentUserId -> authorId
}
postCommentsRef.push(newComment)
.then(() => {
// commented successfully
reset(); // reset form completely
})
.catch(err => {
console.log(`Error whilst posting new comment`, err);
// TODO: handle error
reset({ commentText: formData.commentText }) // reset form, but leave comment as-is
})
}
// update delete handler
_deleteCommentHandler = () => {
if (!comments || !comments[0]) {
console.log('Nothing to delete');
return;
}
const commentToDelete = comments[0];
console.log({
commentUserId: commentToDelete.commentUserId,
currentUser: authContext.currentUser.uid
});
if (authContext.currentUser.uid !== commentToDelete.commentUserId) {
toast.error('That\'s not your comment to delete!');
return;
}
postCommentsRef.child(commentToDelete.key)
.remove()
.then(() => {
// deleted successfully
})
.catch(err => {
console.log(`Error whilst deleting comment #${commentToDelete.key}`, err);
// TODO: handle error
});
};
// return listener cleanup function
return () => postCommentsRef.off('value', listener);
}, [slug]);
const postComment = (values) => _postCommentHandler(values);
const deleteComment = () => _deleteCommentHandler();
Because I renamed currentUser to authContext, this will also need updating:
<div>
<img src={User} alt='Profile' style={{ width: '30px' }} />
<span className='usertag-span'>{authContext?.currentUser?.displayName}</span>
</div>

Resources