DEV Community

Ansh Sheladiya
Ansh Sheladiya

Posted on

Cross-Platform Development with React: Build for Web, iOS, and Android

Building separate applications for the web, iOS, and Android can quickly become expensive. Each platform introduces its own development requirements, testing challenges, and maintenance costs, making it difficult for teams to deliver consistent experiences without duplicating significant amounts of work.

React offers a practical way to approach this problem through reusable components, declarative UI development, and a shared JavaScript ecosystem. By combining React for the web with React Native for mobile applications, teams can reuse business logic while adapting interfaces to the capabilities of each platform.

The key is understanding what should be shared and what should remain platform-specific. Cross-platform development is not about forcing every screen into identical code; it is about reducing duplication without compromising usability or maintainability.

Building a Shared Architecture with React and React Native

React and React Native share a component-based programming model, but they target different rendering environments. React typically renders interfaces using HTML elements in a browser, while React Native uses native platform components such as View, Text, and Pressable. React Native Web can also help teams reuse React Native components in web applications when the project architecture supports it.

A maintainable cross-platform application usually separates business logic, data access, validation, and domain models from presentation. For example, authentication rules, product filtering, currency formatting, and API communication can live in shared JavaScript modules. Web and mobile interfaces can then consume those modules while maintaining platform-appropriate navigation, accessibility, and interactions.

Consider an application that displays products from an API. The product service can fetch and normalize data once, while the web interface and mobile interface independently decide how to display loading indicators, errors, and product cards. This separation makes the codebase easier to test and allows platform teams to evolve their interfaces without duplicating core business rules.

The following runnable JavaScript example demonstrates this architecture without requiring a browser or React Native runtime. It simulates an API, validates product data, applies shared business logic, and renders platform-specific output as text in Node.js. The same service and domain functions could later be imported into actual React and React Native components.

const API_BASE_URL = "https://example.com/api";

// Simulate an API response so the example runs without credentials.
const mockProducts = [
  { id: 1, name: "Wireless Headphones", price: 79.99, stock: 12 },
  { id: 2, name: "Mechanical Keyboard", price: 129.5, stock: 0 },
  { id: 3, name: "USB-C Hub", price: 39.95, stock: 8 },
];

// Step 1: Define shared validation for data received from any API.
function validateProduct(product) {
  if (!product || typeof product !== "object") {
    throw new TypeError("Product must be an object");
  }

  if (!Number.isInteger(product.id) || product.id <= 0) {
    throw new TypeError("Product ID must be a positive integer");
  }

  if (typeof product.name !== "string" || !product.name.trim()) {
    throw new TypeError("Product name is required");
  }

  if (!Number.isFinite(product.price) || product.price < 0) {
    throw new TypeError("Product price must be a non-negative number");
  }

  if (!Number.isInteger(product.stock) || product.stock < 0) {
    throw new TypeError("Product stock must be a non-negative integer");
  }

  return { ...product, name: product.name.trim() };
}

// Step 2: Keep API access separate from UI components.
async function fetchProducts() {
  console.log("[API] Preparing product request...");
  console.log(`[API] Base URL: ${API_BASE_URL}`);

  // Replace this mock with fetch() when a real backend is available.
  await new Promise((resolve) => setTimeout(resolve, 100));
  const products = mockProducts.map(validateProduct);

  console.log(`[API] Received ${products.length} valid products.`);
  return products;
}

// Step 3: Share business rules across web and mobile applications.
function getAvailableProducts(products) {
  return products.filter((product) => product.stock > 0);
}

// Step 4: Share currency formatting across platforms.
function formatPrice(price, locale = "en-US", currency = "USD") {
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  }).format(price);
}

// Step 5: Build a platform-independent product view model.
function createProductViewModel(product) {
  return {
    id: product.id,
    title: product.name,
    formattedPrice: formatPrice(product.price),
    availability: product.stock > 0 ? "In stock" : "Out of stock",
    canPurchase: product.stock > 0,
  };
}

// Step 6: Render output differently for each platform.
// Real applications would use React DOM or React Native components.
function renderWebProduct(product) {
  const status = product.canPurchase ? "Buy now" : "Unavailable";
  return `<article><h2>${product.title}</h2>` +
    `<p>${product.formattedPrice}</p>` +
    `<p>${product.availability}</p><button>${status}</button></article>`;
}

function renderMobileProduct(product) {
  const action = product.canPurchase ? "[ Buy now ]" : "[ Unavailable ]";
  return `📱 ${product.title}\n` +
    `   Price: ${product.formattedPrice}\n` +
    `   Status: ${product.availability}\n` +
    `   Action: ${action}`;
}

// Step 7: Run the shared workflow and display platform-specific results.
async function main() {
  try {
    console.log("\n=== Cross-Platform Product Demo ===");

    const products = await fetchProducts();
    const availableProducts = getAvailableProducts(products);

    console.log(`\n[Business] Available products: ${availableProducts.length}`);

    const viewModels = products.map(createProductViewModel);

    console.log("\n=== Web Presentation ===");
    viewModels.forEach((product) => {
      console.log(renderWebProduct(product));
    });

    console.log("\n=== Mobile Presentation ===");
    viewModels.forEach((product) => {
      console.log(renderMobileProduct(product));
      console.log("---");
    });

    console.log("\n[Done] Shared business logic completed successfully.");
  } catch (error) {
    console.error("[Error] Product workflow failed:", error.message);
    process.exitCode = 1;
  }
}

// Step 8: Start the application when this file is executed.
main();
Enter fullscreen mode Exit fullscreen mode

Conclusion

Cross-platform development with React works best when teams treat code reuse as an architectural decision rather than a requirement to share everything. API services, validation, business rules, and data transformations are strong candidates for shared modules, while layouts, navigation, and platform-specific interactions may need separate implementations.

For production applications, organize shared logic into packages, use TypeScript to strengthen contracts, and test domain functions independently of the UI. Consider React Native Web when shared interface components are valuable, and introduce platform-specific files when native capabilities or user experience require them.

Start with one feature that has clear business rules and implement it across the platforms you support. Measure how much code is genuinely reusable, validate the experience on real devices, and refine the boundaries as the application grows. The goal is not maximum code sharing; it is delivering reliable experiences with less duplicated effort.

Top comments (0)