React MUI ProgressBar not rendering on value change - reactjs

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.

Related

Why isn't functional child component changing state on props change?

I have a form template with a popup as child component. The popup works to display response from server for a form query(ajax) to login/forgot-password, etc. and will disappear automatically after some time.
Now, for the first time popup works fine and displays response and disappears but if I try it again from same page (as query is sent as ajax and page reloading doesn't happen) the message gets updated but it won't appear.
And what I have concluded to be the problem is that state of child component(isShown) not updating on message update and I can't seem to solve this. Or it can be I am overlooking some other thing .
AppForm.jsx (parent template)
function MainContent(props) {
const { title, underTitle, children, buttonText, customHandler, bottomPart } = props;
const [banner, setBanner] = useState({code: null, msg: null})
const reqHandler = async () => {
customHandler().then((resp) => {
setBanner({code: 'success', msg: resp.data.msg})
}).catch((err) => {
setBanner({ code: 'error', msg: err.response.data.msg })
});
}
return (
<ThemeProvider theme={formTheme}>
<Container maxWidth="sm">
<AppFormPopup statusCode={banner.code} msg={banner.msg} />
<Box sx={{ mt: 7, mb: 12 }}>
<Paper>
{children} //displays the fields required by derived form page
<Button type='submit' sx={{ mt: 3, mb: 2 }} onClick={reqHandler}>
{buttonText}
</Button>
</Paper>
</Box>
</Container>
</ThemeProvider>
)
}
function AppForm(props) {
return (
<>
<AppFormNav />
<MainContent {...props} />
</>
);
}
AppForm.propTypes = {
children: PropTypes.node,
};
export default AppForm;
AppFormPopup.jsx (child Component which shows popup msg)
function MainContent(props){
const { msg } = props;
const [progress, setProgress] = useState(0);
const [isShown, setIsShown] = useState(true); //state used for controlling visibility of popup msg.
useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if(oldProgress === 100){
setIsShown(false);
// return 0;
}
return Math.min(oldProgress + 10, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
});
return (
<Grid container sx={{
display: isShown ? 'block' : 'none',
}}>
<Grid item sx={{ color: "white", pl: 1 }}> {msg} </Grid>
</Grid>
)
}
export default function AppFormPopup({msg}) {
if(msg === null) return;
return (
<MainContent msg={msg} />
)
}
ForgotPass.jsx (form page which derives form template AppForm.jsx)
export default function ForgotPass() {
const loc = useLocation().pathname;
const mailRef = useRef(null);
const reqHandler = async () => {
const email = mailRef.current.value;
if(email){
const resp = await axios.post(loc, {email})
return resp;
}
}
return (
<AppForm
buttonText="Send Reset Link"
customHandler={reqHandler}
>
<Box sx={{ mt: 6 }}>
<TextField
autoFocus
label="Email"
inputRef={mailRef}
/>
</Box>
</AppForm>
)
}
This is due to the fact that your useEffect will only render once. My suggestion would be to change your useEffect to render if a value change, for example: Let say you got a state name of UserName. Now each time that UserName is being change will rerender your UseEffect function. By adding a [UserName] at end of useEffect it will cause a rerender each time UserName change.
useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if(oldProgress === 100){
setIsShown(false);
// return 0;
}
return Math.min(oldProgress + 10, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
},[UserName]);
It feels weird answering your question but here we go.
So, thanks #GlenBowler his answer was somewhat right and nudged me towards the right answer.
Instead of modifying of that useEffect, we needed another useEffect to check for changes in msg. So, code should look like this:
AppFormPopup.jsx
function MainContent(props){
const { msg } = props;
const [progress, setProgress] = useState(0);
const [isShown, setIsShown] = useState(true); //state used for controlling visibility of popup msg.
useEffect(() => {
setIsShown(true);
setProgress(0);
}, [msg]);
useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if(oldProgress === 100){
setIsShown(false);
// return 0;
}
return Math.min(oldProgress + 10, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
});
return (
<Grid container sx={{
display: isShown ? 'block' : 'none',
}}>
<Grid item sx={{ color: "white", pl: 1 }}> {msg} </Grid>
</Grid>
)
}
export default function AppFormPopup({msg}) {
if(msg === null) return;
return (
<MainContent msg={msg} />
)
}

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 :)

eslint warning for missing dependency in useEffect

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.

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>

React hook logging a useState element as null when it is not

I have a method,
const handleUpvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log('mappedPosts', mappedPosts); // null
console.log('newPosts', newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
That is attached to a mapped element,
const mapped = userPosts.map((post, index) => (
<ListItem
rightIcon = {
onPress = {
() => handleUpvote(post, index)
}
......
And I have
const [mappedPosts, setMappedPosts] = useState(null);
When the component mounts, it takes userPosts from the redux state, maps them out to a ListItem and appropriately displays it. The problem is that whenever handleUpvote() is entered, it sees mappedPosts as null and therefore sets the whole List to null at setMappedPosts(newPosts);
What am I doing wrong here? mappedPosts is indeed not null at the point when handleUpvote() is clicked because.. well how can it be, if a mappedPosts element was what invoked the handleUpvote() method in the first place?
I tried something like
setMappedPosts({
...mappedPosts,
mappedPosts[index]: post
});
But that doesn't even compile. Not sure where to go from here
Edit
Whole component:
const Profile = ({
navigation,
posts: { userPosts, loading },
auth: { user, isAuthenticated },
fetchMedia,
checkAuth,
upvote,
downvote
}) => {
const { navigate, replace, popToTop } = navigation;
const [mappedPosts, setMappedPosts] = useState(null);
useEffect(() => {
if (userPosts) {
userPosts.forEach((post, index) => {
post.userAction = null;
post.likes.forEach(like => {
if (like._id.toString() === user.id) {
post.userAction = "liked";
}
});
post.dislikes.forEach(dislike => {
if (dislike._id.toString() === user.id) {
post.userAction = "disliked";
}
});
});
const mapped = userPosts.map((post, index) => (
<ListItem
Component={TouchableScale}
friction={100}
tension={100}
activeScale={0.95}
key={index}
title={post.title}
bottomDivider={true}
rightIcon={
<View>
<View style={{ flexDirection: "row", justifyContent: "center" }}>
<Icon
name="md-arrow-up"
type="ionicon"
color={post.userAction === "liked" ? "#a45151" : "#517fa4"}
onPress={() => handleUpvote(post, index)}
/>
<View style={{ marginLeft: 10, marginRight: 10 }}>
<Text>{post.likes.length - post.dislikes.length}</Text>
</View>
<Icon
name="md-arrow-down"
type="ionicon"
color={post.userAction === "disliked" ? "#8751a4" : "#517fa4"}
onPress={() => handleDownvote(post, index)}
/>
</View>
<View style={{ flexDirection: "row" }}>
<Text>{post.comments.length} comments</Text>
</View>
</View>
}
leftIcon={
<View style={{ height: 50, width: 50 }}>
<ImagePlaceholder
src={post.image.location}
placeholder={post.image.location}
duration={1000}
showActivityIndicator={true}
activityIndicatorProps={{
size: "large",
color: index % 2 === 0 ? "blue" : "red"
}}
/>
</View>
}
></ListItem>
));
setMappedPosts(mapped);
} else {
checkAuth();
fetchMedia();
}
}, [userPosts, mappedPosts]);
const handleDownvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
if (post.userAction === "dislike") {
newPosts.userAction = null;
} else {
newPosts.userAction = "dislike";
}
setMappedPosts(newPosts);
downvote(user.id, post._id);
};
const handleUpvote = post => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log("mappedPosts", mappedPosts); // null
console.log("newPosts", newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
return mappedPosts === null ? (
<Spinner />
) : (
<ScrollView
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
this.refreshing = true;
fetchMedia();
this.refreshing = false;
}}
/>
}
>
{mappedPosts}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
}
});
Profile.propTypes = {
auth: PropTypes.object.isRequired,
posts: PropTypes.object.isRequired,
fetchMedia: PropTypes.func.isRequired,
checkAuth: PropTypes.func.isRequired,
upvote: PropTypes.func.isRequired,
downvote: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
posts: state.posts
});
export default connect(
mapStateToProps,
{ fetchMedia, checkAuth, upvote, downvote }
)(Profile);
The reason why your current solution doesn't work is because you're rendering userPosts inside of the useEffect hook, which looks like it only runs once, ends up "caching" the initial state, and that's what you end up seeing in your handlers.
You will need to use multiple hooks to get this working properly:
const Profile = (props) => {
// ...
const [mappedPosts, setMappedPosts] = useState(null)
const [renderedPosts, setRenderedPosts] = useState(null)
useEffect(() => {
if (props.userPosts) {
const userPosts = props.userPosts.map(post => {
post.userAction = null;
// ...
})
setMappedPosts(userPosts)
} else {
checkAuth()
fetchMedia()
}
}, [props.userPosts])
const handleDownvote = (post, index) => {
// ...
setMappedPosts(newPosts)
}
const handleUpvote = (post) => {
// ...
setMappedPosts(newPosts)
}
useEffect(() => {
if (!mappedPosts) {
return
}
const renderedPosts = mappedPosts.map((post, index) => {
return (...)
})
setRenderedPosts(renderedPosts)
}, [mappedPosts])
return !renderedPosts ? null : (...)
}
Here's a simplified example that does what you're trying to do:
CodeSandbox
Also, one note, don't do this:
const Profile = (props) => {
const [mappedPosts, setMappedPosts] = useState(null)
useEffect(() => {
if (userPosts) {
setMappedPosts() // DON'T DO THIS!
} else {
// ...
}
}, [userPosts, mappedPosts])
}
Stay away from updating a piece of state inside of a hook that has it in its dependency array. You will run into an infinite loop which will cause your component to keep re-rendering until it crashes.
Let me use a simplified example to explain the problem:
const Example = props => {
const { components_raw } = props;
const [components, setComponents] = useState([]);
const logComponents = () => console.log(components);
useEffect(() => {
// At this point logComponents is equivalent to
// logComponents = () => console.log([])
const components_new = components_raw.map(_ => (
<div onClick={logComponents} />
));
setComponents(components_new);
}, [components_raw]);
return components;
};
As you can see the cycle in which setComponents is called, components is empty []. Once the state is assigned, it stays with the value logComponents had, it doesn't matter if it changes in a future cycle.
To solve it you could modify the necessary element from the received data, no components. Then add the onClick on the return in render.
const Example = props => {
const { data_raw } = props;
const [data, setData] = useState([]);
const logData = () => console.log(data);
useEffect(() => {
const data_new = data_raw.map(data_el => ({
...data_el // Any transformation you need to do to the raw data.
}));
setData(data_new);
}, [data_raw]);
return data.map(data_el => <div {...data_el} onClick={logData} />);
};

Resources