Updating a React Native/Expo image file does not update the visualization of this image everywhere in the app

I’ve had an interesting problem when saving and updating images in a React Native application built with Expo.

I’m building an app that has contacts and images (that are either taken from the phone contact entry or picked from the gallery).

The issue was that editing the image at one place and saving it, would not update the contact image in the contacts list. When updating the image, I was updating the image file and overriding it in the filesystem.

After saving it, and going to the previous screen, the old image was still there. Only after refreshing the application it was replaced.

Since I was reusing the file name, the prop in the contact card was not modified (the file path was the same), so the component didn’t know it had to re-render.

To solve that, I decided to update my helper function to add a timestamp to the filename. This way the file path would change, forcing all the components with the image to re-render.

export async function persistCachedFile ( cachedFile: string, permanentFolder: string, fileId: string ) {
    const permanentDirectoryPath = `${ FileSystem.documentDirectory }${ permanentFolder }/`
    const uniqueFilePath = `${ permanentDirectoryPath }${ fileId }-${ Date.now() }`;

    await ensureDirExists( permanentDirectoryPath );

    await FileSystem.copyAsync( {
        from: cachedFile,
        to: uniqueFilePath
    } );

    return uniqueFilePath;
}

The downside here is, that the old files are forever going to stay in the app directory. To avoid that, we need to add a cleanup function. I came up with something the following function that runs each time we copy the file.

export async function cleanupOldFilesAsync ( folder: string, fileId: string ) {
    // Finbd all files that have the imageId in their file name (and delete then):
    const directoryFiles = await FileSystem.readDirectoryAsync( folder );
    const previousImages = directoryFiles.filter( file => {

        if ( file.includes( fileId ) ) {
            return true;
        }
        return false;
    } );

    // Delete previous images.
    if ( previousImages.length ) {
        previousImages.forEach( previousImage => {
            // We don't await, because removing the files is not critical
            deleteAsync( `${ folder }${ previousImage }` );
        } )
    }
}

Now call the cleanupOldFiles from persistCachedFile (before we store the updated file) and voilà : )

The end result is:

import {
	deleteAsync,
	getInfoAsync,
	makeDirectoryAsync,
	readDirectoryAsync,
	copyAsync,
	documentDirectory
} from 'expo-file-system';

export async function ensureDirExists ( directory: string ) {
	const dirInfo = await getInfoAsync( directory );
	if ( !dirInfo.exists ) {
		await makeDirectoryAsync( directory, { intermediates: true } );
	}
}


export async function cleanupOldFilesAsync ( folder: string, fileId: string ) {
	// Finbd all files that have the imageId in their file name (and delete then):
	const directoryFiles = await FileSystem.readDirectoryAsync( folder );
	const previousImages = directoryFiles.filter( file => {

		if ( file.includes( fileId ) ) {
			return true;
		}
		return false;
	} );

	// Delete previous images.
	if ( previousImages.length ) {
		previousImages.forEach( previousImage => {
			// We don't await, because removing the files is not critical
			deleteAsync( `${ folder }${ previousImage }` );
		} )
	}
}

export async function persistCachedFile ( cachedFile: string, permanentFolder: string, fileId: string ) {
    const permanentDirectoryPath = `${ FileSystem.documentDirectory }${ permanentFolder }/`
    const filePath = `${ permanentDirectoryPath }${ fileId }`;
    const uniqueFilePath = `${ filePath }-${ Date.now() }`;

    cleanupOldFilesAsync( permanentDirectoryPath, fileId )
    await ensureDirExists( permanentDirectoryPath );

    await FileSystem.copyAsync( {
        from: cachedFile,
        to: uniqueFilePath
    } );

    return uniqueFilePath;
}

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.