Skip to main content

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

LibraryRoleNever use it for
@codeleap/queryServer state — fetching, caching, mutationsLocal UI state
@codeleap/storeClient state — UI flags, preferencesServer data
@codeleap/formForm fields, validation, valuesState that isn't a form
@codeleap/portalsModals, drawers, bottom sheets, alertsInline UI toggles
@codeleap/permissionsDevice permission requestsAny 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