// BAD - causes layout shiftconst [customData, setCustomData] = useState([]);
useEffect(() => {
loadCustomData().then(setCustomData); // Second render!
}, []);
// GOOD - single fetch, no shiftasyncfunctionfetchAllData() {
const [dataA, dataB] = awaitPromise.all([
fetchDataA(),
fetchDataB(),
]);
returncombineData(dataA, dataB);
}
Non-Blocking Operations (Prevent UI Freeze)
Root cause of "tiny delay": Sync operations (execSync, statSync, readdirSync) block the event loop during revalidation, freezing the UI even with cached data displayed.
// BAD - blocks event loop, UI freezes during revalidationimport { execSync } from"child_process";
import { statSync, readdirSync, copyFileSync } from"fs";
functionfetchData() {
copyFileSync(src, dest); // Blocks!const result = execSync("sqlite3 query"); // Blocks!const entries = readdirSync(dir); // Blocks!for (const entry of entries) {
statSync(join(dir, entry)); // Blocks N times!
}
}
// GOOD - fully async, UI renders cached data while refreshingimport { exec } from"child_process";
import { promisify } from"util";
import { stat, readdir, copyFile, access } from"fs/promises";
const execAsync = promisify(exec);
asyncfunctionfetchData() {
awaitcopyFile(src, dest); // Non-blockingconst { stdout } = awaitexecAsync("sqlite3..."); // Non-blocking// Use withFileTypes to avoid extra stat callsconst entries = awaitreaddir(dir, { withFileTypes: true });
const results = entries
.filter(e => e.isDirectory()) // No stat needed!
.map(e => ({ path: join(dir, e.name), name: e.name }));
}
Key optimizations:
Replace execSync with promisify(exec) for shell commands
Worker runs silently on interval, user never sees it
Both commands share the same Cache (scoped to extension, not command)
View command reads synchronously from pre-warmed cache
Use 15m to 1h intervals to avoid battery/rate-limit issues
Large Datasets: useSQL over JSON Cache
For >1,000 items, use SQLite instead of JSON cache for instant filtering:
// BAD - loads entire 10MB JSON into memory to filterconst allProjects = JSON.parse(cache.get("projects"));
const filtered = allProjects.filter(p => p.name.includes(query));
// GOOD - SQLite queries only matching rowsimport { useSQL } from"@raycast/utils";
const { data } = useSQL(dbPath, `SELECT * FROM projects WHERE name LIKE ?`, [`%${query}%`]);
Optimistic UI (Instant Actions)
For write operations, update UI immediately before API confirms:
import { runAppleScript } from"@raycast/utils";
// Get Chrome active tab URLconst url = awaitrunAppleScript(`
tell application "Google Chrome"
return URL of active tab of front window
end tell
`);
// Get Safari URLconst safariUrl = awaitrunAppleScript(`
tell application "Safari"
return URL of current tab of front window
end tell
`);
// Get frontmost appconst app = awaitrunAppleScript(`
tell application "System Events"
return name of first application process whose frontmost is true
end tell
`);
convert -size 512x512 xc:'#6366F1' -fill white -gravity center \
-font Helvetica-Bold -pointsize 280 -annotate +0+20 'M' \
assets/extension-icon.png
Development Workflow
# Install dependencies
npm install
# Start dev server (hot reload)
npm run dev
# Lint and fix
npm run fix-lint
# Build for production
npm run build
Raycast Deeplinks
Trigger Raycast commands programmatically via URL scheme:
# Reload all extensions
open "raycast://extensions/raycast/raycast/reload-extensions"# Open Raycast
open "raycast://focus"# Run any extension command
open "raycast://extensions/{author}/{extension}/{command}"
Auto-reload after build
Add to package.json scripts:
"build":"ray build --skip-types -e dist -o dist && open raycast://extensions/raycast/raycast/reload-extensions"
Or create a reload script:
#!/bin/bash
npm run build && open "raycast://extensions/raycast/raycast/reload-extensions"
Testing in Raycast
Run npm run dev (provides hot reload)
Open Raycast
Search for your command name
Press Enter to run
Without dev server running, use deeplink to reload after changes:
npm run build && open "raycast://extensions/raycast/raycast/reload-extensions"