HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
5 Topics
Chapter•55 Minutes

Modern React Setup, Modules & Tooling

Understand project structure, JavaScript modules, package management, development servers, production builds, environment variables, linting, formatting, and TypeScript integration.

Project Structure & Build Pipeline

A production React application usually contains source modules, static assets, tests, configuration, and a build pipeline. The exact folders vary by framework and organization, but the principles remain stable.

The browser does not necessarily receive your source tree as written. JSX, TypeScript, CSS modules, environment variables, and imported assets are transformed and bundled or otherwise processed by tooling.

Keep application code organized around understandable boundaries: UI components, features, shared utilities, data access, and tests. Avoid both extremes: one giant folder containing everything and a deeply nested hierarchy that makes imports harder to understand.

Typical React Build Flow
Feature-oriented structure
src/
  features/
    users/
      UserList.tsx
      userApi.ts
      userTypes.ts
      UserList.test.tsx
  components/
  routes/
  app/
  shared/
💡 Feature-oriented organization keeps UI, API logic, types, and tests close when they change together.
Key Takeaways
  • Tooling transforms source code into browser-consumable assets.
  • Choose folder boundaries around ownership and change.
  • Keep configuration separate from application behavior.

ES Modules, Imports & Dependency Boundaries

Modern React code is generally organized with ES modules. Named exports make APIs explicit, while default exports can be convenient for single-primary-component modules.

Avoid creating import cycles accidentally. A common healthy dependency direction is UI → feature/application layer → data utilities. If a low-level utility imports a feature component, the dependency graph can become difficult to reason about.

Path aliases can improve readability, but they should not hide architectural boundaries. An import such as @/features/users is useful when it communicates ownership clearly.

Named exports
export type User = {
  id: string;
  name: string;
};

export function formatUserName(user: User) {
  return user.name.trim();
}

// consumer
import { formatUserName, type User } from "./user";
💡 Named exports make the public surface of a module explicit.
Key Takeaways
  • Use module exports to define intentional APIs.
  • Avoid circular dependencies.
  • Aliases improve ergonomics but should not replace architectural discipline.

Environment Variables & Configuration

Environment variables are configuration inputs to a build or server process. A crucial security rule is that anything embedded into browser JavaScript should be considered public. API keys that require secrecy must stay on a trusted server.

Different build systems expose environment variables differently. Do not assume that a variable available to a Node process is automatically available in browser code. Read your framework's conventions and distinguish build-time values from runtime server secrets.

For client configuration, prefer a small typed configuration module rather than reading environment variables throughout the application.

Browser code is public

Never put database passwords, private API keys, signing secrets, or credentials into a client-exposed environment variable.

Typed client configuration
const config = {
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
} as const;

export default config;
💡 The exact environment-variable prefix is tooling-specific. The important idea is to centralize client configuration and never expose secrets.
Key Takeaways
  • Client-exposed environment values are not secrets.
  • Keep configuration access centralized.
  • Know whether a value is evaluated at build time or server runtime.

TypeScript with React

TypeScript adds static checking to React applications. Type component props, state models, event handlers, API responses, and reusable hooks. Avoid using any as a shortcut because it removes the compiler's ability to detect mismatches.

Prefer discriminated unions for UI states that have mutually exclusive shapes. This models loading, success, and error states more accurately than a collection of optional fields.

Use unknown for untrusted values and narrow them before use. Runtime validation is still required for data arriving from APIs because TypeScript types disappear at runtime.

Discriminated async state
type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

function Message({ state }: { state: RequestState<string> }) {
  if (state.status === "loading") return <p>Loading...</p>;
  if (state.status === "error") return <p>{state.message}</p>;
  if (state.status === "success") return <p>{state.data}</p>;
  return <p>Start</p>;
}
💡 The status field lets TypeScript narrow the data shape safely.
Key Takeaways
  • Types should model real UI states.
  • Do not confuse compile-time types with runtime validation.
  • Prefer precise types over any.

Linting, Formatting & Developer Workflow

Linting catches classes of code problems while formatting standardizes style. Tests protect behavior. These tools complement one another rather than replacing code review.

A useful workflow is to run fast checks locally and repeat authoritative checks in CI. Type checking, linting, unit tests, integration tests, and production builds should fail the pipeline when appropriate.

Do not disable a lint rule merely because it is inconvenient. Understand why it exists, then either change the code or document a narrowly scoped exception when the rule genuinely does not fit the case.

Key Takeaways
  • Formatting reduces style noise in reviews.
  • Lint rules often encode correctness as well as style.
  • CI should reproduce the checks that matter for production.