I was scrolling through ios/Podfile in one of my wallpaper app repositories when I noticed three lines at the bottom that I had clearly typed myself. I have no memory of adding them.
I read the Expo SDK 57 release notes right afterwards. The headline is the React Native 0.86 bump, but the item that actually changed my week was a single bullet buried under "other highlights": expo prebuild now clears the native directories before regenerating them.
Those three forgotten lines suddenly needed a destination.
The line that matters most in SDK 57
Read the Expo SDK 57 changelog and the framing is honest: this is a deliberately small release. React Native moves from 0.85 to 0.86, and React stays at 19.2, same as SDK 56. The Expo team says they wanted this to be the easiest upgrade you have ever done.
That said, there are 601 commits touching 1,552 files between 0.85.0 and 0.86.0. Even with no breaking changes intended, pulling those in should be a decision you make on purpose — which is exactly why it landed in a separate SDK rather than a patch.
| Change | How much it affects your day |
|---|---|
| React Native 0.85 → 0.86 (React stays at 19.2) | Android edge-to-edge fixes plus rendering and layout improvements. Good for visual polish |
expo prebuild clears and regenerates android / ios by default | Top priority if you have touched native code. --no-clean restores the old behavior |
expo@57.0.9 resolves the Hermes V1 memory regression | Effectively mandatory if your app imports Reanimated or Worklets |
| Slower startup in development (unresolved) | Production builds are unaffected. It only shows up while you work |
expo-image gains writeToCacheAsync / readFromCacheAsync | Useful when you want to seed the image cache ahead of time |
Do not skim past the third row. The initial SDK 57 release inherited a Hermes V1 regression from SDK 56 that could drastically increase memory usage in apps importing react-native-worklets or react-native-reanimated. expo@57.0.9, published on August 13th, pulls in React Native 0.86.2 and resolves it.
Apps generated by Rork almost always touch the Reanimated family somewhere in their navigation or gesture code. Deciding up front that "if we upgrade, we go to 57.0.9 or later" removed a whole category of confusion for me.
Look at what disappears before you upgrade
I read the --clean default less as a behavior change and more as an honest restatement of a promise that was always there. Continuous Native Generation rests on the idea that your native directories are output, not source. The moment you hand edit them, that promise is already broken.
The painful time to discover it is right after an upgrade. So look first.
Here is the audit I run on a scratch branch: keep a copy of the current directories, regenerate, and read only the delta.
# 1. Work on a throwaway branch
git switch -c chore/sdk57-prebuild-audit
# 2. Snapshot the current native dirs (works even if they are gitignored)
cp -R ios ../ios-before-sdk57
cp -R android ../android-before-sdk57
# 3. Regenerate on your CURRENT SDK, with the new default behavior
npx expo prebuild --clean
# 4. Read only the delta — what survives is your own fingerprint
diff -ru ../ios-before-sdk57/Podfile ios/Podfile
diff -rq ../ios-before-sdk57 ios | grep -v "Pods\|build\|\.xcworkspace"
diff -ru ../android-before-sdk57/app/src/main/AndroidManifest.xml \
android/app/src/main/AndroidManifest.xmlI filter out Pods and build because both are byproducts of dependency resolution and compilation. Leave them in and the diff runs into the thousands of lines, which buries the three that matter.
Sort whatever comes out into three buckets:
- Edits you made by hand — the ones to migrate. This is the real work
- Anything autolinking added — regeneration puts it back identically. Leave it alone
- Template updates from Expo — the newer version wins after the upgrade, and that is the point
In my case bucket 1 was those three Podfile lines plus a photo library usage description I had typed straight into Info.plist. Both were "I just need this to build right now" edits that should have been config plugins from the start.
Move hand edits into a config plugin
Migrating means you stop writing into files that are designed to vanish, and instead describe the change so it reappears on every regeneration. I started with a single plugins/with-native-tweaks.js at the project root.
// plugins/with-native-tweaks.js
const fs = require('fs');
const path = require('path');
const {
withInfoPlist,
withAndroidManifest,
withDangerousMod,
AndroidConfig,
} = require('expo/config-plugins');
// (1) Declare the Info.plist usage string
const withPhotoLibraryUsage = (config, { message }) =>
withInfoPlist(config, (config) => {
config.modResults.NSPhotoLibraryAddUsageDescription = message;
return config;
});
// (2) Declare an AndroidManifest attribute
const withManifestAttribute = (config, { name, value }) =>
withAndroidManifest(config, (config) => {
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
app.$[name] = value;
return config;
});
// (3) Podfile additions are the one case where you must touch the file
const withPodfileSnippet = (config, { snippet }) =>
withDangerousMod(config, [
'ios',
async (config) => {
const podfile = path.join(config.modRequest.platformProjectRoot, 'Podfile');
const contents = fs.readFileSync(podfile, 'utf8');
// Guard against double-appending. prebuild runs many times, so be idempotent
if (contents.includes(snippet.trim())) {
return config;
}
fs.writeFileSync(podfile, `${contents}\n${snippet}\n`, 'utf8');
return config;
},
]);
module.exports = (config, props = {}) => {
config = withPhotoLibraryUsage(config, {
message: props.photoLibraryMessage ?? 'Used to export saved wallpapers to your photo library.',
});
config = withManifestAttribute(config, {
name: 'android:largeHeap',
value: 'true',
});
config = withPodfileSnippet(config, {
snippet: props.podfileSnippet ?? '',
});
return config;
};Then wire it up in app.config.ts:
export default {
name: 'Wallpaper',
slug: 'wallpaper',
plugins: [
[
'./plugins/with-native-tweaks',
{
photoLibraryMessage: 'Used to export saved wallpapers to your photo library.',
podfileSnippet: "post_install do |installer|\n # project specific post-processing\nend",
},
],
],
};The part that surprised me while writing this was how much the idempotency guard in withDangerousMod actually earns its keep. On paper, since prebuild now wipes first, you always append once to a fresh Podfile. In practice, once you mix in a --no-clean run, or your local flow diverges from what EAS Build does, the same lines can land twice. Any plugin that appends rather than assigns should start by checking whether its work is already done.
withInfoPlist and withAndroidManifest assign, so they never have this problem. Treat withDangerousMod as the last resort and express everything you can declaratively — that turned out to be the safer default, not just the tidier one.
When you are done, run npx expo prebuild --clean once more and diff against your snapshot. If your own fingerprints are gone and only autolinking and template updates remain, the migration is finished.
The order to run the upgrade in
Only after the audit and the migration do I touch the SDK itself. Keeping this order made every failure much easier to attribute.
- Align dependencies —
npx expo install expo@^57.0.0 --fix - Verify the version with your own eyes — run
npm ls expoand confirm you are on57.0.9or later. Stay on the initial release and you inherit the memory regression above - Run the health check —
npx expo-doctor@latest - Delete
androidandiosif they were generated for an older SDK — there is no reason to keep them; the next build regenerates them - Regenerate —
npx expo prebuild, and confirm your config plugin took effect - Rebuild your development build — if you use
expo-dev-client, an old dev build cannot load an SDK 57 bundle - Check on a real device — comparing memory behavior on a Reanimated-heavy screen before and after is worth the ten minutes
Step 3 is the one people skip. Please do not. expo-doctor tells you about version mismatches before a device build fails and makes you guess.
One more thing worth remembering: the slower development startup is still unresolved as of writing. It is documented as not affecting production. If your dev loop feels heavier after the upgrade, check the known regression before you start suspecting your own code.
When --no-clean is the right call
There is an escape hatch. npx expo prebuild --no-clean applies changes to the existing folders, exactly as before.
I decided not to make it part of my normal workflow, and the reason comes from my own setup.
The wallpaper apps ship as several separate apps from one codebase, with app.config.ts and EAS build profiles swapping the name, icon, and bundle identifier. In that arrangement, holding on to a single ios/ directory does not mean anything — you cannot say which app it belongs to. I wrote up how that configuration is put together in the white-label setup with app.config.ts and EAS.
Where --no-clean genuinely fits is the middle of a migration, on a day when a build has to ship. If the upgrade arrives while hand edits are still sitting in your native directories, it buys you that day. Then you go back and move them into a config plugin. The order changed; the work did not.
Projects generated by Rork give you the code, which means the temptation to fix things by hand is always there. I have given in to it plenty of times, and I do not think that is a failing on its own. What helps is deciding, on the same day you make the edit, whether the file you just touched is source or output.
The step you can take today is not the upgrade itself — it is capturing one prebuild --clean diff. It takes about five minutes, and if nothing shows up, SDK 57 really will be the easiest upgrade you have made.
Thank you for reading. I am still partway through this myself, with the Android side of the diff waiting for me tomorrow.