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
🟡 High
Priority: Primary user interface for the template.
Estimated Effort: 3-4 days
Dependencies
CI Validation
-
Build succeeds -
styled-ppx processes correctly -
Relude patterns used consistently -
Tests pass -
Type checking passes