#reactjs
Вопрос:
Я создаю внешний компонент, который будет загружать два разных изображения-фотографию обложки и фотографию аватара пользователя. Я использую node.js для моего бэкенда с express.js. Если пользователь загрузит фотографию обложки, серверная часть отправит запрос на отправку(путь к изображению, который он защищает, если изображение является фотографией обложки или аватарфото). Но у меня возникли проблемы с тем, как загрузить их обоих после того, как пользователь загрузит изображение. я использовал разные useState userInfo.coverPhoto и userInfo.avatarPhoto. Проблема в том, что после того, как пользователь нажмет на обложку, а затем на аватарфото, изображение src coverPhoto будет разбито. но после обновления изображение будет там.
userProfile.js
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import axios from 'axios';
import PersonIcon from '@material-ui/icons/Person';
import {
Container,
Grid,
Typography,
TextField,
Button,
Modal,
Backdrop,
} from '@material-ui/core';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import ImageIcon from '@material-ui/icons/Image';
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
import { useForm } from 'react-hook-form';
import FormContainer from '../components/FormContainer';
import useStyles from '../styles/style';
import {
getDetailsUser,
updateDetailsUser,
} from '../redux/actions/userActions';
const ProfilePage = ({ history }) => {
const userDetails = useSelector((state) => state.userDetails);
const { user } = userDetails;
const userLogin = useSelector((state) => state.userLogin);
const { userData } = userLogin;
const userUpdateDetails = useSelector((state) => state.userUpdateDetails);
const { userUpdate } = userUpdateDetails;
const [userInfo, setUserInfo] = useState({
id: user._id,
username: '',
email: '',
fullname: '',
coverPhoto: '',
avatarPhoto: '',
about: '',
mainAddress: '',
country: '',
city: '',
zipcode: '',
});
const [coverPreview, setCoverPreview] = useState('');
const [avatarPreview, setAvatarPreview] = useState('');
const [open, setOpen] = useState(false);
const dispatch = useDispatch();
const classes = useStyles();
useEffect(() => {
if (!userData) {
history.push('/user/login');
} else {
if (!user || !user.username || userUpdate) {
dispatch(getDetailsUser('profile'));
// if (!userInfo.coverPhoto) {
// document.getElementById(
// 'coverPhoto-file-button'
// ).nextElementSibling.style.opacity = '0';
// }
} else {
setUserInfo(user);
}
}
}, [userData, user, userUpdate, history]);
const handleClose = () => {
setOpen(false);
};
const handleChange = (level) => (e) => {
e.preventDefault();
if (!level) {
setUserInfo({
...userInfo,
[e.target.name]: e.target.value,
});
} else {
setUserInfo({
...userInfo,
[level]: {
...userInfo[level],
[e.target.name]: e.target.value,
},
});
}
};
const uploadCoverPhotoHandler = async (e) => {
const imgFile = e.target.files[0];
const imgFieldName = e.target.name;
if (imgFile.type !== 'image/jpeg' amp;amp; imgFile.type !== 'image/png') {
console.log('wrong format');
}
let formData = new FormData();
formData.append(imgFieldName, imgFile);
try {
await axios({
method: 'POST',
url: 'http://localhost:5000/user/profile',
data: formData,
headers: {
'content-type': 'multipart/form-data',
},
}).then((res) => {
if (imgFieldName === 'coverPhoto') {
setUserInfo({ coverPhoto: res.data });
} else {
setUserInfo({ avatarPhoto: res.data });
}
});
} catch (error) {
console.log(error.response);
}
};
const onSubmit = async (e) => {
dispatch(updateDetailsUser(userInfo));
setOpen(true);
};
const { register, handleSubmit, errors, getValues } = useForm({
mode: 'onSubmit',
reValidateMode: 'onBlur',
});
return (
<FormContainer>
<Container component="div" className={classes.profileContainer}>
<Modal
className={classes.modal}
open={open}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
root: classes.modalRoot,
timeout: 500,
}}
>
<div className={classes.modalContainer}>
<Typography
component="h1"
variant="h5"
className={classes.promptTitle}
>
Account has been updated!
</Typography>
<Button
type="submit"
variant="contained"
className={classes.button}
onClick={handleClose}
style={{ width: '60%', marginBottom: '2.5rem' }}
>
Close
</Button>
</div>
</Modal>
<Grid item xs={12}>
<Card className={classes.userProfileCard}>
<CardContent>
<form noValidate onSubmit={handleSubmit(onSubmit)}>
<Typography
component="h1"
variant="h4"
className={classes.titleProfile}
>
<PersonIcon
style={{
position: 'relative',
top: '5px',
marginRight: '0.2em',
}}
/>
Edit Profile
</Typography>
<Grid container spacing={2}>
<Grid item xs={6}>
<TextField
variant="outlined"
id="username"
name="username"
type="text"
label="Username"
value={userInfo amp;amp; userInfo.username}
onChange={handleChange()}
fullWidth
InputLabelProps={{
classes: {
root: classes.label,
focused: classes.focused,
},
shrink: true,
}}
InputProps={{
className: classes.textfield,
classes: {
root: classes.cssOutlinedInput,
focused: classes.cssFocused,
notchedOutline: classes.notchedOutline,
},
}}
inputRef={register({
required: 'You must provide an username.',
minLength: {
value: 4,
message:
'Your username must be greater than 4 characters',
},
pattern: {
value: /^[A-Za-z0-9_] $/i,
message:
'Username may only have letters, number and underscores.',
},
})}
/>
{errors.username amp;amp; (
<span className={classes.error}>
{errors.username.message}
</span>
)}
</Grid>
<Grid item xs={6}>
<TextField
variant="outlined"
id="email"
name="email"
type="email"
label="Email"
value={userInfo amp;amp; userInfo.email}
onChange={handleChange()}
fullWidth
InputLabelProps={{
classes: {
root: classes.label,
focused: classes.focused,
},
shrink: true,
}}
InputProps={{
className: classes.textfield,
classes: {
root: classes.cssOutlinedInput,
focused: classes.cssFocused,
notchedOutline: classes.notchedOutline,
},
}}
inputRef={register({
required: 'You must provide a email.',
pattern: {
value: /^[^@ ] @[^@ ] .[^@ .]{2,}$/,
message: 'You must provide a valid email address!',
},
})}
/>
{errors.email amp;amp; (
<span className={classes.error}>
{errors.email.message}
</span>
)}
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
id="fullname"
name="fullname"
type="text"
label="Fullname"
value={userInfo amp;amp; userInfo.fullname}
onChange={handleChange()}
fullWidth
InputLabelProps={{
classes: {
root: classes.label,
focused: classes.focused,
},
shrink: true,
}}
InputProps={{
className: classes.textfield,
classes: {
root: classes.cssOutlinedInput,
focused: classes.cssFocused,
notchedOutline: classes.notchedOutline,
},
}}
inputRef={register({
required: 'You must provide a fullname.',
minLength: {
value: 6,
message:
'Your password must be greater than 6 characters',
},
pattern: {
value: /^[A-Za-z ] $/i,
message: 'Alphabetical characters only',
},
})}
/>
{errors.fullname amp;amp; (
<span className={classes.error}>
{errors.fullname.message}
</span>
)}
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
id="mainAddress"
name="mainAddress"
type="text"
label="Address"
value={userInfo.address amp;amp; userInfo.address.mainAddress}
onChange={handleChange('address')}
fullWidth
InputLabelProps={{
classes: {
root: classes.label,
focused: classes.focused,
},
shrink: true,
}}
InputProps={{
className: classes.textfield,
classes: {
root: classes.cssOutlinedInput,
focused: classes.cssFocused,
notchedOutline: classes.notchedOutline,
},
}}
/>
</Grid>
<Grid item xs={2}>
<TextField
variant="outlined"
id="city"
name="city"
type="text"
label="City"
value={userInfo.address amp;amp; userInfo.address.city}
onChange={handleChange('address')}
fullWidth
InputLabelProps={{
classes: {
root: classes.label,
focused: classes.focused,
},
shrink: true,
}}
InputProps={{
className: classes.textfield,
classes: {
root: classes.cssOutlinedInput,
focused: classes.cssFocused,
notchedOutline: classes.notchedOutline,
},
}}
/>
</Grid>
<Grid item xs={2}>
<TextField
variant="outlined"
id="country"
name="country"
type="text"
label="Country"
value={userInfo.address amp;amp; userInfo.address.country}
onChange={handleChange('address')}
fullWidth
InputLabelProps={{
classes: {
root: classes.label,
focused: classes.focused,
},
shrink: true,
}}
InputProps={{
className: classes.textfield,
classes: {
root: classes.cssOutlinedInput,
focused: classes.cssFocused,
notchedOutline: classes.notchedOutline,
},
}}
/>
</Grid>
<Grid item xs={2}>
<TextField
variant="outlined"
id="zipcode"
name="zipcode"
type="text"
label="Zipcode"
value={userInfo.address amp;amp; userInfo.address.zipcode}
onChange={handleChange('address')}
fullWidth
InputLabelProps={{
shrink: true,
classes: {
root: classes.label,
focused: classes.focused,
},
}}
InputProps={{
className: classes.textfield,
classes: {
root: classes.cssOutlinedInput,
focused: classes.cssFocused,
notchedOutline: classes.notchedOutline,
},
}}
inputRef={register({
pattern: {
value: /^[0-9] ([0-9] )?$/,
message: 'Numbers only',
},
})}
/>
{errors.zipcode amp;amp; (
<span className={classes.error}>
{errors.zipcode.message}
</span>
)}
</Grid>
<Grid item xs={12}>
<Grid item xs={12}>
<TextField
variant="outlined"
id="aboutme"
name="about"
type="text"
label="About me"
value={userInfo amp;amp; userInfo.about}
onChange={handleChange()}
fullWidth
multiline
rows={2}
InputLabelProps={{
classes: {
root: classes.label,
focused: classes.focused,
},
shrink: true,
}}
InputProps={{
className: classes.textfield,
classes: {
root: classes.cssOutlinedInput,
focused: classes.cssFocused,
notchedOutline: classes.notchedOutline,
},
}}
/>
</Grid>
</Grid>
<Grid item xs={2}>
<Button
type="submit"
fullWidth
variant="contained"
className={classes.button}
>
Update
</Button>
</Grid>
</Grid>
</form>
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card className={classes.userCard}>
<form id="userPhoto">
<CardContent className={classes.coverPhotoContainer}>
<CardMedia
component="img"
src={userInfo amp;amp; userInfo.coverPhoto}
height="180rem"
onload="this.style.display=''"
/>
<input
accept="image/*"
id="coverPhoto-file-button"
type="file"
name="coverPhoto"
style={{ display: 'none' }}
onChange={uploadCoverPhotoHandler}
/>
<label htmlFor="coverPhoto-file-button">
<ImageIcon />
Add Cover Photo
</label>
</CardContent>
<CardContent className={classes.imgProfileContainer}>
<CardMedia
component="img"
src={userInfo amp;amp; userInfo.avatarPhoto}
height="180rem"
onload="this.style.display=''"
/>
<input
accept="image/*"
id="imgProfile-file-button"
type="file"
name="avatarPhoto"
style={{ display: 'none' }}
onChange={uploadCoverPhotoHandler}
/>
<label htmlFor="imgProfile-file-button">
<AccountCircleIcon />
</label>
</CardContent>
</form>
<CardContent>
<Typography
className={classes.titleProfileUsername}
variant="h6"
component="h1"
>
{userInfo amp;amp; userInfo.username}
</Typography>
<Typography
className={classes.profileAbout}
variant="body2"
component="p"
></Typography>
</CardContent>
</Card>
</Grid>
</Container>
</FormContainer>
);
};
export default ProfilePage;
Ошибка
Это произойдет после загрузки аватарфото, изображение с обложки исчезнет, но после обновления страницы оно появится снова.
Ответ №1:
useState
работает иначе this.setState
, чем в компонентах класса.
при записи setUserInfo({ coverPhoto: res.data })
вы удаляете все остальные поля из userInfo
.
Тебе нужно написать:
setUserInfo(prevState => ({ ...prevState, coverPhoto: res.data }))
}).then((res) => {
if (imgFieldName === 'coverPhoto') {
setUserInfo(prevState => ({ ...prevState, coverPhoto: res.data }));
} else {
setUserInfo(prevState => ({ ...prevState, avatarPhoto: res.data }));
}
});
Комментарии:
1. Святая Молли, я забыл, что должен был использовать оператор распространения. таким образом, предыдущее состояние не изменится. Огромное спасибо. Я упускаю из виду эту часть.