Я строю нативное приложение React (используя Expo), где я отображаю отобранные пользователем изображения с использованием компонента изображения по умолчанию.
Изображение не обновляется даже после:
Изображение
Код: Выделить всё
Changing the URI by appending a timestamp:
< /code>
source={{ uri: imageUri + `?ts=${Date.now()}` }}
< /code>
Trying cache: 'reload' (with expo-image)
Deleting the original file using expo-file-system
Renaming the file to generate a new path
Using external remote URLs (same problem occurs)
Adding key={Date.now()} to force re-render
< /code>
Local image example:
file:///data/user/0/host.exp.exponent/cache/ExperienceData/%2540romjan-ali%252Fmra-jobs/ImagePicker/user_70f7fddb-80e4-4b91-ade5-1df5fb6fb9d1.jpeg
Despite doing all of this, [b]the image component seems to still use a cached version and does not reflect the updated image. When I go away from the component after uploading the image and come back to the component, it displays the previously uploaded image, although the image should be changed.[/b] Cloudinary overwrites the file, which means the image changes correctly with the same link in Cloudinary.
✅ What Worked for Me (Partial Solution):
Copying the file to a completely new unique file path using FileSystem.copyAsync():
import * as FileSystem from 'expo-file-system';
const newPath = FileSystem.cacheDirectory + `img_${Date.now()}.jpg`;
await FileSystem.copyAsync({
from: originalPath,
to: newPath,
});
< /code>
Then use:
Это работает - но он кажется хакерским, особенно для удаленных изображений, где копирование не является вариантом.
Как я могу надежно заставлять компонент изображения, чтобы перезагрузить последнее изображение (из локального или повторного URI), если имени файла остается таким же? Я должен избегать изображения по умолчанию React Native и переходить на что -то еще? Если да, то каково легкое и совместимое с экспозицией решение для гарантированного перезагрузки изображений?
React Native: via Expo SDK 50+
expo-image-picker for image selection
expo-file-system
Tested on Android Emulator
< /code>
My full code given below:
import {
View,
Text,
TextInput,
Pressable,
BackHandler,
Button,
Alert,
ActivityIndicator,
ToastAndroid,
Image as ReactNativeImage
} from "react-native";
import { Image } from 'expo-image'
import React, { useEffect, useRef, useState } from "react";
import * as ImagePicker from "expo-image-picker";
import uuid from "react-native-uuid";
import * as FileSystem from "expo-file-system";
import * as Network from "expo-network";
import fetchCurrentDateTime from "../app/stores/date-time-api.js";
import profileImg from "../assets/images/profile.png";
import useUserStore from "../app/stores/user-store.js";
import useImageStore from "../app/stores/image-store.js";
import GradientButton from "./GradientButton.jsx";
// import { CacheManager } from 'expo-cached-image';
// await CacheManager.cleanupCache({ size: 100 });
const SignupProfilePicture = ({ setCurrentComponent }) => {
const { signupUser, tempUser, updateTempUser } = useUserStore();
const {
cloudinaryCloudName,
uploadImageToCloudinary,
isStoredFileExists,
getStoredFileUri,
fileUriToCloudinaryPublicId,
downloadAndSaveImage,
deleteImageFromCloudinary,
deleteFileFromFileSystem,
imageError,
} = useImageStore();
const [error, setError] = useState({
uploadError: null,
deleteError: null,
});
const [imageUri, setImageUri] = useState(null);
const hasRunRef = useRef(false);
const isMounted = useRef(false);
const isChangedImageUri = useRef(false);
const hasRunImageUriRef = useRef(false)
const [isUploading, setIsUploading] = useState(false);
const [isNextButtonPressed, setIsNextButtonPressed] = useState(false)
const [isDeleteButtonPressed, setIsDeleteButtonPressed] = useState(false)
const profileImageInitInfo = {
folderName: 'profile-image',
prefix: 'user_'
}
/* useEffect(() => {
if (isNextButtonPressed && imageError?.uploadError) {
ToastAndroid.show(`Error while uploading image: ${imageError?.uploadError}`, ToastAndroid.LONG)
}
if (isDeleteButtonPressed && imageError?.deleteError) {
ToastAndroid.show(`Error while deleting image: ${imageError?.deleteError}`, ToastAndroid.LONG)
}
setError(imageError);
setIsNextButtonPressed(false)
setIsDeleteButtonPressed(false)
}, [imageError]); */
// Pick an image from gallery with ratio 1 : 1
const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: true,
aspect: [1, 1],
quality: 1,
});
// Delete the file from file system
if (imageUri) {
const { folderName, prefix } = profileImageInitInfo
console.log('image picked and sent request to delete previous image')
// await deleteFileFromFileSystem(folderName, prefix);
setImageUri(null);
}
// If successfully picked an image
if (!result.canceled) {
checkImageUrl(result.assets[0].uri);
console.log('(pickImage) result.assets[0].uri:', result.assets[0].uri)
if (tempUser?.profileImage?.cloudinaryPublicId) {
const result = await deleteImageFromCloudinary(
tempUser?.profileImage?.cloudinaryPublicId
);
if (!result) {
return;
}
// If image deleted successfully, update tempUser profileImage
updateTempUser({
...tempUser,
profileImage: {},
});
}
}
};
// Check the image url is valid or not, set image uri if valid
const checkImageUrl = async (url) => {
try {
const success = await ReactNativeImage.prefetch(url);
if (success) {
setImageUri(url);
} else {
setImageUri(null);
}
return success;
} catch (error) {
setImageUri(null);
return null;
}
};
const isOnline = async () => {
const { isInternetReachable } = await Network.getNetworkStateAsync()
return isInternetReachable ?? false;
}
// Display image url text as the logic
const imageUrlValue = (text) => {
checkImageUrl(text);
return text && text.startsWith("file") ? null : text;
};
// To get image uri using tempUser?.profileImage?.cloudinaryPublicId
const getImageUriAndUpdateTempUser = async () => {
if (!tempUser) {
return;
}
hasRunRef.current = true;
try {
let imageUrl;
const folderName = "profile-image";
const fileNamePrefix = 'user_'
const fileNameExtension = '.jpg'
const cloud_name = cloudinaryCloudName;
// const storedFileUri = await getStoredFileUri(folderName, fileNamePrefix)
// console.log("stored file uri:", storedFileUri);
let public_id;
// If stored file is exists and public_id is null
/* if (storedFileUri) {
console.log("stored file uri:", typeof storedFileUri);
checkImageUrl(storedFileUri); // set image uri
// cloudinaryPublicId will not set until image uploaded
// create a public id by using folderName and storedFileUri
} */
// Update cloudinaryPublicId of profileImage in tempUser data
const update_public_id = (public_id) => {
updateTempUser({
...tempUser,
profileImage: {
...tempUser.profileImage,
cloudinaryPublicId: public_id,
},
});
};
console.log('tempUser?.profileImage?.cloudinaryPublicId', tempUser?.profileImage?.cloudinaryPublicId)
// If public id exists, download and store/update the profile image in device cache
if (tempUser?.profileImage?.cloudinaryPublicId) {
const public_id = tempUser?.profileImage?.cloudinaryPublicId;
imageUrl = `
(Math.random() * 100 + 100)}`;console.log({ imageUrl })
// Create file directory in device cache, download and save to the directory
/* const image_uri = await downloadAndSaveImage(
imageUrl,
folderName,
fileNamePrefix,
fileNameExtension
); */
/* if (image_uri) {
checkImageUrl(image_uri); // update setImageUri
} else {
console.log("Error while download and save an image")
} */
checkImageUrl(imageUrl)
// setImageUri(imageUrl)
}
} catch (error) {
console.log("Error getting image uri:", error);
}
};
useEffect(() => {
if (!tempUser || hasRunRef.current) {
return;
}
getImageUriAndUpdateTempUser();
}, [tempUser]);
useEffect(() => {
// When the component mounts, we set isMounted.current to true (this is by default)
isMounted.current = true;
// Cleanup when component unmounts
return () => {
isMounted.current = false; // Component is unmounted
hasRunRef.current = false;
setError({
uploadError: null,
deleteError: null,
})
};
}, []);
// Handle delete button press
const handleDeleteButtonPress = async () => {
const image_uri = imageUri
setIsDeleteButtonPressed(true)
setImageUri(null);
if (tempUser?.profileImage?.cloudinaryPublicId) {
const result = await deleteImageFromCloudinary(tempUser?.profileImage?.cloudinaryPublicId);
if (!result) {
setImageUri(image_uri)
return
};
updateTempUser({
...tempUser,
profileImage: {},
});
}
const { folderName, prefix } = profileImageInitInfo
// await deleteFileFromFileSystem(folderName, prefix);
// ToastAndroid.show("Image deleted successfully: ", ToastAndroid.SHORT)
};
// Update profile image uri in centralized data of tempUser
/* useEffect(() => {
setError({});
if (imageUri && hasRunImageUriRef.current) {
isChangedImageUri.current = true;
}
if (imageUri) hasRunImageUriRef.current = true;
// console.log('image uri', imageUri)
// console.log('tempUser', tempUser)
}, [imageUri]); */
// Function to handle previous button press
const handlePrevButtonPress = () => {
setCurrentComponent("transaction-details");
};
// Function to upload profile image
const uploadProfileImage = async () => {
try {
if (isUploading) return;
setIsUploading(true);
console.log("image uri", imageUri);
if (!imageUri) {
console.log("Please upload an image or input a valid url to proceed.");
return;
}
let data;
const uniqueId = tempUser?._id || uuid.v4();
if (!tempUser?._id) {
updateTempUser({ ...tempUser, _id: uniqueId });
}
const folderName = "profile-images";
const fileName = `user_${uniqueId}`;
// console.log("fileName", fileName); // Here fileName appearing like this: user_41b2a1a8-8324-4014-b24e-dda2d85459d9
if (tempUser?.profileImage?.cloudinaryPublicId) {
// console.log("publicId", tempUser?.profileImage?.cloudinaryPublicId);
const image_id = tempUser?.profileImage?.cloudinaryPublicId
.split("/")
.slice(1)
.join("/");
data = await uploadImageToCloudinary(
folderName,
fileName,
imageUri,
image_id
);
} else {
data = await uploadImageToCloudinary(folderName, fileName, imageUri);
}
if (!data) return null;
const currentDateTime = await fetchCurrentDateTime();
if (!currentDateTime) return null;
const profileImageData = {
cloudinaryPublicId: data?.public_id,
lastModifiedAt: currentDateTime,
};
updateTempUser({
...tempUser,
profileImage: profileImageData,
});
setIsUploading(false);
return profileImageData;
} catch (error) {
console.log(
"Error:",
error.response ? error.response.data : error.message
);
setIsUploading(false);
return null;
}
};
// Function to handle next button press
const handleNextButtonPress = async () => {
setIsNextButtonPressed(true)
if (!imageUri) {
setError({ ...error, uploadError: "Please upload an image to proceed." });
return;
}
const public_id = tempUser?.profileImage?.cloudinaryPublicId;
console.log('is changed image uri', isChangedImageUri.current)
if (isChangedImageUri.current || !public_id) {
const result = await uploadProfileImage()
if (result) {
ToastAndroid.show("Image uploaded successfully", ToastAndroid.SHORT)
// setCurrentComponent("nid-picture")
}
} else {
// setCurrentComponent("nid-picture")
}
};
const [imageContainerHeight, setImageContainerHeight] = useState(0);
const handleImageContainerLayout = (event) => {
const { height } = event.nativeEvent.layout;
if (!imageContainerHeight) {
setImageContainerHeight(height);
}
};
return (
Profile Picture
Image from Gallery (Limited Data Size)
{isUploading ? (
) : (
{imageUri ? (
) : (
)}
{imageUri && (
)}
{/*
Or
Image URL
{
imageUrlValue(text)
}}
numberOfLines={1}
/>
*/}
)}
{/* {error && (
{error?.uploadError || error.deleteError}
)} */}
Previous
Next
);
};
export default SignupProfilePicture;
< /code>
image-store.js:
import { create } from 'zustand'
import * as FileSystem from 'expo-file-system'
import { nanoid } from 'nanoid/non-secure'
import api from './api.js'
import fetchCurrentDateTime from './date-time-api.js'
const useImageStore = create((set, get) => ({
imageError: {
uploadError: null,
deleteError: null,
},
cloudinaryCloudName: 'dic1ix17e',
randomId: async () => {
const currentTime = await fetchCurrentDateTime()
const newDate = new Date(currentTime)
return newDate.getTime().toString(36) + Math.random().toString(36).slice(2)
},
uploadImageToCloudinary: async (folderName, fileName, imageUri, image_id) => {
// Value of image_id parameter is optional
// Example value of image_id: 'profile-images/m97vj4ls5j7iclpf3mp.jpg'
console.log('uploadImageToCloudinary()')
console.log('folderName:', folderName)
console.log('fileName:', fileName)
console.log('imageUri:', imageUri)
console.log('image_id:', image_id)
if (!imageUri) {
console.log('imageUri:', imageUri)
return
}
if (!folderName) {
console.log('folderName:', folderName)
return
}
try {
const public_id = image_id || `${folderName}/${fileName}`
console.log('public_id', public_id)
const { signature, timestamp } = await api
.post('/image/generate-signature', { public_id })
.then((res) => res.data)
const formData = new FormData()
formData.append('file', {
uri: imageUri,
type: 'image/jpeg',
name: 'upload.jpg',
})
formData.append('upload_preset', 'mra_jobs')
formData.append('public_id', public_id)
formData.append('api_key', '333591318713297')
formData.append('timestamp', timestamp)
formData.append('signature', signature)
formData.append('cloud_name', 'dic1ix17e')
formData.append('invalidate', true)
const cloudinary_response = await fetch(
'https://api.cloudinary.com/v1_1/dic1ix17e/image/upload',
{
method: 'POST',
body: formData,
}
)
const data = await cloudinary_response.json()
// console.log('public id from cloudinary', data.public_id)
// console.log('data from cloudinary', data)
const { imageError } = get()
set({
imageError: {
...imageError,
uploadError: null,
},
})
return data
} catch (error) {
// console.log('|uploadImageToCloudinary()| Error while upload image:', error.response ? error.response.data : error.message);
const { imageError } = get()
set({
imageError: {
...imageError,
uploadError: error?.message || error?.response.data,
},
})
return null
}
},
/*
Example to generate url to get an image from cloudinary:
`
`*/
isStoredFileExists: async (dirName, prefix) => {
const files = await FileSystem.readDirectoryAsync(dirName)
const matchedFiles = files.filter((file) => file.startsWith(prefix))
return matchedFiles.length > 0
},
getStoredFileUri: async (dirName, prefix) => {
try {
const dirUri = `${FileSystem.documentDirectory}${dirName}/`
const files = await FileSystem.readDirectoryAsync(dirUri)
const matchedFiles = files.filter((file) => file.startsWith(prefix))
// console.log({dirName, prefix, dirUri, files, matchedFiles})
if (matchedFiles.length > 0) {
return `${dirUri}/${matchedFiles[0]}`
}
return null
} catch (error) {
console.log('Error getting file from file system:', error)
return null
}
},
fileUriToCloudinaryPublicId: (folderName, fileUri) => {
// Match the path starting from /profile-image/ up to but not including the file extension
const regex = new RegExp(`/${folderName}/[^.]+`)
const match = fileUri.match(regex)
const result = match ? match[0].substring(1) : null
return result
// Example input: 'file:///data/user/0/host.exp.exponent/files/mra_jobs/profile-images/more/user_1c09dd5b-5f84-4396-a812-54fdc6d8e823.jpg'
// Example output: 'mra_jobs/profile-images/more/user_1c09dd5b-5f84-4396-a812-54fdc6d8e823'
},
downloadAndSaveImage: async (
imageUrl,
dirName,
fileNamePrefix,
fileNameExtension
) => {
const uniqueId = nanoid()
const dirUri = FileSystem.documentDirectory + dirName + '/'
const dirInfo = await FileSystem.getInfoAsync(dirUri)
console.log('(downloadAndSaveImage)', {
imageUrl,
dirName,
fileNamePrefix,
fileNameExtension,
dirUri,
})
console.log('(downloadAndSaveImage) dirUri', dirUri)
console.log('(downloadAndSaveImage) FileSystem.documentDirectory', FileSystem.documentDirectory)
if (!dirInfo.exists) {
await FileSystem.makeDirectoryAsync(dirUri, { intermediates: true })
console.log('Directory created:', dirUri)
} else {
console.log('Directory already exists:', dirUri)
}
const fileUri = dirUri + fileNamePrefix + uniqueId + fileNameExtension
const downloadResumable = FileSystem.createDownloadResumable(
imageUrl,
fileUri
)
const { uri } = await downloadResumable.downloadAsync()
console.log('Image saved to:', uri)
return uri
},
deleteFileFromFileSystem: async (dirName, prefix) => {
try {
const dirUri = `${FileSystem.documentDirectory}${dirName}/`
const files = await FileSystem.readDirectoryAsync(dirUri)
const matchedFiles = files.filter((file) => file.startsWith(prefix))
console.log('delete file from file system', {
dirName,
prefix,
files,
matchedFiles,
dirUri,
})
const deletedFiles = []
if (matchedFiles.length > 0) {
for (let file of matchedFiles) {
const fullPath = `${dirUri}${file}`
await FileSystem.deleteAsync(fullPath)
const checkInfo = await FileSystem.getInfoAsync(fullPath)
if (!checkInfo.exists) {
deletedFiles.push(fullPath)
} else {
console.warn('File still exists after deletion complete', fullPath)
return false
}
}
}
if (deletedFiles.length > 0) {
console.log(
'Files successfully deleted from file system:',
deletedFiles
)
}
console.log(await FileSystem.readDirectoryAsync(dirUri))
return true
} catch (error) {
console.log('Error deleting file from file system:', error)
return null
}
},
deleteImageFromCloudinary: async (public_id) => {
try {
const response = await api.get(
`/image/delete-image/?public_id=${public_id}`
)
if (response.data.result === 'ok') {
console.log('Image deleted successfully')
}
const { imageError } = get()
set({
imageError: {
...imageError,
deleteError: null,
},
})
return response.data
} catch (error) {
console.log(
'Error:',
error.response ? error.response.data : error.message
)
const { imageError } = get()
set({
imageError: {
...imageError,
deleteError: error?.message || error?.response.data,
},
})
return null
}
},
}))
export default useImageStore
< /code>
Thanks in advance!
Подробнее здесь: https://stackoverflow.com/questions/797 ... external-i
Мобильная версия