Reputation: 3709
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?
Upvotes: 2
Views: 1078
Reputation: 664395
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;
});
Upvotes: 3
Reputation: 12652
Promise.all
does what you want it to. The easiest way to use it in this situation is by doing return Promise.all(converted)
at the bottom of your function, which will return a special promise that doesn't resolve until all the promises in the argument have resolved.
Upvotes: 0