Libraries Integration Guide
This guide shows how the internal libraries work together in real features. Read this before writing any screen that touches data, forms, or overlays.
The libraries and their roles
| Library | Role | Never use it for |
|---|---|---|
@codeleap/query | Server state — fetching, caching, mutations | Local UI state |
@codeleap/store | Client state — UI flags, preferences | Server data |
@codeleap/form | Form fields, validation, values | State that isn't a form |
@codeleap/portals | Modals, drawers, bottom sheets, alerts | Inline UI toggles |
@codeleap/permissions | Device permission requests | Any non-device permission |
Pattern 1 — Edit form with query mutation
The most common pattern: a form pre-filled at navigation time, submitted via a query mutation.
src/forms/editProfile.ts
import { form, fields, zodValidator } from '@codeleap/form'
import { z } from 'zod'
// Declare OUTSIDE any component
export const editProfileForm = form('editProfile', {
name: fields.text({
label: 'Name',
validate: zodValidator(z.string().min(2)),
}),
email: fields.text({
label: 'Email',
validate: zodValidator(z.string().email()),
}),
})
src/screens/UserList.tsx
import { editProfileForm } from '../forms/editProfile'
function UserList() {
const handleEditPress = (user) => {
// Pre-fill at navigation time — before the screen mounts
editProfileForm.setValues({ name: user.name, email: user.email })
navigation.navigate('EditProfile', { userId: user.id })
}
// ...
}
src/screens/EditProfile.tsx
import { editProfileForm } from '../forms/editProfile'
import { usersManager } from '../query/users'
function EditProfileScreen({ userId }) {
const updateMutation = usersManager.useUpdate()
const handleSubmit = () => {
if (!editProfileForm.isValid) return
updateMutation.mutate({ id: userId, ...editProfileForm.values }, {
onSuccess: () => {
editProfileForm.resetValues()
navigation.goBack()
},
})
}
return (
<>
<TextInput {...editProfileForm.register('name')} />
<TextInput {...editProfileForm.register('email')} />
<Button onPress={handleSubmit} text='Save' disabled={updateMutation.isPending} />
</>
)
}
Pattern 2 — Confirmation portal before a destructive mutation
Use portal.request() to get a user decision before running a delete mutation.
src/portals/confirmDelete.tsx
import { modal } from '@codeleap/portals'
export const ConfirmDeletePortal = modal({ id: 'CONFIRM_DELETE' })
.content((props) => {
const { request, itemName } = props
return (
<View>
<Text text={`Delete "${itemName}"?`} />
<Button text='Cancel' onPress={() => request.resolve(false)} />
<Button text='Delete' onPress={() => request.resolve(true)} />
</View>
)
})
src/screens/UserList.tsx
import { ConfirmDeletePortal } from '../portals/confirmDelete'
import { usersManager } from '../query/users'
function UserList() {
const deleteMutation = usersManager.useDelete()
const handleDeletePress = async (user) => {
const confirmed = await ConfirmDeletePortal.request({ itemName: user.name })
if (!confirmed) return
deleteMutation.mutate(user.id)
}
// ...
}
Pattern 3 — Permission check before a feature
Always check() on mount (silent), only request() when the user explicitly triggers the action.
src/screens/Camera.tsx
import { permissionsManager } from '../app/permissionsManager'
function CameraScreen() {
// Silent check on mount — updates stored status without prompting
permissionsManager.check('camera')
const handleCapturePress = async () => {
// Only prompt when user explicitly acts
const status = await permissionsManager.request('camera')
if (!status.isGranted) return
// proceed with camera capture
}
// ...
}
Pattern 4 — Global UI state with store
Use store for UI state that multiple components need, but has no server equivalent.
src/stores/ui.ts
import { globalState } from '@codeleap/store'
export const uiState = globalState({
sidebarOpen: false,
activeTab: 'feed' as 'feed' | 'profile' | 'settings',
})
export const toggleSidebar = () =>
uiState.set({ sidebarOpen: !uiState.value.sidebarOpen })
export const setActiveTab = (tab: typeof uiState.value.activeTab) =>
uiState.set({ activeTab: tab })
src/components/Sidebar.tsx
function Sidebar() {
// Selector — only re-renders when sidebarOpen changes
const isOpen = uiState.use(s => s.sidebarOpen)
return isOpen ? <SidebarContent /> : null
}
Pattern 5 — Bottom sheet with filters (portals + query)
src/portals/filters.tsx
import { bottomSheet } from '@codeleap/portals'
export const FiltersSheet = bottomSheet({ id: 'FILTERS' })
.content((props) => {
const { request, currentFilters } = props
const [filters, setFilters] = useState(currentFilters)
return (
<View>
<Text text='Filters' />
{/* filter controls */}
<Button text='Apply' onPress={() => request.resolve(filters)} />
<Button text='Cancel' onPress={() => request.resolve(null)} />
</View>
)
})
src/screens/UserList.tsx
import { FiltersSheet } from '../portals/filters'
import { usersManager } from '../query/users'
function UserList() {
const [filters, setFilters] = useState({})
const { items } = usersManager.useList({ filters })
const handleFiltersPress = async () => {
const newFilters = await FiltersSheet.request({ currentFilters: filters })
if (newFilters) setFilters(newFilters)
}
// ...
}
Pattern 6 — Styling components with @codeleap/styles
Always use string variants and createStyles — never StyleSheet.create() or hardcoded values.
src/components/UserCard.tsx
import { createStyles } from '@codeleap/styles'
// createStyles for theme-dependent values
const styles = createStyles((theme) => ({
avatar: {
borderRadius: theme.radius.full,
borderWidth: theme.stroke.thin,
borderColor: theme.colors.primary,
},
}))
function UserCard({ user, onPress }) {
return (
// String variants for layout and spacing
<Touchable style={['row', 'alignCenter', 'gap:2', 'p:3']} onPress={onPress}>
<Image style={[styles.avatar, { width: 40, height: 40 }]} source={user.avatar} />
<View style={['flex', 'column', 'gap:0.5']}>
<Text style={['h4', 'color:textPrimary']}>{user.name}</Text>
<Text style={['body2', 'color:textSecondary']}>{user.email}</Text>
</View>
</Touchable>
)
}
Anti-patterns to avoid
// ❌ Don't use StyleSheet.create() — it bypasses the theme
const styles = StyleSheet.create({ container: { padding: 16 } })
// ✅ Use createStyles()
const styles = createStyles((theme) => ({ container: { ...theme.spacing.padding(2) } }))
// ❌ Don't hardcode colors or spacing
<Text style={{ color: '#007AFF', fontSize: 16 }} />
// ✅ Use theme tokens and string variants
<Text style={['color:primary', 'body1']} />
// ❌ Don't store API data in store
const usersState = globalState([])
fetch('/api/users').then(data => usersState.set(data))
// ✅ Use usersManager.useList() instead
// ❌ Don't pre-fill forms inside the component
function EditScreen({ user }) {
useEffect(() => { editProfileForm.setValues(user) }, [user.id])
}
// ✅ Call setValues before navigating to the screen
// ❌ Don't use useState for confirmation dialogs
const [showConfirm, setShowConfirm] = useState(false)
// ✅ Use ConfirmDeletePortal.request() — returns a promise
// ❌ Don't request permissions on mount
useEffect(() => { permissionsManager.request('camera') }, [])
// ✅ Call check() on mount, request() only on user action
// ❌ Don't use @codeleap/modals for new code
import { modal } from '@codeleap/modals'
// ✅ Use @codeleap/portals — it's the evolution with drawers, sheets and alerts