The very first decision on a new React Native project — Expo's managed workflow with a custom development client, or the bare React Native workflow with its own native iOS and Android projects checked into the repository — is also one of the most expensive to reverse late. Choosing wrong doesn't fail loudly on day one; it fails quietly, months in, when a team discovers they need a native SDK Expo doesn't wrap, or that they spent weeks maintaining Xcode and Android Studio project files a managed workflow would have handled for them.
Expo today is not the restrictive, JavaScript-only sandbox it used to have a reputation for — a development client lets an Expo-managed app include arbitrary native modules and config plugins, and EAS Build compiles a real native binary from it, so the practical gap between "Expo" and "bare React Native" is narrower than many teams assume going in. This exercise asks you to build the actual decision logic a team should run through before choosing, rather than defaulting to whichever workflow a tutorial happened to use.
Problem Statement
You are advising three different teams starting new React Native projects, and each has described their situation. Team A is a two-person team building an MVP fast, with no native code experience and no immediate need for anything beyond camera, location, and push notifications — all of which have well-supported Expo modules. Team B needs a specific Bluetooth Low Energy SDK from a hardware vendor that ships only as a native iOS/Android library with no existing Expo config plugin, and has an engineer on the team comfortable writing native bridging code. Team C is migrating an existing bare React Native app that already has substantial custom native code, and is evaluating whether adopting Expo's tooling (EAS Build, Expo Router) is worth the migration cost without giving up their existing native modules.
Your task is to implement a decision function that takes each team's project profile as structured input and returns a specific, justified recommendation — not just "Expo" or "bare," but which specific configuration (Expo managed with development client, Expo with a specific config plugin, or bare workflow with native modules) fits, and the concrete reason why. A correct implementation reflects that the modern Expo development client changes what used to be a hard binary choice into a spectrum of configurations.
Requirements
Implement a `recommendWorkflow` function that accepts a `ProjectProfile` describing: whether the team has native iOS/Android engineering experience, whether the project needs a native SDK with no existing Expo config plugin, whether the project is a migration of an existing bare app with custom native modules already in place, and how time-constrained the team is. The function must return a `WorkflowRecommendation` object containing a `workflow` field (one of `"expo-managed"`, `"expo-dev-client"`, or `"bare"`) and a `reason` field explaining the specific factor that drove the decision.
The decision logic must handle all three described teams correctly: Team A's profile (no native experience, no unsupported native SDK, time-constrained) should resolve to `"expo-managed"`; Team B's profile (has native experience, needs an unsupported native SDK) should resolve to `"expo-dev-client"`, reflecting that a development client can include custom native modules without abandoning Expo's tooling entirely; Team C's profile (existing bare app with custom native modules already in place) should resolve to `"bare"`, reflecting that a large existing native investment is a strong reason to stay on the bare workflow regardless of the other factors.
Starter Code
// workflow-recommender.starter.ts — complete this decision function.
// It currently compiles and runs, but always returns the same answer —
// that's the bug you need to fix using the TODOs below.
interface ProjectProfile {
hasNativeExperience: boolean;
needsUnsupportedNativeSDK: boolean;
isExistingBareAppWithNativeModules: boolean;
isTimeConstrained: boolean;
}
interface WorkflowRecommendation {
workflow: "expo-managed" | "expo-dev-client" | "bare";
reason: string;
}
function recommendWorkflow(profile: ProjectProfile): WorkflowRecommendation {
// TODO 1: if this is a migration of an existing bare app with custom
// native modules already in place, that existing investment should win
// regardless of the other factors — recommend "bare".
// TODO 2: if the project needs a native SDK with no Expo config plugin,
// recommend "expo-dev-client" when the team has native experience to
// maintain the custom native code a dev client build would include.
// TODO 3: otherwise (no unsupported SDK, no existing bare investment),
// recommend "expo-managed" — this is the default for the common case.
return { workflow: "expo-managed", reason: "placeholder — replace with real logic" };
}
const teamA: ProjectProfile = {
hasNativeExperience: false,
needsUnsupportedNativeSDK: false,
isExistingBareAppWithNativeModules: false,
isTimeConstrained: true,
};
console.log("Team A (starter):", recommendWorkflow(teamA));
The starter compiles and runs — it always returns `"expo-managed"` with a placeholder reason, regardless of the profile passed in, which is deliberately wrong for Team B and Team C. The three TODOs mark exactly where the real discriminating logic needs to go, in the order that matches the priority described in the Requirements section: an existing bare investment overrides everything else, an unsupported native SDK with native expertise on the team calls for a dev client, and the remaining case defaults to a fully managed workflow.
Hints & Common Pitfalls
The most common mistake is checking `needsUnsupportedNativeSDK` before checking `isExistingBareAppWithNativeModules`, which produces a wrong answer for Team C: a team migrating an app that already has substantial custom native modules almost certainly also "needs a native SDK with no Expo config plugin" in some form, since that's exactly the kind of code that made them bare in the first place — but the right recommendation for them is still "bare," not "expo-dev-client," because migrating an established native codebase into Expo's tooling is a much bigger cost than the dev-client path assumes for a project starting fresh.
A second, subtler pitfall is treating `hasNativeExperience` as the deciding factor for the unsupported-SDK case in isolation, without also considering `isTimeConstrained` — a heavily time-constrained team with some native experience may still be better served accepting an Expo config plugin workaround or a third-party library than committing to maintaining custom native bridging code under deadline pressure, which is a nuance worth reasoning about even though the three required test cases in this exercise don't force that branch to be handled specially.
A common mistake in this exercise is writing the three conditions as independent if-statements rather than an ordered if/else-if chain, which lets a profile matching more than one condition fall through to the wrong branch or, worse, silently match the last condition checked regardless of earlier, higher-priority conditions. The symptom is that Team C's profile — which can plausibly satisfy both "existing bare app" and "needs unsupported SDK" simultaneously — returns "expo-dev-client" instead of the correct "bare" recommendation. The root cause is treating each condition as independently sufficient rather than recognizing that "existing bare investment" is a higher-priority override that should short-circuit the rest of the function. The fix is an ordered chain — existing bare investment checked first and returned immediately, then the unsupported-SDK case, then the managed-workflow default — so a profile matching multiple conditions is resolved by priority, not by whichever branch happens to run last.
Reference Solution
// workflow-recommender.solution.ts — a complete, correctly ordered
// implementation of the decision function.
interface ProjectProfile {
hasNativeExperience: boolean;
needsUnsupportedNativeSDK: boolean;
isExistingBareAppWithNativeModules: boolean;
isTimeConstrained: boolean;
}
interface WorkflowRecommendation {
workflow: "expo-managed" | "expo-dev-client" | "bare";
reason: string;
}
function recommendWorkflow(profile: ProjectProfile): WorkflowRecommendation {
// Priority 1: an existing bare app with custom native modules already in
// place is a large sunk investment — migrating it into Expo's tooling
// costs more than it saves, regardless of the other factors.
if (profile.isExistingBareAppWithNativeModules) {
return {
workflow: "bare",
reason: "existing native modules already in place outweigh the cost of migrating to Expo tooling",
};
}
// Priority 2: a genuinely unsupported native SDK, with native expertise
// on the team to maintain the resulting custom native code, is exactly
// what a development client build exists for.
if (profile.needsUnsupportedNativeSDK && profile.hasNativeExperience) {
return {
workflow: "expo-dev-client",
reason: "an unsupported native SDK needs custom native code, and the team has the expertise to maintain it inside a dev client build",
};
}
// Priority 3 (default): no unsupported SDK and no existing bare
// investment — the fully managed workflow is the lowest-overhead fit.
return {
workflow: "expo-managed",
reason: "no unsupported native SDK and no existing native investment — the managed workflow minimizes tooling overhead",
};
}
const teamA: ProjectProfile = {
hasNativeExperience: false,
needsUnsupportedNativeSDK: false,
isExistingBareAppWithNativeModules: false,
isTimeConstrained: true,
};
const teamB: ProjectProfile = {
hasNativeExperience: true,
needsUnsupportedNativeSDK: true,
isExistingBareAppWithNativeModules: false,
isTimeConstrained: false,
};
const teamC: ProjectProfile = {
hasNativeExperience: true,
needsUnsupportedNativeSDK: true,
isExistingBareAppWithNativeModules: true,
isTimeConstrained: false,
};
console.log("Team A:", recommendWorkflow(teamA));
console.log("Team B:", recommendWorkflow(teamB));
console.log("Team C:", recommendWorkflow(teamC));
The ordering is the entire solution: checking `isExistingBareAppWithNativeModules` first, before the unsupported-SDK check, is exactly what makes Team C resolve correctly even though Team C's profile also satisfies the unsupported-SDK condition. If the two checks were reordered, Team C would incorrectly receive `"expo-dev-client"`, which is the exact pitfall described earlier — the function would be individually checking each condition correctly while still producing the wrong recommendation, because priority, not just correctness, was part of the actual requirement.
Notice also that the `reason` field is not a generic restatement of the workflow name — it names the specific factor from the profile that drove the decision, which is what makes the recommendation actually useful to a team deciding between options rather than a bare label they'd have to independently re-justify. A recommendation function that returns a workflow without a specific, checkable reason is not meaningfully more helpful than a coin flip with better production values.
Testing Your Solution
#!/bin/bash
# scaffold-commands.sh — the actual CLI commands each recommended workflow
# maps to, printed rather than executed, so this exercise stays runnable
# offline while still documenting the real setup step for each outcome.
set -e
echo "expo-managed -> npx create-expo-app my-app"
echo "expo-dev-client -> npx create-expo-app my-app --template; npx expo install expo-dev-client; npx expo run:ios (or run:android) to build the custom dev client"
echo "bare -> npx @react-native-community/cli init MyApp"
# A quick self-check: confirm all three scaffold strings were produced,
# the way a project's setup script might sanity-check its own output.
count=$(echo "expo-managed
expo-dev-client
bare" | wc -l)
if [ "$count" -eq 3 ]; then
echo "OK: all three workflow scaffold commands documented ($count)"
else
echo "FAIL: expected 3 scaffold entries, found $count"
exit 1
fi
This script deliberately does not invoke `npx create-expo-app` or `npx @react-native-community/cli init` directly, because those commands reach out to the network to scaffold a real project and download dependencies — appropriate on your own machine, not appropriate for an automated check that has to run the same way every time without a network dependency. What it does verify is that your recommendation function's three possible outputs each map to a real, correct scaffold command, which is the part of "testing your solution" that's actually about your understanding rather than about exercising npm's install pipeline.
A more complete test of the TypeScript solution itself would call `recommendWorkflow` with several profiles that deliberately combine conditions in edge-case ways — a profile with `isExistingBareAppWithNativeModules: true` and every other flag also true, or a profile with `needsUnsupportedNativeSDK: true` but `hasNativeExperience: false` — and assert the returned `workflow` field against the expected value for each, which is exactly the kind of table-driven test the reference solution's ordering was designed to survive.