Best Practices

A curated collection of best practices, patterns, and recommendations for building high-quality Teachfloor apps.

Development

Project Structure

Organize your code logically:

my-app/
├── src/
│   ├── index.js                # Entry point
│   ├── views/                  # Viewport components
│   │   ├── App.jsx
│   │   ├── CourseView.jsx
│   │   └── SettingsView.jsx
│   ├── components/             # Reusable components
│   │   ├── Header.jsx
│   │   └── shared/
│   ├── hooks/                  # Custom hooks
│   │   ├── useAppData.js
│   │   └── usePreferences.js
│   ├── utils/                  # Utilities
│   │   ├── api.js
│   │   └── formatting.js
│   └── constants/              # Constants
│       └── config.js
├── public/
└── teachfloor-app.json

Component Design

Do: Create small, focused components

// ✅ Good
function NotesList({ notes }) {
  return notes.map(note => <NoteItem key={note.id} note={note} />)
}

function NoteItem({ note }) {
  return <div>{note.title}</div>
}

Don't: Create monolithic components

// ❌ Bad
function NotesApp() {
  // 500 lines of code...
}

Custom Hooks

Extract logic into reusable hooks:

// ✅ Good
function useNotes() {
  const [notes, setNotes] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    loadNotes()
  }, [])

  async function loadNotes() {
    const data = await retrieve('notes', 'userdata')
    setNotes(data || [])
    setLoading(false)
  }

  async function addNote(note) {
    const updated = [...notes, note]
    await store('notes', updated, 'userdata')
    setNotes(updated)
  }

  return { notes, loading, addNote }
}

// Usage
function MyComponent() {
  const { notes, loading, addNote } = useNotes()
  // ...
}

Error Boundaries

Implement error boundaries:

class ErrorBoundary extends React.Component {
  state = { hasError: false }

  static getDerivedStateFromError(error) {
    return { hasError: true }
  }

  componentDidCatch(error, errorInfo) {
    console.error('App error:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return (
        <Container>
          <Text>Something went wrong. Please refresh.</Text>
        </Container>
      )
    }

    return this.props.children
  }
}

// Usage
<ErrorBoundary>
  <App />
</ErrorBoundary>

Performance

Lazy Loading

Load components only when needed:

import { lazy, Suspense } from 'react'

const HeavyComponent = lazy(() => import('./HeavyComponent'))

function App() {
  return (
    <Suspense fallback={<Loader />}>
      <HeavyComponent />
    </Suspense>
  )
}

Memoization

Memoize expensive calculations:

import { useMemo } from 'react'

function AnalyticsDashboard({ data }) {
  const statistics = useMemo(() => {
    // Expensive calculation
    return calculateStatistics(data)
  }, [data])

  return <div>{statistics}</div>
}

Debouncing

Debounce frequent operations:

import { useCallback, useRef } from 'react'

function SearchInput() {
  const timeoutRef = useRef(null)

  const debouncedSearch = useCallback((query) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
    }

    timeoutRef.current = setTimeout(() => {
      performSearch(query)
    }, 500)
  }, [])

  return (
    <TextInput
      placeholder="Search..."
      onChange={(e) => debouncedSearch(e.target.value)}
    />
  )
}

Bundle Size

Minimize bundle size:

// ✅ Good: Import only what you need
import { Button, Text } from '@teachfloor/extension-kit'

// ❌ Bad: Import everything
import * as ExtensionKit from '@teachfloor/extension-kit'

Caching

Implement data caching:

const cache = new Map()

async function getCachedData(key, fetchFn, ttl = 60000) {
  const cached = cache.get(key)

  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data
  }

  const data = await fetchFn()
  cache.set(key, { data, timestamp: Date.now() })

  return data
}

Security

Input Validation

Always validate user input:

function validateEmail(email) {
  const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return re.test(email)
}

function saveUserData(email, name) {
  if (!validateEmail(email)) {
    throw new Error('Invalid email format')
  }

  if (!name || name.length < 2) {
    throw new Error('Name must be at least 2 characters')
  }

  // Save data
}

Sanitization

Sanitize data before storage:

function sanitizeString(str) {
  return str
    .replace(/<script[^>]*>.*?<\/script>/gi, '')
    .replace(/<[^>]+>/g, '')
    .trim()
}

async function saveNote(content) {
  const sanitized = sanitizeString(content)
  await store('note', sanitized, 'userdata')
}

Sensitive Data

Never expose secrets:

// ❌ Bad
const API_KEY = 'secret-key-123'

// ✅ Good: Let users provide their own keys
function Settings() {
  const [apiKey, setApiKey] = useState('')

  async function saveKey() {
    await store('api-key', apiKey, 'userdata')
  }

  return (
    <div>
      <PasswordInput
        value={apiKey}
        onChange={(e) => setApiKey(e.target.value)}
      />
      <Button onClick={saveKey}>Save</Button>
    </div>
  )
}

HTTPS Only

Always use HTTPS for external APIs:

// ✅ Good
fetch('https://api.example.com/data')

// ❌ Bad
fetch('http://api.example.com/data')

User Experience

Loading States

Show loading indicators:

function DataDisplay() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    loadData()
  }, [])

  if (loading) {
    return <Loader />
  }

  return <DisplayData data={data} />
}

Empty States

Handle empty data gracefully:

function NotesList({ notes }) {
  if (notes.length === 0) {
    return (
      <Container>
        <Text c="dimmed">No notes yet. Create your first note!</Text>
        <Button onClick={createNote}>Create Note</Button>
      </Container>
    )
  }

  return notes.map(note => <NoteItem key={note.id} note={note} />)
}

Error Handling

Provide helpful error messages:

function DataLoader() {
  const [error, setError] = useState(null)

  async function loadData() {
    try {
      const data = await fetchData()
      setData(data)
    } catch (error) {
      setError('Failed to load data. Please try again.')
      showToast(error.message, { type: 'error' })
    }
  }

  if (error) {
    return (
      <Container>
        <Text c="red">{error}</Text>
        <Button onClick={loadData}>Retry</Button>
      </Container>
    )
  }

  return <div>Content</div>
}

Feedback

Provide immediate feedback:

async function saveData(data) {
  try {
    showToast('Saving...', { type: 'info' })
    await store('data', data, 'userdata')
    showToast('Saved successfully!', { type: 'success' })
  } catch (error) {
    showToast('Failed to save', { type: 'error' })
  }
}

Accessibility

Make your app accessible:

// ✅ Good: Proper labels and ARIA attributes
<Button
  aria-label="Save note"
  onClick={saveNote}
>
  Save
</Button>

<TextInput
  label="Note title"
  aria-required="true"
  aria-invalid={hasError}
/>

Code Quality

Naming Conventions

Use clear, descriptive names:

// ✅ Good
const userNotes = []
function calculateTotalScore() {}
const isAuthenticated = true

// ❌ Bad
const arr = []
function calc() {}
const flag = true

Comments

Write meaningful comments:

// ✅ Good
// Calculate the total score by summing all module scores
// and applying a weighted average based on difficulty
function calculateTotalScore(modules) {
  // ...
}

// ❌ Bad
// This function calculates score
function calculateTotalScore(modules) {
  // Loop through modules
  // ...
}

Constants

Use constants for magic numbers:

// ✅ Good
const MAX_NOTES = 100
const CACHE_TTL = 60000 // 1 minute

if (notes.length >= MAX_NOTES) {
  // Handle limit
}

// ❌ Bad
if (notes.length >= 100) {
  // Handle limit
}

DRY Principle

Don't repeat yourself:

// ✅ Good
function formatDate(date) {
  return new Intl.DateTimeFormat('en-US').format(date)
}

const created = formatDate(note.createdAt)
const updated = formatDate(note.updatedAt)

// ❌ Bad
const created = new Intl.DateTimeFormat('en-US').format(note.createdAt)
const updated = new Intl.DateTimeFormat('en-US').format(note.updatedAt)

Testing

Unit Tests

Write tests for utility functions:

// utils/formatting.test.js
import { formatDate } from './formatting'

test('formats date correctly', () => {
  const date = new Date('2024-01-01')
  expect(formatDate(date)).toBe('1/1/2024')
})

Component Tests

Test component rendering:

import { render, screen } from '@testing-library/react'
import NotesList from './NotesList'

test('renders empty state', () => {
  render(<NotesList notes={[]} />)
  expect(screen.getByText(/no notes yet/i)).toBeInTheDocument()
})

test('renders notes', () => {
  const notes = [{ id: 1, title: 'Test Note' }]
  render(<NotesList notes={notes} />)
  expect(screen.getByText('Test Note')).toBeInTheDocument()
})

Manual Testing

Test in all viewports:

□ Tested in course list page
□ Tested in course detail page
□ Tested in settings page
□ Tested on mobile viewport
□ Tested with different user roles
□ Tested with empty data
□ Tested with maximum data
□ Tested error scenarios

Deployment

Pre-Deployment Checklist

Before uploading:

□ Version incremented in manifest
□ All console.log() removed
□ Environment variables removed
□ Tests passing
□ No linting errors
□ Build succeeds
□ Bundle size acceptable
□ Permissions list complete
□ README updated
□ Changelog updated

Versioning

Follow semantic versioning:

1.0.0 → 1.0.1: Bug fix
1.0.0 → 1.1.0: New feature
1.0.0 → 2.0.0: Breaking change

Changelog

Maintain a changelog:

# Changelog

## [1.1.0] - 2024-01-15
### Added
- Export notes to PDF feature
- Dark mode support

### Fixed
- Note saving bug on slow connections

## [1.0.0] - 2024-01-01
### Added
- Initial release
- Basic note-taking functionality

Documentation

Keep documentation updated:

# My App

## Features
- Feature 1
- Feature 2

## Installation
1. Install from marketplace
2. Configure settings

## Usage
...

## Support
Email: support@example.com

Patterns

State Management

Use appropriate state management:

// Local state
const [count, setCount] = useState(0)

// Context for app-wide state
const AppContext = createContext()

function AppProvider({ children }) {
  const [user, setUser] = useState(null)

  return (
    <AppContext.Provider value={{ user, setUser }}>
      {children}
    </AppContext.Provider>
  )
}

// Custom hooks for complex state
function useAppState() {
  const [state, setState] = useState(initialState)

  function updateState(updates) {
    setState(prev => ({ ...prev, ...updates }))
  }

  return [state, updateState]
}

API Integration

Structure API calls:

// api.js
class API {
  constructor(baseURL) {
    this.baseURL = baseURL
  }

  async get(endpoint) {
    const response = await fetch(`${this.baseURL}${endpoint}`)
    if (!response.ok) throw new Error('API error')
    return response.json()
  }

  async post(endpoint, data) {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
    if (!response.ok) throw new Error('API error')
    return response.json()
  }
}

export const api = new API('https://api.example.com')

// Usage
const data = await api.get('/notes')
await api.post('/notes', { title: 'New Note' })

Form Handling

Handle forms effectively:

function NoteForm({ onSave }) {
  const [values, setValues] = useState({ title: '', content: '' })
  const [errors, setErrors] = useState({})

  function validate() {
    const errors = {}

    if (!values.title) errors.title = 'Title is required'
    if (!values.content) errors.content = 'Content is required'

    return errors
  }

  async function handleSubmit(e) {
    e.preventDefault()

    const errors = validate()
    if (Object.keys(errors).length > 0) {
      setErrors(errors)
      return
    }

    try {
      await onSave(values)
      showToast('Saved!', { type: 'success' })
    } catch (error) {
      showToast('Failed to save', { type: 'error' })
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <TextInput
        label="Title"
        value={values.title}
        onChange={(e) => setValues({ ...values, title: e.target.value })}
        error={errors.title}
      />
      <Textarea
        label="Content"
        value={values.content}
        onChange={(e) => setValues({ ...values, content: e.target.value })}
        error={errors.content}
      />
      <Button type="submit">Save</Button>
    </form>
  )
}

Remember: Quality over speed. Take time to write maintainable, secure, and user-friendly code.