100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
React Native
50 minintermediate

Setting Up a Project: Expo vs Bare Workflow

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.

Analogy🏏Cricket
🏏 Think of it like cricket: A young batter's family choosing which academy to train at is choosing an entire pipeline, not just a coach for this month — a well-run academy handles equipment, fitness conditioning, match scheduling, and travel logistics as a package, letting the batter focus purely on technique, while an independent setup requires the family to arrange every one of those pieces themselves, with far more control over each one but far more to manage directly. Neither choice is wrong in general — a batter with serious individual needs, an unusual bowling action requiring specialist biomechanics work no standard academy offers, may be better served managing that piece independently even while the rest of the training stays standard — but the choice has to be made deliberately, weighing what the batter actually needs against what each pipeline actually provides, not picked by default because it's what a friend's academy happened to be. Just as an academy's managed pipeline trades some direct control for enormously reduced logistical overhead, Expo's managed workflow with EAS Build trades some direct native-project control for a build, update, and submission pipeline the team doesn't have to maintain by hand. Just as an independent setup gives a family full control at the cost of managing every piece themselves, the bare workflow gives a team full control over the native Xcode and Android Studio projects at the cost of maintaining that native tooling directly. The insight is that this decision should be driven by the project's actual, specific needs — assessed deliberately, the way a serious family assesses an academy's fit — not by which option happens to be the default in whatever tutorial a team started from.

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.

Analogy🏏Cricket
🏏 Think of it like cricket: A national selection committee doesn't ask one blanket question — "is this player good?" — and stop there; it asks a structured set of questions specific to the situation: which format is the squad being picked for, what conditions will the tour involve, does the squad already have cover for a given role, does this specific player address a genuine gap or merely duplicate a skill the squad already has in depth. Skipping that structured assessment and picking players by reputation alone produces a squad that looks strong on paper but has real gaps once it's actually tested by specific match conditions, the same failure mode as choosing a project's workflow by reputation — "Expo is for beginners," "bare is for serious apps" — rather than by an actual structured assessment of what the project genuinely needs. Just as a selection committee evaluates format, conditions, and existing squad gaps for each specific situation, a workflow decision has to evaluate a project's specific native SDK needs, team expertise, and existing codebase, not a generic reputation. Just as the same player can be the right pick for one format and the wrong pick for another, the same workflow — Expo managed, Expo with a config plugin, or bare — can be exactly right for one team's situation and wrong for a superficially similar one. The insight is that a good decision process asks the specific structured questions a situation actually calls for, rather than reaching for a generic rule of thumb that ignores what makes this particular case different.

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.

Analogy🏏Cricket
🏏 Think of it like cricket: A fast bowler's workload-management plan is never a single blanket rule like "bowl no more than four overs a day" applied identically to every player — it is a specific set of conditions checked in a specific order: has this bowler had a recent injury history, is this a must-win knockout match versus a dead rubber, is the bowler already carrying fatigue from the previous match, and only after weighing those specific factors together does the medical team issue a specific workload cap for that bowler on that day. A blanket rule ignoring those factors either overloads a bowler who genuinely needed more caution, or unnecessarily rests a bowler who was actually fine, because a single flat rule can't capture what a structured, ordered set of conditions can. Just as a workload plan checks specific conditions in a specific order before issuing a specific cap, this exercise's decision function has to check native-SDK needs, team experience, and existing native investment in a coherent order before issuing a specific workflow recommendation. Just as the same bowler gets a different workload cap on a different day depending on which conditions actually apply, the same team profile with one factor changed — swap "no unsupported SDK" for "needs an unsupported SDK" — has to produce a genuinely different recommendation, not the same default answer regardless of input. The insight is that a decision function's value comes from correctly discriminating between genuinely different situations, not from returning a plausible-sounding answer regardless of what was actually asked.

Starter Code

typescript
// 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.

Analogy🏏Cricket
🏏 Think of it like cricket: A team management group evaluating whether to recall an experienced but currently out-of-form batter has to check conditions in the right order — first, is there already a settled, in-form player occupying that exact role, because if there is, the experienced batter's credentials don't override an incumbent who's already delivering; only once that's ruled out does the conversation move to whether the returning player's experience genuinely suits the specific format and conditions coming up. A selector who checks form and reputation first, before checking whether the role is even open, ends up recommending a swap that disrupts a settled team for no real gain — the same ordering mistake as checking a project's native-SDK need before checking whether an existing native investment already settles the question. Just as ruling out "is the role already filled by someone delivering" has to come before evaluating a candidate's individual merits, ruling out "is there already a large native investment in place" has to come before evaluating whether a native SDK need alone should push the answer toward a dev client. The insight is that the order conditions are checked in is not a stylistic detail — checking them in the wrong order produces genuinely wrong recommendations even when every individual condition is evaluated correctly on its own.

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

typescript
// 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.

Analogy🏏Cricket
🏏 Think of it like cricket: A team's selection announcement that just names eleven players, with no stated reasoning, forces every commentator and fan to independently guess why each choice was made — was a batter picked for form, for the conditions, to cover an injury — and that guessing produces genuine disagreement even among people looking at the same eleven names. A selection committee that instead states, for each contested pick, the specific factor that drove it — "recalled for his record against left-arm spin on this specific type of pitch" — gives everyone the same concrete, checkable reasoning to evaluate, agree with, or challenge, rather than a bare name with the justification left implicit. Just as a stated, specific reason lets a selection decision be evaluated rather than merely accepted or guessed at, this function's `reason` field gives a specific, checkable justification rather than a bare workflow label. Just as "recalled for his record against left-arm spin" is meaningfully more useful than "recalled because he's good," "an unsupported native SDK needs custom native code the team can maintain" is meaningfully more useful than a workflow name with no stated cause. The insight is that a good recommendation is not just a correct conclusion — it's a correct conclusion paired with the specific reasoning that lets someone else verify it rather than simply trust it.

Testing Your Solution

bash
#!/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.

Analogy🏏Cricket
🏏 Think of it like cricket: Before a fast bowler's revamped action is trusted in a real match, it isn't tested with a handful of easy, comfortable deliveries — a proper assessment specifically throws deliberately awkward cases at it: bowling into a strong crosswind, bowling on a wearing pitch late in the day, bowling with an over already going badly, because those are exactly the conditions most likely to expose a flaw a few clean net sessions wouldn't reveal. A bowling coach who only ever tests the action in ideal, controlled conditions and calls it match-ready is setting the bowler up to discover the actual flaw in a real match, at the worst possible time. Just as an action is genuinely tested by deliberately awkward conditions rather than easy ones, this decision function is genuinely tested by profiles that deliberately combine conditions in edge-case ways, not just the three straightforward profiles from the Problem Statement. Just as a coach specifically seeks out the conditions most likely to expose a flaw, a thorough test specifically seeks out the profile combinations most likely to reveal a wrong-priority bug, like the ordering mistake this lesson's warning box describes. The insight is that confidence in a solution should come from deliberately trying to break it, not from watching it succeed on the easy cases it was obviously designed to handle.
Lesson 3 of 35
0% complete