Skip to content

Web Frontend with ReasonReact & Apollo

Objective

Build the web application using ReasonReact with Apollo Client integration, styled-ppx for styling, and Relude for functional programming patterns.

Background

This is where everything comes together - ReasonReact components using our Apollo bindings to query the PostGraphile API with type-safe GraphQL, styled with styled-ppx, and leveraging Relude's functional utilities.

Tasks

  • Create packages/web structure
  • Set up ReasonReact with Melange
  • Integrate Apollo Client bindings
  • Configure styled-ppx for styling
  • Create authentication flow (login/signup)
  • Build task management UI components
  • Implement routing with Relude patterns
  • Add state management using functional patterns
  • Create reusable component library
  • Set up Vite for development

Package Structure

packages/web/
├── src/
│   ├── components/
│   │   ├── App.re
│   │   ├── Login.re
│   │   ├── TaskList.re
│   │   └── TaskItem.re
│   ├── pages/
│   │   ├── Dashboard.re
│   │   ├── Projects.re
│   │   └── Settings.re
│   ├── queries/
│   │   ├── UserQueries.re
│   │   └── TaskQueries.re
│   ├── styles/
│   │   └── Theme.re
│   ├── utils/
│   │   └── Auth.re
│   └── Index.re
├── public/
│   └── index.html
├── lib/
│   └── dune
├── package.json
├── vite.config.js
└── dune-project

Component Example (Relude + styled-ppx)

(* src/components/TaskList.re *)
open ReasonReact;
open MelangeApollo;
open Relude.Globals;

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

module Styles = {
  open Styled;
  
  let container = [%styled.div {|
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1.5rem;
    background: var(--background);
  |}];
  
  let loading = [%styled.div {|
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 200px;
    color: var(--text-muted);
  |}];
  
  let error = [%styled.div {|
    padding: 1rem;
    border-radius: 0.5rem;
    background: var(--error-bg);
    color: var(--error-text);
  |}];
};

[@react.component]
let make = (~projectId) => {
  let (result, refetch) = useQuery(GetTasks.make(~projectId, ()));
  
  <Styles.container>
    {result
     |> RemoteData.fold(
          () => <Styles.loading> {React.string("Loading tasks...")} </Styles.loading>,
          error => 
            <Styles.error> 
              {error##message |> Option.getOrElse("An error occurred") |> React.string} 
            </Styles.error>,
          data =>
            data##tasks
            |> Array.map(task =>
                <TaskItem
                  key={task##id}
                  task
                  onUpdate={() => refetch() |> ignore}
                />
              )
            |> React.array
        )}
  </Styles.container>
};

Styled-PPX Theme Setup

(* src/styles/Theme.re *)
module Colors = {
  let primary = "#007AFF";
  let secondary = "#5856D6";
  let success = "#34C759";
  let warning = "#FF9500";
  let error = "#FF3B30";
  
  let background = "#FFFFFF";
  let surface = "#F2F2F7";
  let text = "#000000";
  let textMuted = "#8E8E93";
};

module Spacing = {
  let xs = "0.25rem";
  let sm = "0.5rem";
  let md = "1rem";
  let lg = "1.5rem";
  let xl = "2rem";
};

module Button = [%styled.button {|
  padding: $(Spacing.sm) $(Spacing.md);
  border-radius: 0.375rem;
  border: none;
  background: $(Colors.primary);
  color: white;
  font-weight: 500;
  cursor: pointer;
  transition: opacity 0.2s;
  
  &:hover {
    opacity: 0.9;
  }
  
  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
|}];

Apollo Setup with Relude

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

let getAuthHeader = () =>
  Auth.getToken()
  |> Option.map(token => ("Authorization", "Bearer " ++ token))
  |> Option.toList
  |> List.toArray;

let client = createClient({
  uri: "http://localhost:5000/graphql",
  headers: getAuthHeader(),
});

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

Routing with Relude

(* src/Router.re *)
open ReasonReactRouter;
open Relude.Globals;

type route =
  | Dashboard
  | Login
  | Projects
  | ProjectDetail(string)
  | NotFound;

let parseRoute = url =>
  url.path
  |> List.fromArray
  |> (
    fun
    | [] => Dashboard
    | ["login"] => Login
    | ["projects"] => Projects
    | ["projects", id] => ProjectDetail(id)
    | _ => NotFound
  );

[@react.component]
let make = () => {
  let url = useUrl();
  
  url
  |> parseRoute
  |> (
    fun
    | Dashboard => <Dashboard />
    | Login => <Login />
    | Projects => <Projects />
    | ProjectDetail(id) => <ProjectDetail id />
    | NotFound => <NotFound />
  );
};

State Management with Relude

(* src/utils/Store.re *)
open Relude.Globals;

type state = {
  user: option(User.t),
  tasks: list(Task.t),
  loading: bool,
};

type action =
  | SetUser(option(User.t))
  | SetTasks(list(Task.t))
  | SetLoading(bool);

let reducer = (state, action) =>
  action |> (
    fun
    | SetUser(user) => {...state, user}
    | SetTasks(tasks) => {...state, tasks}
    | SetLoading(loading) => {...state, loading}
  );

let initialState = {
  user: None,
  tasks: [],
  loading: false,
};

Acceptance Criteria

  • Application builds with Melange
  • styled-ppx generates CSS correctly
  • All pattern matching uses Relude utilities
  • Authentication flow works
  • CRUD operations on tasks work
  • Real-time updates work (subscriptions)
  • Routing works with functional patterns
  • Hot reload in development
  • Production build optimized

Dune Configuration

(melange.emit
 (target web)
 (libraries
  melange_shared
  melange_apollo
  reason-react
  relude
  styled-ppx)
 (preprocess
  (pps melange.ppx reason-react-ppx styled-ppx)))

Testing Guidelines

  • Use Relude's Result/Option for all error handling
  • Component tests with melange-jest
  • Integration tests with Apollo MockProvider
  • Verify styled-ppx output

Priority: 🟡 High

Primary user interface for the template.

Estimated Effort: 3-4 days

Dependencies

  • #36 Apollo bindings
  • #39 GraphQL-PPX
  • #40 Shared types
  • #38 PostGraphile API
  • #8 styled-ppx integration

CI Validation

  • Build succeeds
  • styled-ppx processes correctly
  • Relude patterns used consistently
  • Tests pass
  • Type checking passes