Please bear with me 🙂 I am working on a small nextjs hobby app and wanted to test zustand library with it. I managed to use next-auth with firestore adapter and I have a session created and also can access in my client page using getSession.
Now, I took this Zustand and created userStore where I wanted to let user modify their address and profile. For that I created UserState associated User type and some mutation methods like updateUser, setUser, addAddress, removeAddress,updateAddress etc. (showing only few lines of code)
And on client component I am doing something like below :
'use client'
import useUserStore from '@/lib/store/useUserStore'
import { validateName } from '@/utils/validations'
import { CalendarDate } from '@internationalized/date'
import { DateInput, Image } from '@nextui-org/react'
import { useSession } from 'next-auth/react'
import { ChangeEvent, useEffect, useState } from 'react'
const AccountComponent: React.FC = () => {
const { data: session, status } = useSession()
const { user, setUser, updateUser, uploadImage } = useUserStore()
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [birthdate, setBirthdate] = useState(new CalendarDate(1995, 11, 6))
const [imagePreview, setImagePreview] = useState(session?.user?.image)
const [email, setEmail] = useState('')
const [errors, setErrors] = useState<{ [key: string]: string }>({})
const [feedbackMessage, setFeedbackMessage] = useState<string | null>(null)
useEffect(() => {
if (session?.user) {
const user = session?.user
setUser(user)
const [firstName, lastName] = user?.name?.split(' ')
setFirstName(firstName)
setLastName(lastName)
setEmail(user.email)
}
}, [])
const handleSave = async () => {
const newErrors = {
firstName: validateName(firstName),
lastName: validateName(lastName),
}
if (Object.values(newErrors).some((error) => error)) {
setErrors(newErrors)
return
}
try {
await updateUser({
name: `${firstName} ${lastName}`,
email,
dateOfBirth: birthdate.toString(),
// Add other fields to update here
})
setFeedbackMessage('Profile updated successfully.')
} catch (error) {
setFeedbackMessage('Failed to update profile. Please try again.')
}
// Clear feedback message after 3 seconds
setTimeout(() => setFeedbackMessage(null), 3000)
}
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
const imageUrl = await uploadImage(file)
setImagePreview(imageUrl)
console.log('Uploaded Image URL:', imageUrl)
}
}
And below is my useUserStore code
import { Address, User } from '@/models/User'
import { collection, doc, getDoc, getDocs, updateDoc } from 'firebase/firestore'
import { create } from 'zustand'
import { db } from '../firebaseConfig'
interface UserState {
user: User | null
setUser: (user: User) => void
addAddress: (address: Address) => Promise<void>
updateAddress: (
alias: string,
updatedAddress: Partial<Address>
) => Promise<void>
removeAddress: (alias: string) => Promise<void>
loadUser: (userId: string) => Promise<void>
}
interface UserState {
user: User | null
setUser: (user: User) => void
addAddress: (address: Address) => Promise<void>
updateAddress: (
alias: string,
updatedAddress: Partial<Address>
) => Promise<void>
removeAddress: (alias: string) => Promise<void>
loadUser: (userId: string) => Promise<void>
updateUser: (updatedFields: Partial<User>) => Promise<void>
uploadImage: (file: File) => Promise<string>
clearUser: () => void
}
const useUserStore = create<UserState>((set) => ({
user: null,
setUser: (user) => set({ user }),
addAddress: async (address) => {
const { user } = useUserStore.getState()
if (!user) return
const updatedAddresses = [...(user.savedAddresses || []), address]
const userDocRef = doc(db, 'users', user.id)
await updateDoc(userDocRef, {
savedAddresses: updatedAddresses,
})
set((state) => ({
user: {
...state.user,
savedAddresses: updatedAddresses,
} as User,
}))
},
updateAddress: async (alias, updatedAddress) => {
const { user } = useUserStore.getState()
if (!user) return
const updatedAddresses = user.savedAddresses.map((addr) =>
addr.alias === alias ? { ...addr, ...updatedAddress } : addr
)
const userDocRef = doc(db, 'users', user.id)
await updateDoc(userDocRef, {
savedAddresses: updatedAddresses,
})
set((state) => ({
user: {
...state.user,
savedAddresses: updatedAddresses,
} as User,
}))
},
removeAddress: async (alias) => {
const { user } = useUserStore.getState()
if (!user) return
const updatedAddresses = user.savedAddresses.filter(
(addr) => addr.alias !== alias
)
const userDocRef = doc(db, 'users', user.id)
await updateDoc(userDocRef, {
savedAddresses: updatedAddresses,
})
set((state) => ({
user: {
...state.user,
savedAddresses: updatedAddresses,
} as User,
}))
},
loadUser: async (userId) => {
if (!userId) {
throw new Error('User ID is required to load user data.')
}
const userDocRef = doc(db, 'users', userId)
const userDoc = await getDoc(userDocRef)
const savedAddressesCollectionRef = collection(userDocRef, 'savedAddresses')
if (userDoc.exists()) {
const userData = userDoc.data() as User
userData.id = userDoc.id
const savedAddressesSnapshot = await getDocs(savedAddressesCollectionRef)
if (!savedAddressesSnapshot.empty) {
const savedAddresses = savedAddressesSnapshot.docs.map(
(doc) => doc.data() as Address
)
userData.savedAddresses = savedAddresses
}
console.log('User document found:', userData)
set({ user: userData })
} else {
console.warn('User document does not exist.')
set({ user: null })
}
},
updateUser: async (updatedFields) => {
console.log('Updating user:', updatedFields)
const { user } = useUserStore.getState()
if (!user?.id) {
console.log('User not found')
return
}
const userDocRef = doc(db, 'users', user.id)
await updateDoc(userDocRef, updatedFields)
set((state) => ({
user: {
...state.user,
...updatedFields,
} as User,
}))
},
uploadImage: async (file) => {
const { user } = useUserStore.getState()
if (!user) throw new Error('User not found')
const formData = new FormData()
formData.append('userId', user.id)
formData.append('file', file)
const response = await fetch('/api/uploadProfileImage', {
method: 'POST',
body: formData,
})
if (!response.ok) {
throw new Error('Failed to upload image')
}
const { url } = await response.json()
await useUserStore.getState().updateUser({ image: url })
return url
},
clearUser: () => set({ user: null }),
}))
export default useUserStore
Now the question is as I am not trying yet with multiple users logged in and changing their data. But you think this code is thread-safe and also user session safe. I read that zustand creates a global state and in the browser (client side) only one instance of store will be available.
So if although we initialize userStore with current user session it is still available for any user to initialize with their own session, thus corrupting changes from one session to other, or is it not correct ?
If there is issue in the code and its not the proper way to mutate user state please let me know. thanks