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

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} />
)
}

Related

Change background color after first rendering, but the background transition animations sometimes don't show

Render 5 Mid components.
export default () => {
return (
<>
<Mid />
<Mid />
<Mid />
<Mid />
<Mid />
</>
);
};
Mid is to get the data and only show the Block if the data is not empty
const Mid = () => {
const [data, setData] = useState(null);
useEffect(() => {
const t = setTimeout(() => {
setData([]);
}, 500);
return () => {
clearInterval(t);
};
}, []);
if (data) {
return <Block />;
}
return null;
};
Block has set css transition. After it first rendering, changed the background color in useEffect.
const Block = () => {
const eleRef = useRef();
useEffect(() => {
eleRef.current.style.background = "green";
}, []);
return (
<span
ref={eleRef}
style={{
transition: "background 5s",
background: "#ccc",
margin: 4,
}}
/>
);
};
I hope to see every Block has the background transition animations when it first rendered, but some of them has no animation.
Here is the online demo and gif.
codesandbox demo
What is the cause?

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.

React Redirect Not Loading Page Correctly

When redirecting in react, the page url changes successfully, but nothing loads. In fact, I get a TypeError: n is not a function error. But if I reload the page I redirected to it loads perfectly fine. What am I missing here? Ever since I migrated to hooks, I've struggled to find a straight answer on this. The only links I currently use are located in my App.js and utilize Material-UI buttons.
"react-router-dom": "^5.2.0"
App.js
...
<main className={classes.content}>
<div className={classes.toolbar} />
<BrowserRouter>
<Switch>
<Route exact path='/portal/:stateName/data/' component={DataView} />
<Route exact path='/portal/data-comparison-tool' component={CompareTool} />
<Route exact path='/portal/regional-analysis' component={RegionalAnalysis} />
<Route exact path='/portal/reporting' component={Reporting} />
<Route path='/portal/upload' component={UploadCsv} /> <--- Route in question
<Route exact path='/portal/' component={Dashboard} />
</Switch>
</BrowserRouter>
</main>
...
Dashboard.js
Here is where I am attempting to redirect upon a button click. When the button is selected, the state changes enabling the below functionality.
if (bulkUpload) {
return (
<Redirect to={{ pathname: '/portal/upload', state: { from: props.location } }} />
)
}
UploadCsv
Per other's ask, here is the component I am trying to load.
...
export default function UploadCsv (props) {
const classes = useStyles()
const [doneLoading, setDoneLoading] = useState()
const [uploadData, setUploadData] = useState([])
const [dataSet, setDataSet] = useState(null)
const [columns, setColumns] = useState(tableHfcCols)
const [error, setError] = useState()
const [dbData, setDbData] = useState()
const [isProcessing, setIsProcessing] = useState()
const [stateName, setStateName] = useState()
const [existData, setExistData] = useState([])
const [newData, setNewData] = useState([])
const [openDialog, setOpenDialog] = useState()
const [checks, setChecks] = useState({
invalidCols: [],
endUse: [],
category: [],
state: [],
year: []
})
useEffect(async () => {
async function load () {
// load credential data
const { data } = await axios('/account/fetch')
await setStateName(data.user_state_name)
}
// Load
await load()
await setDoneLoading(true)
}, [])
const loadData = async () => {
// Endpoints
const scenarioEndpoint = `/api/measurements/${stateName}/`
const metricsEndpoint = `/api/metrics/${stateName}/`
// Gather measurements data for specific state
if (dataSet === 'hfc') {
const apiData = await axios(scenarioEndpoint).then(r => r.data.hfc_metrics)
await setDbData(apiData)
return apiData
} else if (dataSet === 'state') {
const apiData = await axios(metricsEndpoint).then(r => r.data.state_metrics)
await setDbData(apiData)
return apiData
}
}
// add file to upload box
const handleOnFileLoad = async (data) => {
const headerClean = await CleanHeader(data)
const dataSetName = await WhichData(headerClean)
if (dataSetName === 'hfc') {
const cleanData = await headerClean.map(d => ShapeHfcData(d))
await setDataSet(dataSetName)
await setColumns(tableHfcCols)
await setUploadData(cleanData)
// analyze data
const { invalidCols } = await CheckColumns({ setName: 'hfc', data: cleanData })
await setChecks({ ...checks, invalidCols: invalidCols })
// clean end_use fields
} else if (dataSetName === 'state') {
const cleanData = await headerClean.map(d => ShapeStateData(d))
await setDataSet(dataSetName)
await setColumns(tableStateCols)
await setUploadData(headerClean)
// analyze data
const { invalidCols } = await CheckColumns({ setName: 'state', data: cleanData })
await setChecks({ ...checks, invalidCols: invalidCols })
}
}
// Remove file from upload field
const handleOnRemoveFile = async (data) => {
setUploadData([])
setColumns([])
setDataSet()
}
const handlePrepSubmit = async () => {
setIsProcessing(true)
// get data
const apiData = await loadData()
// check which records exist and which don't
const { exist, notExist } = await parseData({ apiData: apiData, uploadData: uploadData })
// run checks and return results
const { endUseCheck, categoryCheck, yearCheck, stateCheck } = await runChecks(uploadData)
// update state
await setChecks({
...checks,
endUse: endUseCheck,
category: categoryCheck,
state: stateCheck,
year: yearCheck
})
await setExistData(exist)
await setNewData(notExist)
await setOpenDialog(true)
}
const runChecks = async (data) => {
// check if endUse values are correct
const endUseCheck = []
const categoryCheck = []
const yearCheck = []
const stateCheck = []
data.forEach(async function (row) {
const endUseStatus = await CheckEndUse(row.end_use)
const categoryStatus = await CheckCategory(row.category)
const stateStatus = await CheckState(row.state)
const yearStatus = await CheckYear(row.year)
if (endUseStatus === 'fail') {
endUseCheck.push(row)
} else if (categoryStatus === 'fail') {
categoryCheck.push(row)
} else if (stateStatus === 'fail') {
stateCheck.push(row)
} else if (yearStatus === 'fail') {
yearCheck.push(row)
}
})
return { endUseCheck, categoryCheck, yearCheck, stateCheck }
}
// Check to see which are updates and which are new records
const parseData = ({ apiData, uploadData }) => {
const exist = []
const notExist = []
const hfcHeader = [[...HfcHeader, ['id']].flat()][0]
uploadData.forEach(function (row) {
const dbRecordsYear = reducedFilter(apiData, hfcHeader, item => item.year === row.year)
const dbRecordsCategory = reducedFilter(dbRecordsYear, hfcHeader, item => item.category === row.category)
const record = reducedFilter(dbRecordsCategory, hfcHeader, item => item.end_use === row.end_use)
const apiRecord = reducedFilterId(dbRecordsCategory, hfcHeader, item => item.end_use === row.end_use)
if (record.length > 0) {
row.id = apiRecord[0].id
exist.push(row)
} else if (record.length < 1) {
notExist.push(row)
}
})
return { exist, notExist }
}
// Dialog Actions
const handleDialogClose = () => {
setOpenDialog(false)
}
if (!doneLoading) {
return <CircularProgress color='secondary' size='2.5rem' thickness='2.0' />
}
const submitData = async () => {
const location = dataSet === 'hfc' ? 'measurements' : 'metrics'
if (existData.length > 0) {
existData.forEach(function (row) {
updateRecord(row, location)
console.log('update', row)
})
}
if (newData.length > 0) {
newData.forEach(function (row) {
createRecord(row, location)
console.log('create', row)
})
}
}
const updateRecord = async (row, location) => {
// If the object exists, update it via API
const endpoint = `/api/${location}/record/${row.id}/`
await axios.put(endpoint, row)
}
const createRecord = async (row, location) => {
const endpoint = `/api/${location}/create`
await axios.post(endpoint, row)
}
console.log(uploadData)
return (
<>
<Grid container className={classes.container}>
<Toolbar className={classes.pageHeader}>
<Typography className={classes.title} variant='h3' color='secondary' align='left'>
Upload Data
</Typography>
</Toolbar>
<Grid item xs={12}>
<Paper className={classes.stagePaper}>
<ReactMarkdown
source={HelpText}
escapeHtml={false}
/>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.middlePaper}>
<Grid container className={classes.container}>
<Toolbar className={classes.pageHeader}>
<Typography className={classes.title} variant='h5' color='secondary' align='left'>
Sample Data Sets
</Typography>
</Toolbar>
<Grid item xs={12}>
<SampleDataText />
</Grid>
<Grid item xs={6} className={classes.innerGrid}>
<Button variant='outlined' fullWidth color='secondary' onClick={() => csvDownload(hfcData, 'scenario_sample_data.csv')}>Scenario Data</Button>
</Grid>
<Grid item xs={6} className={classes.innerGrid}>
<Button variant='outlined' fullWidth color='primary' onClick={() => csvDownload(stateData, 'state_sample_data.csv')}>State Data</Button>
</Grid>
</Grid>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.middlePaper}>
<Toolbar className={classes.pageHeader}>
<Typography className={classes.title} variant='h5' color='secondary' align='left'>
Upload .CSV File
</Typography>
</Toolbar>
<Upload
setError={setError}
error={error}
handleOnFileLoad={handleOnFileLoad}
handleOnRemoveFile={handleOnRemoveFile}
/>
</Paper>
</Grid>
{uploadData.length < 1
? ''
: <Grid item xs={12} className={classes.stagePaper}>
<MaterialButton
onClick={() => handlePrepSubmit()}
disabled={isProcessing}
text={isProcessing ? 'PROCESSING PRELIM CHECKS...' : 'BEGIN UPLOAD'}
color='accent'
width='100'
/>
</Grid>}
<Grid item xs={12}>
<Paper className={classes.stagePaper}>
<link rel='stylesheet' href='https://fonts.googleapis.com/icon?family=Material+Icons' />
<CustomMaterialTable
title='Preview Data'
columns={(uploadData.length > 0) ? columns : []}
data={(uploadData.length > 0) ? uploadData : []}
components={{
Container: props => props.children
}}
options={{
headerStyle: {
padding: '10px 4px 10px 4px',
fontSize: '.85rem'
},
cellStyle: {
padding: '14px 4px 14px 4px',
fontSize: '.8rem'
},
pageSize: uploadData.length,
selection: true,
rowStyle: rowData => ({ backgroundColor: rowData.tableData.checked ? '#37b15933' : '' })
}}
editable={{
onRowAddCancelled: rowData => console.log('Row adding cancelled'),
onRowUpdateCancelled: rowData => console.log('Row editing cancelled'),
onRowAdd: newData =>
new Promise((resolve, reject) => {
setTimeout(() => {
/* setData([...data, newData]); */
resolve()
}, 1000)
}),
onRowUpdate: (newData, oldData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
const dataUpdate = [...uploadData]
const index = oldData.tableData.id
dataUpdate[index] = newData
setUploadData([...dataUpdate])
resolve()
}, 1000)
}),
onRowDelete: oldData =>
new Promise((resolve, reject) => {
setTimeout(() => {
const dataDelete = [...uploadData]
const index = oldData.tableData.id
dataDelete.splice(index, 1)
setUploadData([...dataDelete])
resolve()
}, 1000)
})
}}
/>
</Paper>
</Grid>
</Grid>
<Footer />
<UploadPop
openDialog={openDialog}
handleDialogClose={handleDialogClose}
existData={existData}
newData={newData}
submitData={submitData}
checks={checks}
/>
</>
)
}
Try changing the route path for /portal/upload/ to
<Route exact path='/portal/upload' component={UploadCsv} />
If this does not fix it, you might have to post the definition fro {UploadCsv}
I was never able to find a straight answer to this. It's apparently a pretty common problem, and from what I've seen most people seem to be working around it. My headaches around React-Router are partially why I enjoy working with Nextjs so much.
Here's my workaround since I was never able to get history.push(/portal/) to work correctly using React Router v5.
window.location.href = '/portal/'
It's not an ideal solution but solves 90% of my use case. Time to move on.

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