React Integration
True Life uses React 19 with the React Compiler and the observable system for efficient, reactive UI state management. This replaces traditional Redux patterns with direct observable subscriptions.
Core Concepts
Why React with React Compiler?
- Automatic memoization - React Compiler optimizes components without manual useMemo/useCallback
- Familiar ecosystem - Large community, extensive tooling, and component libraries
- Standard JSX - Industry-standard syntax
- React 19 features - Server components, actions, and improved hooks
Why Observables Instead of Redux?
- Cross-runtime sync - State syncs automatically between web, client, and server
- Unified patterns - Same observable API across all runtimes
- Less boilerplate - No actions, reducers, or slice definitions
- Direct updates - Services can update UI state directly
Hooks
useService
Get a service instance via dependency injection:
import { useService } from "@core/react";
import { NeedsService } from "@modules/core/needs/needs.service";
function StatusPanel() {
const needsService = useService(NeedsService);
// Access service methods and observables
const handleReset = () => {
needsService.resetNeeds();
};
return <button onClick={handleReset}>Reset</button>;
}Important: useService must be called at the top level of a component, not inside conditionals or loops.
useObservable
Subscribe to an observable with automatic cleanup:
import { useService, useObservable } from "@core/react";
import { NeedsService } from "@modules/core/needs/needs.service";
function StatusPanel() {
const needsService = useService(NeedsService);
// Returns the current value and re-renders when it changes
const state = useObservable(needsService.hudState);
// Access directly (no function call needed)
return (
<div>
<div>Hunger: {state.hunger}%</div>
<div>Thirst: {state.thirst}%</div>
</div>
);
}Key points:
- Returns the current observable value directly
- Automatically subscribes to observable changes
- Cleans up subscription when component unmounts
- Triggers re-render when observable value changes
Creating Components
Basic Component Structure
// src/modules/my-feature/huds/my-hud/index.tsx
"use web";
import React from "react";
import { useService, useObservable } from "@core/react";
import { MyService } from "@modules/my-feature/my.service";
export default function MyHudPanel() {
const myService = useService(MyService);
const state = useObservable(myService.hudState);
return (
<div className="rounded-lg bg-black/50 p-4 text-white">
<h2 className="text-lg font-bold">My HUD</h2>
<div>{state.someValue}</div>
</div>
);
}Handling Events
function InteractivePanel() {
const notificationService = useService(NotificationService);
const handleClick = () => {
notificationService.pushNotification({
type: "info",
title: "Clicked!",
message: "Button was clicked",
duration: 3000,
});
};
return (
<button className="rounded bg-blue-500 px-4 py-2" onClick={handleClick}>
Click Me
</button>
);
}Conditional Rendering
function ConditionalPanel() {
const myService = useService(MyService);
const state = useObservable(myService.hudState);
return (
<div>
{/* Standard conditional rendering */}
{state.isVisible && <div>Visible content</div>}
{/* Ternary for multiple conditions */}
{state.status === "loading" ? (
<div>Loading...</div>
) : state.status === "error" ? (
<div>Error occurred</div>
) : (
<div>Success!</div>
)}
</div>
);
}List Rendering
function NotificationsList() {
const notificationService = useService(NotificationService);
const state = useObservable(notificationService.notificationsState);
return (
<div className="space-y-2">
{state.notifications.map((notification) => (
<div key={notification.id} className="rounded bg-gray-800 p-2">
<h3>{notification.title}</h3>
<p>{notification.message}</p>
</div>
))}
</div>
);
}Service with webOwned Observable
Services expose webOwned observables for UI state:
// src/modules/needs/needs.service.ts
import { webOwned } from "@core/observable";
interface HudState {
hunger: number;
thirst: number;
health: number;
armor: number;
}
export class NeedsService {
// Web-owned observable - web has R/W, client/server can update
public hudState = webOwned<HudState>({
id: "needs:hud",
initialValue: { hunger: 100, thirst: 100, health: 100, armor: 0 },
});
@ClientOnly
initClientSide(): void {
// Client can update the web observable
setInterval(() => {
const playerHealth = GetEntityHealth(PlayerPedId());
const playerArmor = GetPedArmour(PlayerPedId());
this.hudState.set({
...this.hudState.value,
health: playerHealth,
armor: playerArmor,
});
}, 1000);
}
@ServerOnly
updatePlayerNeeds(playerId: number, hunger: number, thirst: number): void {
// Server can also update the web observable
this.hudState.set({
...this.hudState.value,
hunger,
thirst,
});
}
}Registering UI Components
UI components are registered in module.ts:
// src/modules/needs/module.ts
import { registerModule } from "@core/module";
import { NeedsService } from "@modules/core/needs/needs.service";
import StatusPanel from "@modules/core/needs/huds/status/index";
export default registerModule({
name: "needs",
services: [NeedsService],
huds: [
{
name: "status",
panel: StatusPanel,
hudPosition: "bottom-left",
hudPriority: -10,
hudGroup: "default",
},
],
});Differences from Redux
| Redux | React/Observables |
|---|---|
useSelector(selectData) | useObservable(service.state) |
dispatch(action) | service.state.set(value) |
createSlice() | webOwned() |
| Action creators | Direct service method calls |
| Reducers | Observable .set() method |
Migration Guide
From useAppSelector
// Before (Redux)
const data = useAppSelector(selectData);
return <div>{data.value}</div>;
// After (React/Observables)
const myService = useService(MyService);
const data = useObservable(myService.dataState);
return <div>{data.value}</div>;From useAppDispatch
// Before (Redux)
const dispatch = useAppDispatch();
const handleClick = () => dispatch(updateData(newValue));
// After (React/Observables)
const myService = useService(MyService);
const handleClick = () => myService.dataState.set(newValue);From useNuiEvent
// Before (Redux)
useNuiEvent("my-event", (payload) => {
dispatch(updateData(payload));
});
// After (React/Observables)
// No need! Services update observables directly from client/server
// The UI automatically reacts to observable changesBest Practices
- Use services for all state - Don't create local state for shared data
- Call useService at top level - Not inside conditionals or loops
- Use map for lists - Standard React pattern with key prop
- Use conditional rendering -
&&or ternary operators - Access state directly -
state.valuenotstate().value - Use className - React uses className instead of class
- Let React Compiler optimize - Avoid premature useMemo/useCallback