I have an object with nested File
instances in various locations. I would like to recursively go through the object, check if an object is an instanceof File
, use a promise to create a data url from the instance, and resolve the promise only when all of the promises have been resolved.
I have an existing functions that returns a Promise and resolves when the data URL from the file is ready.
export const parsePhoto = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
try {
reader.readAsDataURL(file);
reader.onloadend = () => {
return resolve(reader.result);
}
} catch(e) {
console.warn('Could not upload photo', e.target);
}
})
}
I have a function to recursively look for a File
in the object.
export const convertPhotosToBase64 = (values) => {
if (!values) return values;
const converted = Object.keys(values).reduce((acc, key) => {
if (values[key] instanceof File) {
// Do something here
acc[key] = parsePhoto(values[key]);
}
if (isArray(values[key])) {
acc[key] = values[key].map(value => {
if (typeof value === 'object' && !isArray(value)) {
return convertPhotosToBase64(value);
}
return value;
})
}
// Recurse if object
if (typeof values[key] === 'object' && !isArray(values[key])) {
acc[key] = convertPhotosToBase64(values[key]);
}
return acc;
}, values);
return converted;
}
I want to keep the existing structure of the object passed (values
) and only replace the File instances with the base64 string.
I'm also aware of Promise.all
but unsure how to use it in this context.
How can I return convertPhotosToBase64
as a promise that resolves when all of the files have been converted to base64 strings?
Let's first simplify your function a bit, to reduce the duplication of all those conditions:
export function convertPhotosToBase64(value) {
if (typeof value !== 'object') return value;
if (value instanceof File) return parsePhoto(value);
if (isArray(value)) return value.map(convertPhotosToBase64);
return Object.keys(value).reduce((acc, key) => {
acc[key] = convertPhotosToBase64(value[key]);
return acc;
}, {});
}
Now, parsePhoto
is asynchronous and returns a promise. This means that the whole convertPhotosToBase64
will need to become asynchronous and always return a promise. Given the four clearly distinct cases, that's actually simpler than it sounds:
export function convertPhotosToBase64(value) {
// wrap value
if (typeof value !== 'object') return Promise.resolve(value);
// already a promise
if (value instanceof File) return parsePhoto(value);
// map creates all the promises in parallel, use `Promise.all` to await them
if (isArray(value)) return Promise.all(value.map(convertPhotosToBase64));
// chain one after the other
return Object.keys(value).reduce((accP, key) =>
accP.then(acc =>
convertPhotosToBase64(value[key]).then(res => {
acc[key] = res;
return acc;
})
)
, Promise.resolve({}));
}
If you are ok with doing everything in parallel (not only the arrays), you can also simplify the last case to
return Object.keys(value).reduce((accP, key) =>
Promise.all([accP, convertPhotosToBase64(value[key])]).then([acc, res] => {
acc[key] = res;
return acc;
})
, Promise.resolve({}));
or maybe better
const keys = Object.keys(value);
return Promise.all(keys.map(key => convertPhotosToBase64(value[key])).then(results => {
const acc = {};
for (const [key, i] of keys.entries())
acc[key] = results[i];
return acc;
});