| name | vue-composables |
| description | Patterns for Vue 3 composables with localStorage persistence in Nuxt 4. Activate when creating composables, working with useProgress, useQuiz, useCertificate, or localStorage. Use when this capability is needed. |
| metadata | {"author":"fawzymohamed"} |
Vue Composables Patterns (Nuxt 4)
Activation Triggers
- Creating new composables in
app/composables/ directory
- Working with reactive state management
- Implementing localStorage persistence
- Building useProgress, useQuiz, or useCertificate
Nuxt 4 Composables Location
app/
└── composables/
├── useProgress.ts
├── useQuiz.ts
└── useCertificate.ts
Composable Structure Template
import { ref, computed, readonly } from 'vue'
export function useExample() {
const _state = ref<StateType>(initialValue)
const derivedValue = computed(() => {
return _state.value.something
})
function doSomething(param: ParamType) {
_state.value =
}
return {
state: readonly(_state),
derivedValue,
doSomething
}
}
useProgress Implementation
import type { UserProgress, SubtopicProgress } from '~/data/types'
const STORAGE_KEY = 'devops-lms-progress'
export function useProgress() {
const progress = useState<UserProgress>('user-progress', () => {
return loadFromStorage() ?? createDefaultProgress()
})
function loadFromStorage(): UserProgress | null {
if (typeof window === 'undefined') return null
try {
const stored = localStorage.getItem(STORAGE_KEY)
return stored ? JSON.parse(stored) : null
} catch (e) {
console.warn('Failed to load progress from localStorage', e)
return
}
}
(): {
{
: ().(),
: {}
}
}
() {
( === )
{
.(, .(progress.))
} (e) {
.(, e)
}
}
() {
(!progress..[phaseId]) {
progress..[phaseId] = { : {} }
}
(!progress..[phaseId].[topicId]) {
progress..[phaseId].[topicId] = { : {} }
}
}
() {
(phaseId, topicId)
progress..[phaseId].[topicId].[subtopicId] = {
: ,
: ().(),
:
}
()
}
() {
(phaseId, topicId)
subtopic = progress..[phaseId].[topicId].[subtopicId]
(subtopic) {
subtopic. = score
subtopic. = ().()
} {
progress..[phaseId].[topicId].[subtopicId] = {
: ,
: ,
: score,
: ().()
}
}
()
}
(): {
(!topicId) {
phase = progress..[phaseId]
(!phase)
}
(!subtopicId) {
topic = progress..[phaseId]?.[topicId]
(!topic)
}
!!progress..[phaseId]?.[topicId]?.[subtopicId]?.
}
(): {
count =
( phase .(progress..)) {
( topic .(phase.)) {
( subtopic .(topic.)) {
(subtopic.) count++
}
}
}
count
}
(): {
.(progress., , )
}
() {
{
parsed = .(data)
progress. = parsed
()
} {
}
}
() {
progress. = ()
()
}
{
: (progress),
markComplete,
recordQuizScore,
isComplete,
getCompletedCount,
exportProgress,
importProgress,
resetProgress
}
}
useQuiz Implementation
import type { Quiz, QuizQuestion, QuizAnswer } from '~/data/types'
export function useQuiz(quiz: Ref<Quiz> | Quiz) {
const quizData = isRef(quiz) ? quiz : ref(quiz)
const currentIndex = ref(0)
const answers = ref<QuizAnswer[]>([])
const isComplete = ref(false)
const score = ref(0)
const currentQuestion = computed(() =>
quizData.value.questions[currentIndex.value]
)
const totalQuestions = computed(() =>
quizData.value.questions.length
)
const isLastQuestion = computed(() =>
currentIndex.value === totalQuestions.value - 1
)
const isFirstQuestion = computed(
currentIndex. ===
)
passed = (
score. >= quizData..
)
() {
answers.[currentIndex.] = {
: currentIndex.,
selected
}
}
() {
(!isLastQuestion.) {
currentIndex.++
}
}
() {
(!isFirstQuestion.) {
currentIndex.--
}
}
() {
(index >= && index < totalQuestions.) {
currentIndex. = index
}
}
() {
score. = ()
isComplete. =
}
(): {
correct =
answers..( {
question = quizData..[index]
((answer, question)) {
correct++
}
})
.((correct / totalQuestions.) * )
}
(): {
(!answer)
(question.) {
:
answer. === question.
:
selected = answer. []
correct = question.!
(
selected. === correct. &&
selected.( correct.(s))
)
:
answer. === question.
:
}
}
(): | {
answers.[index]
}
() {
currentIndex. =
answers. = []
isComplete. =
score. =
}
{
: (currentIndex),
currentQuestion,
totalQuestions,
isLastQuestion,
isFirstQuestion,
: (answers),
: (isComplete),
: (score),
passed,
submitAnswer,
nextQuestion,
previousQuestion,
goToQuestion,
finishQuiz,
getAnswerForQuestion,
reset
}
}
useCertificate Implementation
import type { CertificateData } from '~/data/types'
export function useCertificate() {
const isGenerating = ref(false)
const error = ref<string | null>(null)
function generateCertificateId(): string {
const timestamp = Date.now().toString(36)
const random = Math.random().toString(36).substring(2, 8)
return `DEVOPS-${timestamp}-${random}`.toUpperCase()
}
function calculateTotalHours(completedLessons: number): number {
return Math.round((completedLessons * 15) / 60)
}
async (): < | > {
( === )
isGenerating. =
error. =
{
[{ : html2canvas }, { : jsPDF }] = .([
(),
()
])
element = .()
(!element) {
()
}
canvas = (element, {
: ,
:
})
imgData = canvas.()
pdf = ({
: ,
: ,
:
})
imgWidth =
imgHeight = (canvas. * imgWidth) / canvas.
pdf.(imgData, , , , imgWidth, imgHeight)
pdf.()
} (e) {
error. = e ? e. :
} {
isGenerating. =
}
}
() {
blob = (data)
(!blob)
url = .(blob)
link = .()
link. = url
link. =
..(link)
link.()
..(link)
.(url)
}
{
: (isGenerating),
: (error),
generateCertificateId,
calculateTotalHours,
generatePDF,
downloadCertificate
}
}
SSR Compatibility Patterns
function useClientSideFeature() {
const isClient = ref(false)
onMounted(() => {
isClient.value = true
})
return { isClient }
}
const sharedState = useState('key', () => defaultValue)
const { data } = await useAsyncData('key', () => fetchData())
const loadClientLib = async () => {
if (typeof window === 'undefined') return null
const lib = await import('client-only-lib')
return lib
}
Best Practices
- Always use
readonly() for exposed state - Prevents accidental mutations
- Check
typeof window before localStorage - Prevents SSR errors
- Use
useState for cross-component state - Nuxt's SSR-safe alternative to global refs
- Wrap localStorage in try/catch - Handles quota errors and private browsing
- Provide TypeScript types for all returns - Better DX and error catching
- Use
isRef to handle both ref and raw values - More flexible composable APIs
- Dynamic imports for browser-only libs - html2canvas, jsPDF, etc.
Converted and distributed by TomeVault — claim your Tome and manage your conversions.