Skip to content

React Native Mobile Foundation

Objective

Build React Native mobile application using reason-react-native with Apollo Client, shared types, and Relude functional patterns.

Background

Mobile application shares the same GraphQL API and types as the web app, demonstrating true full-stack type safety with ReasonML.

Tasks

  • Create packages/mobile structure
  • Set up Expo with ReasonML compilation
  • Configure reason-react-native bindings
  • Integrate Apollo Client bindings
  • Implement navigation with React Navigation
  • Create mobile UI components
  • Add mobile-specific features (biometric auth, push notifications)
  • Configure Metro bundler for Melange output
  • Set up development workflow

Package Structure

packages/mobile/
├── src/
│   ├── components/
│   │   ├── App.re
│   │   ├── Navigation.re
│   │   ├── TaskList.re
│   │   └── TaskItem.re
│   ├── screens/
│   │   ├── LoginScreen.re
│   │   ├── DashboardScreen.re
│   │   └── TaskScreen.re
│   ├── queries/
│   │   └── (shared with web via melange_shared)
│   ├── utils/
│   │   ├── Auth.re
│   │   └── Navigation.re
│   └── Index.re
├── lib/
│   └── dune
├── metro.config.js
├── app.json
├── package.json
└── dune-project

Component Example (Mobile + Relude)

(* src/screens/TaskScreen.re *)
open ReactNative;
open MelangeApollo;

module GetTasks = [%graphql {|
  query GetTasks($projectId: ID\!) {
    tasks(projectId: $projectId) {
      id
      title
      status
      priority
    }
  }
|}];

module Styles = Style.(StyleSheet.create({
  "container": style(~flex=1., ~padding=16.->dp, ()),
  "loading": style(~flex=1., ~justifyContent=`center, ~alignItems=`center, ()),
  "error": style(~backgroundColor="#ffebee", ~padding=16.->dp, ~borderRadius=8., ()),
  "taskItem": style(~backgroundColor="white", ~padding=12.->dp, ~marginVertical=4.->dp, ~borderRadius=8., ()),
}));

[@react.component]
let make = (~projectId) => {
  let (result, refetch) = useQuery(GetTasks.make(~projectId, ()));
  
  <View style={Styles.container}>
    {result
     |> RemoteData.fold(
          () => 
            <View style={Styles.loading}>
              <Text> {React.string("Loading tasks...")} </Text>
            </View>,
          error => 
            <View style={Styles.error}>
              <Text> 
                {error##message 
                 |> Option.getOrElse("An error occurred") 
                 |> React.string} 
              </Text>
            </View>,
          data =>
            <ScrollView>
              {data##tasks
               |> Array.map(task =>
                   <TouchableOpacity
                     key={task##id}
                     style={Styles.taskItem}
                     onPress={() => Navigation.navigate("TaskDetail", ~params={"id": task##id})}>
                     <Text> {React.string(task##title)} </Text>
                     <Text> {task##status |> TaskStatus.toString |> React.string} </Text>
                   </TouchableOpacity>
                 )
               |> React.array}
            </ScrollView>
        )}
  </View>
};

Apollo Setup (Mobile)

(* src/utils/Apollo.re *)
open MelangeApollo;

[@mel.raw "__DEV__"] external isDev: bool = "";

let getApiUrl = () =>
  isDev 
  |> Bool.fold(
      () => "https://api.yourdomain.com/graphql",
      () => "http://localhost:5000/graphql"
     );

let client = createClient({
  uri: getApiUrl(),
  headers: Auth.getAuthHeaders(),
});

[@react.component]
let make = (~children) => {
  <ApolloProvider client>
    children
  </ApolloProvider>
};

Mobile-Specific Features

(* src/utils/BiometricAuth.re *)
open ReactNative;

[@mel.module "@react-native-async-storage/async-storage"] 
external setItem: (string, string) => Js.Promise.t(unit) = "";

[@mel.module "expo-local-authentication"]
external authenticateAsync: unit => Js.Promise.t(Js.t({. "success": bool})) = "";

let authenticateUser = () =>
  authenticateAsync()
  |> Js.Promise.then_(result =>
      result##success
      |> Bool.fold(
          () => Js.Promise.reject(Js.Exn.raiseError("Authentication failed")),
          () => Js.Promise.resolve(())
         )
     );

Dune Configuration (Relude.Globals opened automatically)

(melange.emit
 (target mobile)
 (module_systems es6)
 (libraries
  melange_shared
  melange_apollo
  reason-react-native
  relude)
 (preprocess
  (pps melange.ppx reason-react-ppx))
 (flags (:standard -open Relude.Globals)))

Metro Configuration

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');

module.exports = (() => {
  const config = getDefaultConfig(__dirname);
  
  // Watch _build directory for Melange output
  config.watchFolders = [
    path.resolve(__dirname, '_build/default/packages/mobile/src')
  ];
  
  // Ensure proper module resolution
  config.resolver.alias = {
    ...config.resolver.alias,
    '@melange': path.resolve(__dirname, '_build/default/packages')
  };
  
  return config;
})();

Development Scripts

{
  "scripts": {
    "start": "dune build @melange --watch & expo start",
    "ios": "expo start --ios", 
    "android": "expo start --android",
    "build": "dune build @melange",
    "test": "dune runtest"
  }
}

Expo Configuration

{
  "expo": {
    "name": "Melange MVP",
    "slug": "melange-mvp",
    "platforms": ["ios", "android"],
    "version": "1.0.0",
    "main": "_build/default/packages/mobile/src/Index.js",
    "assetBundlePatterns": ["**/*"],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.melange.mvp"
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#FFFFFF"
      },
      "package": "com.melange.mvp"
    }
  }
}

Acceptance Criteria

  • App builds and runs on iOS simulator
  • App builds and runs on Android emulator
  • Apollo integration works
  • Navigation between screens works
  • Shared types work correctly
  • All pattern matching uses Relude functional patterns
  • Relude.Globals opened via dune flags (not in source files)
  • All external bindings use [@mel.xxx] syntax
  • Metro bundler correctly resolves Melange .js output
  • Mobile-specific features work
  • Hot reload works in development

Priority: 🟡 Medium

Important for full-stack demonstration.

Estimated Effort: 4-5 days

Dependencies

  • #36 Apollo bindings
  • #40 Shared types
  • #38 PostGraphile API

CI Validation

  • Build succeeds for both platforms
  • Tests pass
  • Type checking passes
  • Can create development builds
  • Melange compilation produces correct .js files