UI Features
Features are the building blocks of True Life's UI system. They encapsulate components, state, and lifecycle hooks.
Feature Definition
Features are defined using defineFeature():
// src/modules/my-feature/huds/my-hud/feature.tsx
"use web";
import { defineFeature } from "@ui/feature";
import Panel from "@modules/my-feature/huds/my-hud/Panel";
import reducer, { mySlice } from "@modules/my-feature/huds/my-hud/state/slice";
export default defineFeature({
// React component to render
panel: Panel,
// Redux slices to inject when feature loads
slices: [{ name: mySlice.name, reducer }],
// For HUD features: grid position
hudPosition: "bottom-left",
// For HUD features: render priority (lower = renders first)
hudPriority: -10,
// Optional: called after slices injected, before rendering
preload: async () => {
// Load initial data
},
// Optional: called when feature unloads
teardown: async () => {
// Cleanup
},
});
HUD Positions
HUDs are positioned in a 3x3 grid:
| Position | Use Case |
|---|---|
top-left | Minimap, time |
top-center | Notifications |
top-right | Money, inventory quick-view |
center-left | Quest trackers |
center-center | Modal dialogs |
center-right | Vehicle HUD |
bottom-left | Health, needs |
bottom-center | Speedometer |
bottom-right | Radio, voice |
HUD Priority
When multiple HUDs share a position, hudPriority determines stacking order:
// Renders first (bottom of stack)
hudPriority: -100,
// Renders last (top of stack)
hudPriority: 100,
Creating a HUD Feature
1. Create Directory Structure
src/modules/my-feature/huds/my-hud/
├── feature.tsx
├── Panel.tsx
└── state/
└── slice.ts
2. Create Redux Slice
// src/modules/my-feature/huds/my-hud/state/slice.ts
"use web";
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
interface MyHudState {
value: number;
label: string;
}
const initialState: MyHudState = {
value: 0,
label: "",
};
export const myHudSlice = createSlice({
name: "myHud",
initialState,
reducers: {
setValue: (state, action: PayloadAction<number>) => {
state.value = action.payload;
},
setLabel: (state, action: PayloadAction<string>) => {
state.label = action.payload;
},
update: (state, action: PayloadAction<Partial<MyHudState>>) => {
Object.assign(state, action.payload);
},
},
});
export const { setValue, setLabel, update } = myHudSlice.actions;
// Selectors
export const selectValue = (state: { myHud: MyHudState }) => state.myHud.value;
export const selectLabel = (state: { myHud: MyHudState }) => state.myHud.label;
export default myHudSlice.reducer;
3. Create Panel Component
// src/modules/my-feature/huds/my-hud/Panel.tsx
"use web";
import { useAppSelector, useNuiEvent, useAppDispatch } from "@ui/hooks";
import { selectValue, selectLabel, update } from "@modules/my-feature/huds/my-hud/state/slice";
export default function Panel() {
const dispatch = useAppDispatch();
const value = useAppSelector(selectValue);
const label = useAppSelector(selectLabel);
// Receive events from FiveM client
useNuiEvent<{ value: number; label: string }>("my-hud:update", (data) => {
dispatch(update(data));
});
return (
<div className="rounded-lg bg-black/80 p-4 backdrop-blur">
<div className="text-sm text-gray-400">{label}</div>
<div className="text-2xl font-bold text-white">{value}</div>
</div>
);
}
4. Create Feature Definition
// src/modules/my-feature/huds/my-hud/feature.tsx
"use web";
import { defineFeature } from "@ui/feature";
import Panel from "@modules/my-feature/huds/my-hud/Panel";
import reducer, { myHudSlice } from "@modules/my-feature/huds/my-hud/state/slice";
export default defineFeature({
panel: Panel,
slices: [{ name: myHudSlice.name, reducer }],
hudPosition: "bottom-left",
hudPriority: 0,
});
5. Register in Module
// src/modules/my-feature/module.ts
import { registerModule } from "@core/module";
export default registerModule({
name: "my-feature",
huds: {
"my-hud": () => import("@modules/my-feature/huds/my-hud/feature"),
},
});
Creating a Page Feature
Pages follow the same pattern but without positioning:
// src/modules/atm/pages/atm-interface/feature.tsx
"use web";
import { defineFeature } from "@ui/feature";
import Page from "@modules/atm/pages/atm-interface/Page";
import reducer, { atmSlice } from "@modules/atm/pages/atm-interface/state/slice";
export default defineFeature({
panel: Page,
slices: [{ name: atmSlice.name, reducer }],
// No hudPosition = page feature
});
Lifecycle Hooks
preload
Called after slices are injected, before rendering:
preload: async () => {
// Fetch initial data
const data = await fetchInitialData();
store.dispatch(setInitialData(data));
},
teardown
Called when the feature is unloaded:
teardown: async () => {
// Cleanup subscriptions
subscription.unsubscribe();
// Clear state
store.dispatch(clearState());
},
Feature Loading
Features are loaded dynamically when needed:
// Load a feature
await dispatch(loadFeature("my-hud"));
// Unload a feature
await dispatch(unloadFeature("my-hud"));
The feature system automatically:
- Dynamically imports the feature module
- Injects Redux slices
- Calls
preloadhook - Renders the component
- On unload, calls
teardownand removes slices
Best Practices
- Use
"use web"directive - All feature files need it - Keep components focused - Single responsibility
- Use selectors - Don't access state directly
- Handle loading states - Show skeletons/loaders
- Clean up in teardown - Prevent memory leaks
- Use path aliases - Consistent imports