My first reaction to the SDK 58 changelog was simply that it is long.
Removals, behavior changes, deprecations, each in its own section. By the time I reached the end, I could no longer say what the first section had contained. Understanding the notes is one task. Finding where they apply to your own project is another, and it is still waiting when you finish reading.
So this time I reversed the order. I pulled out only the items that can be searched for, turned them into a small script, and ran it against the project first. Judgment comes after the list exists.
Expo SDK 58 was released on October 15, 2026, according to the expo.dev changelog, and it moves React Native from 0.86 to 0.88. A project exported from Rork is an ordinary Expo project underneath, but as far as I could confirm, there is no announcement yet about when Rork itself will support SDK 58. Everything below is about upgrading an exported repository on your own.
Sort the items into three bins first
Listing the changelog as it stands does not make a checklist. I sorted by what you must do about each one.
- REMOVED: the build or the type check stops if it remains. Highest priority
- BEHAVIOR: it still runs, but the result differs. A human has to decide if that matches intent
- DEPRECATED: it works today. Fix it next time, not on upgrade day
From the changelog, these are the items I made searchable.
| Bin | Target | Replacement |
|---|---|---|
| REMOVED | InteractionManager, the root Touchable export, NativeMethods types, the Modal animated prop | Pressable, instance types, animationType, and so on |
| REMOVED | Deep imports from react-native/Libraries/* (Strict TypeScript API is now the default) | Imports from the public API |
| REMOVED | expo-sqlite syncLibSQL and libSQLOptions | libSQL support itself is removed |
| BEHAVIOR | File.write() becomes async (expo-file-system) | await it, or use writeSync() |
| BEHAVIOR | expo-notifications shows foreground notifications by default | Confirm it is what you want |
| BEHAVIOR | NODE_ENV=test expo start now loads .env.development | Rethink where test env files live |
| DEPRECATED | File.md5, ImageBackground, withLayoutContext | File.digest(), View plus Image, integrateWithRouter |
The changelog also says some deprecated StatusBar props are removed, but I could not pin down which ones. The script includes networkActivityIndicatorVisible purely as an example, and you should check the official list to see whether it applies.
The full audit script
It runs on Node.js alone. SDK 58 expects Node 22.13 or later on the 22 line, 24.3 or later on the 24 line, or Node 26 and above; the odd-numbered 23 and 25 are not supported. There are no dependencies.
// audit-sdk58.mjs — SDK 58 へ上げる前に、プロジェクト内の「消える・変わる」API を一覧にする
// 使い方: node audit-sdk58.mjs [プロジェクトのパス]
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
import { join, relative, extname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const SELF = fileURLToPath(import.meta.url); // 自分自身は検査対象から外す
const root = process.argv[2] ?? '.';
const SKIP = new Set(['node_modules', '.git', 'ios', 'android', '.expo', 'dist']);
const EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs']);
// level: REMOVED=消える/BEHAVIOR=挙動が変わる/DEPRECATED=非推奨(今は動く)
const RULES = [
{ id: 'rn-interactionmanager', level: 'REMOVED', re: /\bInteractionManager\b/, why: 'RN から削除。requestAnimationFrame か、描画後に走らせたい処理を useEffect 側へ' },
{ id: 'rn-touchable-root', level: 'REMOVED', re: /import\s*\{[^}]*\bTouchable\b[^}]*\}\s*from\s*['"]react-native['"]/, why: 'Touchable のルート export が削除。Pressable へ' },
{ id: 'rn-nativemethods', level: 'REMOVED', re: /\bNativeMethods\b/, why: 'NativeMethods 型が削除。ViewInstance など instance 型へ' },
{ id: 'rn-modal-animated', level: 'REMOVED', re: /<Modal\b[^>]*\banimated\b/, why: 'Modal の animated prop が削除。animationType を使う' },
{ id: 'rn-deep-import', level: 'REMOVED', re: /from\s*['"]react-native\/Libraries\//, why: 'Strict TypeScript API が既定。Libraries/* の deep import は型エラー' },
{ id: 'rn-statusbar-ios-props', level: 'REMOVED', re: /<StatusBar\b[^>]*\bnetworkActivityIndicatorVisible\b/, why: 'StatusBar の非推奨 prop の一部が削除対象。この prop が該当するかは公式の変更履歴で確認(ここは例)' },
{ id: 'fs-file-write', level: 'BEHAVIOR', re: /\.write\(/, why: 'expo-file-system の File.write() は非同期に。同期のままなら writeSync()(File 以外の .write も拾うので目視)' },
{ id: 'fs-md5', level: 'DEPRECATED', re: /\.md5\b/, why: 'File.md5 は非推奨。File.digest() へ' },
{ id: 'sqlite-libsql', level: 'REMOVED', re: /\b(syncLibSQL|libSQLOptions|useLibSQL)\b/, why: 'expo-sqlite の libSQL 対応が削除' },
{ id: 'rn-imagebackground', level: 'DEPRECATED', re: /\bImageBackground\b/, why: '非推奨。View + 絶対配置の Image へ' },
{ id: 'router-withlayoutcontext', level: 'DEPRECATED', re: /\bwithLayoutContext\b/, why: '非推奨。integrateWithRouter へ' },
{ id: 'notif-foreground', level: 'BEHAVIOR', re: /from\s*['"]expo-notifications['"]/, why: 'フォアグラウンド通知が既定で表示される。意図した挙動か確認' },
{ id: 'nodeenv-test', level: 'BEHAVIOR', re: /NODE_ENV=test/, why: 'NODE_ENV=test で expo start すると .env.development を読む' },
];
function* walk(dir) {
for (const name of readdirSync(dir)) {
if (SKIP.has(name)) continue;
const p = join(dir, name);
const st = statSync(p);
if (resolve(p) === SELF) continue;
if (st.isDirectory()) yield* walk(p);
else if (EXT.has(extname(name)) || name === 'package.json') yield p;
}
}
const hits = [];
for (const file of walk(root)) {
const lines = readFileSync(file, 'utf8').split('\n');
lines.forEach((line, i) => {
for (const r of RULES) if (r.re.test(line)) hits.push({ r, file: relative(root, file), line: i + 1, text: line.trim() });
});
}
// ネイティブ側の確認(CNG でない場合に効く)
const notes = [];
if (existsSync(join(root, 'ios'))) notes.push('ios/ があります。CNG なら削除して再生成、手書き管理なら AppDelegate のシーン対応(SceneDelegate.swift)を確認');
if (existsSync(join(root, 'android'))) notes.push('android/ があります。R8 が release で既定オンになるため keep ルールを確認');
const order = { REMOVED: 0, BEHAVIOR: 1, DEPRECATED: 2 };
hits.sort((a, b) => order[a.r.level] - order[b.r.level] || a.file.localeCompare(b.file));
for (const h of hits) console.log(`[${h.r.level}] ${h.file}:${h.line} ${h.r.id}\n ${h.text}\n → ${h.r.why}`);
const c = (l) => hits.filter((h) => h.r.level === l).length;
console.log(`\n集計: REMOVED ${c('REMOVED')} / BEHAVIOR ${c('BEHAVIOR')} / DEPRECATED ${c('DEPRECATED')}`);
notes.forEach((n) => console.log('NOTE: ' + n));
process.exit(c('REMOVED') > 0 ? 1 : 0);A few notes on the design choices follow.
- The rules are an array, and each
whynames the replacement. The output tells you the next action as you read it, and adding a rule is one line - The exit code depends on whether any REMOVED item exists. If you later put it in CI, "stop unless REMOVED is zero" works as is
- It ends with a note about
ios/andandroid/. Native code cannot be seen by a text search, so the script only says "this needs a separate check"