{"id":"l-rn-5-4","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Ship JavaScript fixes to Trailhead users with EAS Update, keep updates compatible with installed builds using channels and runtime versions, roll back safely, and settle into a sustainable release routine.","html":"<p>A user reports that Trailhead rejects “8,4” as a distance on her German phone. You fix the parser in five minutes. Without anything else, getting that fix to her means a new build, a submission and a review, then waiting for her to update. <strong>EAS Update</strong> shortens that to one command: it publishes new JavaScript and assets, and installed copies of Trailhead download them the next time they start.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">How an update reaches a phone</h2>\n<p>A release build contains two things: the native app (compiled Swift, Kotlin and C++, including every native module) and an <strong>embedded JavaScript bundle</strong>. An update replaces only the second part. When the app launches, <code dir=\"ltr\">expo-updates</code> checks the server for a newer bundle for its channel and runtime version. By default it starts immediately with the code it already has, downloads the new update in the background, and uses it on the <strong>next</strong> launch. Startup never waits on the network, which matters for an app opened at a trailhead with one bar of signal.</p>\n<p>Set it up once, then make new builds so they include the update configuration:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">eas</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> update:configure</span></span></code></pre></div>\n<p>This adds the update URL and a <code dir=\"ltr\">runtimeVersion</code> to your app config. Then give each build profile a channel in <code dir=\"ltr\">eas.json</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">eas.json</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"build\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"preview\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"distribution\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"internal\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"preview\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"production\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"autoIncrement\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"production\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Publishing an update is one command:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">eas</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> update</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --channel</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> production</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --message</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"Accept comma decimals in distance\"</span></span></code></pre></div>\n<p>Production builds pick it up on their next launch. Publish to <code dir=\"ltr\">preview</code> first, check it on your testers’ builds, then publish the same code to <code dir=\"ltr\">production</code>.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">What can and can’t go over the air</h2>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Ships with <code dir=\"ltr\">eas update</code></th>\n<th>Needs a new build and store release</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Components, screens, styles, logic</td>\n<td>Adding or upgrading a native module</td>\n</tr>\n<tr>\n<td>Images and fonts bundled with the JavaScript</td>\n<td>Permission descriptions and other native config</td>\n</tr>\n<tr>\n<td>Bug fixes and copy changes</td>\n<td>App icon, splash screen, bundle identifier</td>\n</tr>\n<tr>\n<td>New screens built from existing native modules</td>\n<td>Expo SDK upgrades</td>\n</tr>\n</tbody>\n</table></div><p>This is the JavaScript-versus-native distinction from the second lesson of the course, now with consequences. If a change touches native code or native configuration, an update can’t deliver it.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Runtime versions keep updates compatible</h2>\n<p>Imagine Trailhead 1.1 adds <code dir=\"ltr\">expo-camera</code>. Its JavaScript imports a native module that 1.0 builds don’t contain. If 1.0 users received that update, the app would crash when it touched the camera. The <strong>runtime version</strong> prevents that: every build and every update carries one, and an update only applies to builds with the same runtime version.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">app.json</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"expo\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"version\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"1.1.0\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"runtimeVersion\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"policy\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"appVersion\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>With the <code dir=\"ltr\">appVersion</code> policy, the runtime version is your app’s <code dir=\"ltr\">version</code>, so you raise <code dir=\"ltr\">version</code> whenever native code changes, and 1.0 and 1.1 builds stop sharing updates. The <code dir=\"ltr\">fingerprint</code> policy computes a hash of the native parts of your project instead, so the runtime version changes automatically when native code does. Either works; pick one and understand it.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Shipping native changes as an update</span></p>\n<p>The classic outage: a developer adds a native library, tests in a fresh development build where it works, and publishes an update without a new runtime version. Older builds receive JavaScript that calls a module they don’t have. Before every <code dir=\"ltr\">eas update</code>, ask one question: did anything native change since the build this channel serves? If yes, it’s a new build and a store release.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">When an update goes wrong</h2>\n<p>Updates reach many users quickly, which cuts both ways. If one misbehaves, roll back:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">eas</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> update:rollback</span></span></code></pre></div>\n<p>It walks you through returning the channel’s branch to a previous update or to the code embedded in the build. Users get the rollback on their next launch, the same way they got the update. That’s why every update deserves the same care as a release: run your tests, publish to <code dir=\"ltr\">preview</code>, open it on a real phone, then publish to <code dir=\"ltr\">production</code>. Both stores allow updating JavaScript this way for bug fixes and improvements; using it to change what the app fundamentally does, without review, is against their rules.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">A release routine that holds up</h2>\n<p>Here’s a rhythm that works for a small team:</p>\n<ul>\n<li><strong>JavaScript fixes</strong>: test, <code dir=\"ltr\">eas update</code> to preview, check, then to production. Same day.</li>\n<li><strong>Native changes or new features</strong>: raise <code dir=\"ltr\">version</code>, production build, TestFlight and internal track, review, release. Planned, every few weeks.</li>\n<li><strong>Every release</strong>: watch crash reports for the first day, and keep <code dir=\"ltr\">eas update:rollback</code> ready.</li>\n</ul>\n<h2 id=\"sec-6\" tabindex=\"-1\">Where you are now</h2>\n<p>You started with a single <code dir=\"ltr\">Text</code> in an Expo project. Trailhead now has lists that stay fast, tabs and stacks with typed params, a form that handles the keyboard, data in SQLite and a token in secure storage, GPS tracking, photos, reminders, tests, store builds and a way to fix bugs the same afternoon. More importantly, you know where React Native differs from the web and why: native views, two threads, no cascade, permissions that can be refused, and two kinds of change, JavaScript and native.</p>\n<p>Good next steps: add Reanimated and react-native-gesture-handler for richer interactions, TanStack Query for server data, and Maestro for a couple of end-to-end tests. Then ship something small to real users and learn from what they do with it.</p>\n","toc":[{"id":"sec-1","text":"How an update reaches a phone","level":2},{"id":"sec-2","text":"What can and can’t go over the air","level":2},{"id":"sec-3","text":"Runtime versions keep updates compatible","level":2},{"id":"sec-4","text":"When an update goes wrong","level":2},{"id":"sec-5","text":"A release routine that holds up","level":2},{"id":"sec-6","text":"Where you are now","level":2}],"takeaways":["EAS Update replaces your app’s JavaScript bundle and assets on installed builds, without a new binary or store review.","Only JavaScript and asset changes can ship over the air; new native modules, permission strings, icons and SDK upgrades need a new build.","A build’s channel decides which updates it receives, and its runtime version decides which updates are compatible with its native code.","By default an update downloads in the background and applies on the next launch.","When an update misbehaves, <code dir=\"ltr\">eas update:rollback</code> returns users to a previous update or to the code embedded in their build."],"further":[{"title":"EAS Update introduction","url":"https://docs.expo.dev/eas-update/introduction/"},{"title":"How EAS Update works","url":"https://docs.expo.dev/eas-update/how-it-works/"},{"title":"Runtime versions and updates","url":"https://docs.expo.dev/eas-update/runtime-versions/"},{"title":"Rollbacks","url":"https://docs.expo.dev/eas-update/rollbacks/"}],"quiz":[{"q":"<p>Which Trailhead change can reach users with <code dir=\"ltr\">eas update</code> alone?</p>","options":[{"text":"Adding <code dir=\"ltr\">expo-camera</code> to scan QR codes on trail markers.","why":"A new native module must be compiled into the binary, so it needs a new build and a store release."},{"text":"Fixing the comma bug in <code dir=\"ltr\">parseDistanceKm</code> and rewording an error message.","why":"Correct. Both are JavaScript changes, which is exactly what an update replaces."},{"text":"Changing the location permission description in app.json.","why":"Usage descriptions are compiled into the native configuration; they change only with a new build."},{"text":"Upgrading to the next Expo SDK.","why":"An SDK upgrade changes native code and the runtime version, so it always needs new builds."}],"answer":1},{"q":"<p>You publish an update to the production channel. A user opens Trailhead an hour later. With default settings, when do they see the fix?</p>","options":[{"text":"Immediately; the app blocks on the splash screen until the update downloads.","why":"By default the app launches with the code it already has, so a slow network never delays startup."},{"text":"Only after they reinstall the app from the store.","why":"Updates install without the store. That’s the whole point of EAS Update."},{"text":"On the next launch after this one; the update downloads in the background during this session.","why":"Correct. The app checks on launch, downloads in the background, and applies the update the next time it starts."}],"answer":2},{"q":"<p>Users on Trailhead 1.0 (built before you added <code dir=\"ltr\">expo-camera</code>) and 1.1 (built after) both use the production channel. You publish a JavaScript update that imports <code dir=\"ltr\">expo-camera</code>. What keeps 1.0 users from crashing?</p>","options":[{"text":"Runtime versions; 1.1 has a different runtime version, so updates built for it don’t apply to 1.0 builds.","why":"Correct. With the <code dir=\"ltr\">appVersion</code> or <code dir=\"ltr\">fingerprint</code> policy, native changes produce a new runtime version, and updates only reach compatible builds."},{"text":"Nothing; you must publish separate updates by hand and hope the right users get them.","why":"Runtime versions exist precisely so you don’t have to rely on hope."},{"text":"EAS Update installs the missing native module along with the JavaScript.","why":"Updates can’t add native code. That’s why compatibility checks matter."}],"answer":0}],"exercise":{"kind":"choice","multiple":true,"options":["copy-fix","reducer-bug","new-screen","add-sdk-module","icon","permission-text","new-image"],"correct":["copy-fix","reducer-bug","new-screen","new-image"],"title":"Update or new build?","prompt":"<p>Trailhead 1.2 is live with <code dir=\"ltr\">runtimeVersion</code> set to the <code dir=\"ltr\">appVersion</code> policy. The team’s list for this week is below. Select <strong>every</strong> change you can ship to existing 1.2 users with <code dir=\"ltr\">eas update</code> alone, without a new build or store review.</p>\n","hints":["Ask of each change: does it touch only JavaScript and bundled assets, or does it change the compiled native app?","Anything in app.json that the stores or the OS read (icons, permission strings) is native configuration.","Four changes can go over the air."],"explanation":"<p>Text, logic, new screens built from modules already in the binary, and images bundled with the JavaScript are all part of the update bundle, so they ship with <code dir=\"ltr\">eas update</code>. A new native module like <code dir=\"ltr\">expo-haptics</code> needs to be compiled in; the app icon and permission descriptions live in native configuration the OS reads from the installed binary. Those three need a new <code dir=\"ltr\">version</code>, a production build and a store release.</p>\n","checkLabels":{},"optionText":{"copy-fix":{"text":"Reword the empty state on the hike log to “No hikes yet. Log your first one!”","why":"Update. It’s a string in a component, part of the JavaScript bundle."},"reducer-bug":{"text":"Fix <code dir=\"ltr\">hikesReducer</code> so editing a hike’s date re-sorts the list.","why":"Update. Pure JavaScript logic, the ideal over-the-air fix."},"new-screen":{"text":"Add a yearly stats screen built only from View, Text and FlatList.","why":"Update. New screens are fine as long as every native module they use is already in the build."},"add-sdk-module":{"text":"Add <code dir=\"ltr\">expo-haptics</code> so the Save button vibrates gently.","why":"New build. A native module that isn’t in the installed binary can’t arrive in an update."},"icon":{"text":"Replace the app icon with the new logo.","why":"New build. The home-screen icon is part of the installed app package, not the JavaScript bundle."},"permission-text":{"text":"Clarify the location permission description in the <code dir=\"ltr\">expo-location</code> plugin options.","why":"New build. Config plugin options are written into native configuration when the app is built."},"new-image":{"text":"Swap the empty-state illustration for a new PNG imported with <code dir=\"ltr\">require()</code>.","why":"Update. Images bundled through the JavaScript bundler are assets that updates include."}}},"words":825,"readMinutes":4}