I was learning Zustand from the following youtube tutorial: https://www.youtube.com/watch?v=m41aGndJNPU. The guy has taught it in Javascript but I was doing it in Typescript by reading the Typescript specific docs for Zustand along with it.
The app is a simple course list, where you can add, remove and toggle the status of courses. In order to display the courses in a list form I grabbed the courses
, removeCourse
and ToggleCourse
actions from the store:
courseStore.ts:
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'
type Course = {
id: Number,
title: String,
completed: boolean,
}
interface StoreState {
courses: Course[],
addCourse: (course: Course) => void, // type of input it takes and type of return
removeCourse: (courseId: Number) => void,
toggleCourse: (courseId: Number) => void,
}
const useCourseStore = create<StoreState>()(
devtools(
persist(
(set) => ({
courses: [], //initial state
// actions
addCourse: (course) => {
set((state) => ({
courses: [course, ...state.courses] // corrected line
}))
},
removeCourse: (courseId) => {
set((state) => ({
courses: state.courses.filter((course) => course.id !== courseId) // corrected line
}))
},
toggleCourse: (courseId) => {
set((state) => ({
courses: state.courses.map((course) => course.id === courseId
? { ...course, completed: !course.completed }
: course
)
}))
}
}),
{ name: 'courseStore' },
),
),
)
export default useCourseStore;
courseList.tsx:
import React from 'react'
import useCourseStore from '../app/courseStore'
const CourseList = () => {
// const {courses, removeCourse, toggleCourse} = useCourseStore(
// (state) => ({
// courses: state.courses,
// removeCourse: state.removeCourse,
// toggleCourse: state.toggleCourse,
// })
// )
const courses = useCourseStore(state => state.courses)
const removeCourse = useCourseStore((state) => state.removeCourse)
const toggleCourse = useCourseStore((state) => state.toggleCourse)
return (
<>
<ul>
{courses.map((course, i) => {
return (
<React.Fragment key={i}>
<li
className={`course-item`}
style={{
backgroundColor: course.completed ? "#00FF0044" : "white"
}}
>
<span className="course-item-col-1">
<input
checked={course.completed}
type="checkbox"
onChange={() => {
toggleCourse(course.id)
}}
/>
</span>
<span>{course?.title}</span>
<button
onClick={() => {
removeCourse(course.id)
}}
className="delete-btn"
>
Delete
</button>
</li>
</React.Fragment>
)
})}
</ul>
</>
)
}
export default CourseList
In CourseList
when I used the commented out code to get the actions from the store, I got this error:
But when I use the code below the commented out code, it works just fine.
Can anyone explain why this happens?
I thought the error was due to not using useEffect
while getting the actions, but it seems without it, it works using the current implementation.
The root problem is, that you are always returning a new object from your selector ((state) => ({ ... }))
, which breaks reference equality every time.
So you have two solutions to fix this, either you split your selectors, as such:
const courses = useCourseStore((state) => state.courses)
const removeCourse = useCourseStore((state) => state.removeCourse)
const toggleCourse = useCourseStore((state) => state.toggleCourse)
Or alternatively, you use the shallow option so that multiple selected fields in a single object don’t trigger re-renders unless the contents really change.
E.g. as such:
import shallow from 'zustand/shallow'
const { courses, removeCourse, toggleCourse } = useCourseStore(
(state) => ({
courses: state.courses,
removeCourse: state.removeCourse,
toggleCourse: state.toggleCourse,
}),
shallow
)