Shared Code Strategy for React Native
This document outlines what code can be shared between the web (Next.js) and mobile (React Native) apps, and how to structure it.
Shared Packages Structure
packages/
├── shared/
│ ├── types/
│ │ ├── journal.ts # Journal entry types
│ │ ├── user.ts # User and profile types
│ │ ├── notebook.ts # Notebook types
│ │ ├── api.ts # API request/response types
│ │ └── index.ts # Re-exports
│ ├── utils/
│ │ ├── dateUtils.ts # Date formatting (date-fns)
│ │ ├── moonPhase.ts # Moon phase calculations
│ │ ├── timeUtils.ts # Time-of-day utilities
│ │ └── validation.ts # Zod schemas
│ ├── constants/
│ │ ├── fonts.ts # Font names and mappings
│ │ └── colors.ts # Color constants
│ └── package.json
└── ui/ # (Optional) React Native Web components
└── package.json
Shared Types
Journal Entry Types
// packages/shared/types/journal.ts
export interface JournalEntry {
id: string
user_id: string
title: string | null
content: string
mood: string | null
tags: string[]
notebook_id: string | null
font?: string | null
font_size?: number | null
background_image?: string | null
created_at: string
updated_at: string
notebooks?: Notebook | null
}
export interface CreateEntryRequest {
title?: string
content: string
mood?: string
tags?: string[]
notebook_id?: string | null
font?: string
font_size?: number
}
export interface UpdateEntryRequest extends Partial<CreateEntryRequest> {
id: string
}
User Types
// packages/shared/types/user.ts
export interface UserProfile {
id: string
full_name: string | null
avatar_url: string | null
preferences: UserPreferences
role?: 'user' | 'admin' | 'super_admin'
created_at: string
updated_at: string
}
export interface UserPreferences {
theme?: 'light' | 'dark' | 'system'
default_font?: string
default_font_size?: number
[key: string]: any
}
Notebook Types
// packages/shared/types/notebook.ts
export interface Notebook {
id: string
user_id: string
name: string
cover_image: string | null
color: string
font?: string | null
font_size?: number | null
created_at: string
updated_at: string
}
API Types
// packages/shared/types/api.ts
export interface ApiResponse<T> {
data?: T
error?: string
message?: string
}
export interface PaginatedResponse<T> {
items: T[]
page: number
limit: number
total: number
hasMore: boolean
}
Shared Utilities
Date Utilities
// packages/shared/utils/dateUtils.ts
import { format, formatDistanceToNow, isToday, isYesterday } from 'date-fns'
export function formatRelativeDate(date: Date | string): string {
const dateObj = typeof date === 'string' ? new Date(date) : date
if (isToday(dateObj)) {
return formatDistanceToNow(dateObj, { addSuffix: true })
}
if (isYesterday(dateObj)) {
return 'Yesterday'
}
return formatDistanceToNow(dateObj, { addSuffix: true })
}
export function formatAbsoluteDate(date: Date | string): string {
const dateObj = typeof date === 'string' ? new Date(date) : date
return format(dateObj, 'MMMM d, yyyy')
}
Moon Phase Utilities
// packages/shared/utils/moonPhase.ts
export function getMoonPhase(date: Date): number {
// Known new moon date: January 11, 2024 11:57 UTC
const knownNewMoon = new Date('2024-01-11T11:57:00Z').getTime()
const lunarCycle = 29.53059 * 24 * 60 * 60 * 1000
const dateTime = date.getTime()
const daysSinceNewMoon = (dateTime - knownNewMoon) % lunarCycle
const phase = daysSinceNewMoon / lunarCycle
return phase < 0 ? phase + 1 : phase
}
export function getMoonPhaseIcon(phase: number): string {
if (phase < 0.03 || phase >= 0.97) return '🌑'
if (phase < 0.22) return '🌒'
if (phase < 0.28) return '🌓'
if (phase < 0.47) return '🌔'
if (phase < 0.53) return '🌕'
if (phase < 0.72) return '🌖'
if (phase < 0.78) return '🌗'
return '🌘'
}
Time Utilities
// packages/shared/utils/timeUtils.ts
export function isAfterSunset(): boolean {
const now = new Date()
const hour = now.getHours()
return hour >= 18 // 6 PM
}
export function getTimeCircle(date: Date): number {
const hours = date.getHours()
const minutes = date.getMinutes()
let minutesSince3am = (hours * 60 + minutes) - (3 * 60)
if (minutesSince3am < 0) {
minutesSince3am += 24 * 60
}
const minutesPerCircle = (24 * 60) / 7
const circle = Math.floor(minutesSince3am / minutesPerCircle)
return Math.min(6, Math.max(0, circle))
}
Font Utilities
// packages/shared/utils/fonts.ts
export const AVAILABLE_FONTS = [
'PremiumUltra1',
'PremiumUltra2',
'PremiumUltra3',
'PremiumUltra4',
'PremiumUltra5',
'PremiumUltra26',
] as const
export type FontName = typeof AVAILABLE_FONTS[number]
export function resolveFont(font: string | null | undefined, fallback?: string): string {
if (!font) return fallback || 'system'
return AVAILABLE_FONTS.includes(font as FontName) ? font : fallback || 'system'
}
export function resolveFontSize(size: number | null | undefined, fallback: number = 16): number {
return size && size > 0 ? size : fallback
}
Validation Schemas
// packages/shared/utils/validation.ts
import { z } from 'zod'
export const createEntrySchema = z.object({
title: z.string().optional(),
content: z.string().min(1, 'Content is required'),
mood: z.string().optional(),
tags: z.array(z.string()).optional(),
notebook_id: z.string().uuid().nullable().optional(),
font: z.string().optional(),
font_size: z.number().positive().optional(),
})
export const updateEntrySchema = createEntrySchema.partial().extend({
id: z.string().uuid(),
})
Setting Up Shared Package
1. Create Package Structure
mkdir -p packages/shared/{types,utils,constants}
cd packages/shared
npm init -y
2. Update package.json
{
"name": "@hivejournal/shared",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {
"date-fns": "^2.30.0",
"zod": "^3.22.4"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
3. Create tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
4. Move Source Files
mkdir -p src/{types,utils,constants}
# Move type files to src/types/
# Move utility files to src/utils/
# Move constant files to src/constants/
5. Create Index File
// packages/shared/src/index.ts
export * from './types'
export * from './utils'
export * from './constants'
Using Shared Package
In Web App (Next.js)
// apps/frontend/package.json
{
"dependencies": {
"@hivejournal/shared": "workspace:*"
}
}
// Usage
import { JournalEntry, formatRelativeDate } from '@hivejournal/shared'
In Mobile App (React Native)
// apps/mobile/package.json
{
"dependencies": {
"@hivejournal/shared": "workspace:*"
}
}
// Usage
import { JournalEntry, formatRelativeDate } from '@hivejournal/shared'
Platform-Specific Adaptations
What CAN'T Be Shared
- UI Components: Web uses HTML/CSS, React Native uses View/Text
- Navigation: Next.js uses file-based routing, RN uses React Navigation
- Image Handling: Next.js Image vs React Native Image
- Storage: localStorage vs AsyncStorage
- Platform APIs: Browser APIs vs React Native APIs
What CAN Be Shared
- Type Definitions: 100% shareable
- Business Logic: Calculations, transformations
- Validation Schemas: Zod schemas work everywhere
- Date Utilities: date-fns works in both
- Constants: Font names, colors, etc.
- API Types: Request/response interfaces
Migration Strategy
Phase 1: Extract Types
- Move all TypeScript interfaces to shared package
- Update imports in web app
- Use in mobile app from day one
Phase 2: Extract Utilities
- Move date formatting, moon phase, etc.
- Test in web app
- Use in mobile app
Phase 3: Extract Constants
- Move font lists, color constants
- Ensure consistency across platforms
Example: Shared Entry Card Logic
While the UI components can't be shared, the data transformation logic can:
// packages/shared/utils/entryUtils.ts
import { JournalEntry } from '../types'
import { formatRelativeDate, getMoonPhaseIcon, getTimeCircle } from './'
export function getEntryDisplayData(entry: JournalEntry) {
const createdAt = new Date(entry.created_at)
return {
relativeDate: formatRelativeDate(createdAt),
absoluteDate: formatAbsoluteDate(createdAt),
moonPhase: getMoonPhaseIcon(getMoonPhase(createdAt)),
timeCircle: getTimeCircle(createdAt),
hasCustomBackground: !!entry.background_image,
}
}
Then use in both platforms:
// Web
const displayData = getEntryDisplayData(entry)
return <div>{displayData.relativeDate}</div>
// Mobile
const displayData = getEntryDisplayData(entry)
return <Text>{displayData.relativeDate}</Text>
Benefits
- Type Safety: Single source of truth for types
- Consistency: Same logic across platforms
- Maintainability: Update once, works everywhere
- Testing: Test utilities once, use everywhere
- Developer Experience: Autocomplete and types work in both apps
Next Steps
- Create
packages/sharedstructure - Move types from web app
- Extract utilities
- Set up build process
- Use in mobile app from the start