From 837979e89a1efd3942cf86711f20e3c5e5b4adf1 Mon Sep 17 00:00:00 2001 From: Dustin Do Date: Tue, 16 Jul 2024 16:46:27 +0700 Subject: [PATCH] feat(mobile): update categories to use zustand store (#131) --- .../app/(app)/category/[categoryId].tsx | 39 ++--- apps/mobile/app/(app)/category/index.tsx | 16 +- .../app/(app)/category/new-category.tsx | 34 ++-- apps/mobile/lib/client.ts | 1 + apps/mobile/metro.config.js | 20 ++- apps/mobile/package.json | 4 +- apps/mobile/stores/category/hooks.tsx | 152 ++++++++++++++++++ apps/mobile/stores/category/queries.ts | 25 +++ apps/mobile/stores/category/store.ts | 41 +++++ pnpm-lock.yaml | 64 +++++++- 10 files changed, 323 insertions(+), 73 deletions(-) create mode 100644 apps/mobile/stores/category/hooks.tsx create mode 100644 apps/mobile/stores/category/queries.ts create mode 100644 apps/mobile/stores/category/store.ts diff --git a/apps/mobile/app/(app)/category/[categoryId].tsx b/apps/mobile/app/(app)/category/[categoryId].tsx index d74f4ff4..b1c007b1 100644 --- a/apps/mobile/app/(app)/category/[categoryId].tsx +++ b/apps/mobile/app/(app)/category/[categoryId].tsx @@ -1,34 +1,15 @@ import { CategoryForm } from '@/components/category/category-form' import { Text } from '@/components/ui/text' -import { updateCategory } from '@/mutations/category' -import { categoryQueries, useCategories } from '@/queries/category' -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useCategory, useUpdateCategory } from '@/stores/category/hooks' import { useLocalSearchParams, useRouter } from 'expo-router' -import { Alert, ScrollView, View } from 'react-native' +import { ScrollView, View } from 'react-native' export default function EditCategoryScreen() { - const { categoryId } = useLocalSearchParams<{ categoryId: string }>() - const { data: categories = [] } = useCategories() - const queryClient = useQueryClient() const router = useRouter() + const { categoryId } = useLocalSearchParams<{ categoryId: string }>() + const { category } = useCategory(categoryId!) - const { mutateAsync: mutateUpdate } = useMutation({ - mutationFn: updateCategory, - onError(error) { - Alert.alert(error.message) - }, - onSuccess() { - router.back() - }, - async onSettled() { - await queryClient.invalidateQueries({ - queryKey: categoryQueries.list._def, - }) - }, - throwOnError: true, - }) - - const category = categories.find((category) => category.id === categoryId) + const { mutateAsync: mutateUpdate } = useUpdateCategory() if (!category) { return ( @@ -39,9 +20,15 @@ export default function EditCategoryScreen() { } return ( - + mutateUpdate({ id: category.id, data: values })} + onSubmit={async (values) => { + mutateUpdate({ id: category.id, data: values }) + router.back() + }} hiddenFields={['type']} defaultValues={{ name: category?.name, diff --git a/apps/mobile/app/(app)/category/index.tsx b/apps/mobile/app/(app)/category/index.tsx index d406551d..2f6f3fb6 100644 --- a/apps/mobile/app/(app)/category/index.tsx +++ b/apps/mobile/app/(app)/category/index.tsx @@ -2,7 +2,7 @@ import { CategoryItem } from '@/components/category/category-item' import { AddNewButton } from '@/components/common/add-new-button' import { Skeleton } from '@/components/ui/skeleton' import { Text } from '@/components/ui/text' -import { useCategories } from '@/queries/category' +import { useCategoryList } from '@/stores/category/hooks' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' import { useRouter } from 'expo-router' @@ -12,16 +12,10 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context' export default function CategoriesScreen() { const { i18n } = useLingui() const router = useRouter() - const { data: categories = [], isLoading, refetch } = useCategories() + const { incomeCategories, expenseCategories, isRefetching, refetch } = + useCategoryList() const { bottom } = useSafeAreaInsets() - const incomeCategories = categories.filter( - (category) => category.type === 'INCOME', - ) - const expenseCategories = categories.filter( - (category) => category.type === 'EXPENSE', - ) - const sections = [ { key: 'INCOME', title: 'Incomes', data: incomeCategories }, { key: 'EXPENSE', title: 'Expenses', data: expenseCategories }, @@ -31,7 +25,7 @@ export default function CategoriesScreen() { item.id} @@ -42,7 +36,7 @@ export default function CategoriesScreen() { renderSectionFooter={({ section }) => ( <> {!section.data.length && - (isLoading ? ( + (isRefetching ? ( <> diff --git a/apps/mobile/app/(app)/category/new-category.tsx b/apps/mobile/app/(app)/category/new-category.tsx index 5287d117..3cdb5f20 100644 --- a/apps/mobile/app/(app)/category/new-category.tsx +++ b/apps/mobile/app/(app)/category/new-category.tsx @@ -1,35 +1,27 @@ import { CategoryForm } from '@/components/category/category-form' -import { createCategory } from '@/mutations/category' -import { categoryQueries } from '@/queries/category' -import type { CategoryTypeType } from '@6pm/validation' -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useCreateCategory } from '@/stores/category/hooks' +import type { CategoryFormValues, CategoryTypeType } from '@6pm/validation' +import { createId } from '@paralleldrive/cuid2' import { useLocalSearchParams, useRouter } from 'expo-router' -import { Alert, View } from 'react-native' +import { View } from 'react-native' export default function CreateCategoryScreen() { const router = useRouter() const { type = 'EXPENSE' } = useLocalSearchParams<{ type?: CategoryTypeType }>() - const queryClient = useQueryClient() - const { mutateAsync } = useMutation({ - mutationFn: createCategory, - onError(error) { - Alert.alert(error.message) - }, - onSuccess() { - router.back() - }, - async onSettled() { - await queryClient.invalidateQueries({ - queryKey: categoryQueries.list._def, - }) - }, - }) + const { mutateAsync } = useCreateCategory() + + const handleCreate = async (data: CategoryFormValues) => { + mutateAsync({ data, id: createId() }).catch(() => { + // ignore + }) + router.back() + } return ( - + ) } diff --git a/apps/mobile/lib/client.ts b/apps/mobile/lib/client.ts index 0b13be6a..b1334217 100644 --- a/apps/mobile/lib/client.ts +++ b/apps/mobile/lib/client.ts @@ -24,6 +24,7 @@ export const queryClient = new QueryClient({ queries: { networkMode: 'offlineFirst', gcTime: 1000 * 60 * 60 * 24 * 7, // 1 week + staleTime: 1000 * 60 * 60 * 24, // 1 day }, }, }) diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index 34392745..279b0fcf 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -1,12 +1,12 @@ -const { getDefaultConfig } = require('expo/metro-config'); -const { withNativeWind } = require('nativewind/metro'); +const { getDefaultConfig } = require("expo/metro-config"); +const { withNativeWind } = require("nativewind/metro"); -const path = require('path'); +const path = require("path"); // Find the project and workspace directories const projectRoot = __dirname; // This can be replaced with `find-yarn-workspace-root` -const monorepoRoot = path.resolve(projectRoot, '../..'); +const monorepoRoot = path.resolve(projectRoot, "../.."); const config = getDefaultConfig(projectRoot); @@ -14,11 +14,17 @@ const config = getDefaultConfig(projectRoot); config.watchFolders = [monorepoRoot]; // 2. Let Metro know where to resolve packages and in what order config.resolver.nodeModulesPaths = [ - path.resolve(projectRoot, 'node_modules'), - path.resolve(monorepoRoot, 'node_modules'), + path.resolve(projectRoot, "node_modules"), + path.resolve(monorepoRoot, "node_modules"), +]; + +config.resolver.unstable_conditionNames = [ + "browser", + "require", + "react-native", ]; config.resolver.unstable_enableSymlinks = true; config.resolver.unstable_enablePackageExports = true; -module.exports = withNativeWind(config, { input: './global.css' }); +module.exports = withNativeWind(config, { input: "./global.css" }); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index ca6a04da..be329431 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -30,6 +30,7 @@ "@lingui/macro": "^4.11.1", "@lingui/react": "^4.11.1", "@lukemorales/query-key-factory": "^1.3.4", + "@paralleldrive/cuid2": "^2.2.2", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", @@ -78,7 +79,8 @@ "react-native-web": "~0.19.10", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zustand": "^4.5.4" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/apps/mobile/stores/category/hooks.tsx b/apps/mobile/stores/category/hooks.tsx new file mode 100644 index 00000000..960eff88 --- /dev/null +++ b/apps/mobile/stores/category/hooks.tsx @@ -0,0 +1,152 @@ +import { getHonoClient } from '@/lib/client' +import { useMeQuery } from '@/queries/auth' +import { + type Category, + type CategoryFormValues, + CategorySchema, +} from '@6pm/validation' +import { createId } from '@paralleldrive/cuid2' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { keyBy, omit } from 'lodash-es' +import { useMemo } from 'react' +import { z } from 'zod' +import { categoryQueries } from './queries' +import { useCategoryStore } from './store' + +export const useCategoryList = () => { + const categories = useCategoryStore().categories + const setCategoriesState = useCategoryStore((state) => state.setCategories) + + const query = useQuery({ + ...categoryQueries.all({ setCategoriesState }), + initialData: categories, + }) + + const { categoriesDict, incomeCategories, expenseCategories } = + useMemo(() => { + const categoriesDict = keyBy(categories, 'id') + const incomeCategories = categories.filter( + (category) => category.type === 'INCOME', + ) + const expenseCategories = categories.filter( + (category) => category.type === 'EXPENSE', + ) + + return { + categoriesDict, + incomeCategories, + expenseCategories, + } + }, [categories]) + + return { + ...query, + categories, + categoriesDict, + incomeCategories, + expenseCategories, + } +} + +export const useCategory = (categoryId: string) => { + const categories = useCategoryStore().categories + const category: Category | null = useMemo( + () => categories.find((category) => category.id === categoryId) || null, + [categories, categoryId], + ) + + return { category } +} + +export const useUpdateCategory = () => { + const updateCategoryInStore = useCategoryStore( + (state) => state.updateCategory, + ) + const { categoriesDict } = useCategoryList() + const queryClient = useQueryClient() + + const mutation = useMutation( + { + mutationFn: async ({ + id, + data, + }: { id: string; data: CategoryFormValues }) => { + const hc = await getHonoClient() + const result = await hc.v1.categories[':categoryId'].$put({ + param: { categoryId: id }, + json: omit(data, 'type'), // prevent updating category type + }) + + if (result.ok) { + const category = CategorySchema.parse(await result.json()) + return category + } + + throw result + }, + onMutate({ id, data }) { + let category = categoriesDict[id] + if (!category) { + return + } + + category = { ...category, ...data, updatedAt: new Date() } + + updateCategoryInStore(category) + + return category + }, + }, + queryClient, + ) + + return mutation +} + +export const useCreateCategory = () => { + const { data: userData } = useMeQuery() + const updateCategoryInStore = useCategoryStore( + (state) => state.updateCategory, + ) + + const mutation = useMutation({ + mutationFn: async ({ + id = createId(), + data, + }: { id?: string; data: CategoryFormValues }) => { + const hc = await getHonoClient() + const result = await hc.v1.categories.$post({ + json: { id, ...data }, + }) + + if (result.ok) { + const json = await result.json() + const category = CategorySchema.extend({ + id: z.string(), + }).parse(json) + return category + } + + throw result + }, + onMutate({ id, data }) { + const category: Category = { + id: id!, + createdAt: new Date(), + updatedAt: new Date(), + parentId: null, + userId: userData?.id || '', + description: '', + color: '', + icon: '', + ...data, + } + + updateCategoryInStore(category) + + return category + }, + }) + + return mutation +} diff --git a/apps/mobile/stores/category/queries.ts b/apps/mobile/stores/category/queries.ts new file mode 100644 index 00000000..817f1da2 --- /dev/null +++ b/apps/mobile/stores/category/queries.ts @@ -0,0 +1,25 @@ +import { getHonoClient } from '@/lib/client' +import { type Category, CategorySchema } from '@6pm/validation' +import { createQueryKeys } from '@lukemorales/query-key-factory' + +export const categoryQueries = createQueryKeys('categories', { + all: ({ + setCategoriesState, + }: { setCategoriesState: (categories: Category[]) => void }) => ({ + queryKey: [{}], + queryFn: async () => { + const hc = await getHonoClient() + const res = await hc.v1.categories.$get() + if (!res.ok) { + throw new Error(await res.text()) + } + + const items = await res.json() + const categories = items.map((item) => CategorySchema.parse(item)) + + setCategoriesState(categories) + + return categories + }, + }), +}) diff --git a/apps/mobile/stores/category/store.ts b/apps/mobile/stores/category/store.ts new file mode 100644 index 00000000..f946a69b --- /dev/null +++ b/apps/mobile/stores/category/store.ts @@ -0,0 +1,41 @@ +import type { Category } from '@6pm/validation' +import AsyncStorage from '@react-native-async-storage/async-storage' +import { orderBy, uniqBy } from 'lodash-es' +import { create } from 'zustand' +import { createJSONStorage, persist } from 'zustand/middleware' + +interface CategoryStore { + categories: Category[] + setCategories: (categories: Category[]) => void + updateCategory: (category: Category) => void +} + +function normalizeCategories(categories: Category[]) { + return orderBy(uniqBy(categories, 'id'), 'name') +} + +export const useCategoryStore = create()( + persist( + (set) => ({ + categories: [], + setCategories: (categories: Category[]) => + set({ categories: normalizeCategories(categories) }), + updateCategory: (category: Category) => + set((state) => { + const index = state.categories.findIndex((c) => c.id === category.id) + if (index === -1) { + return { + categories: normalizeCategories([...state.categories, category]), + } + } + + state.categories[index] = category + return { categories: normalizeCategories(state.categories) } + }), + }), + { + name: 'category-storage', + storage: createJSONStorage(() => AsyncStorage), + }, + ), +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index daf535fe..a4a4af09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,6 +138,9 @@ importers: '@lukemorales/query-key-factory': specifier: ^1.3.4 version: 1.3.4(@tanstack/query-core@5.51.1)(@tanstack/react-query@5.45.1(react@18.3.1)) + '@paralleldrive/cuid2': + specifier: ^2.2.2 + version: 2.2.2 '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.3.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -218,7 +221,7 @@ importers: version: 15.0.3(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) expo-router: specifier: ~3.5.15 - version: 3.5.16(expo-constants@16.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-linking@6.3.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-modules-autolinking@1.11.1)(expo-status-bar@1.12.1)(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)))(react-native-reanimated@3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-screens@3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(typescript@5.3.3) + version: 3.5.16(4mulymdxn7fmzuvb6n5wc7wvuq) expo-secure-store: specifier: ^13.0.1 version: 13.0.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) @@ -285,6 +288,9 @@ importers: zod: specifier: ^3.23.8 version: 3.23.8 + zustand: + specifier: ^4.5.4 + version: 4.5.4(@types/react@18.2.79)(react@18.3.1) devDependencies: '@babel/core': specifier: ^7.20.0 @@ -1101,7 +1107,7 @@ packages: '@backpackapp-io/react-native-toast@0.11.0': resolution: {integrity: sha512-ZRVYQPK6QOvt6vP1bF0I5oBpN5pnssdrX1JLV+KHPyxXWSNNvl1oo0qdJ5uzM7zj2TxMMUBPIsMofFeLA8E/dw==} peerDependencies: - react: 18.2.0 + react: '*' react-native: '*' react-native-gesture-handler: '>=2.2.1' react-native-reanimated: '>=2.8.0' @@ -1539,7 +1545,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-native': '*' - react: 18.2.0 + react: '*' react-native: '*' react-native-gesture-handler: '>=1.10.1' react-native-reanimated: '>=2.2.0' @@ -1804,6 +1810,10 @@ packages: cpu: [x64] os: [win32] + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1820,6 +1830,9 @@ packages: resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@paralleldrive/cuid2@2.2.2': + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2184,7 +2197,7 @@ packages: resolution: {integrity: sha512-plhc8UvCZs0UkV+sI+3bisIyn78wz9O/BiWZXpounu72k/R/Sj5PuZYFJ1fi6psvriUveMCGh4LeZckAZu2qiQ==} peerDependencies: '@react-navigation/native': ^6.0.0 - react: 18.2.0 + react: '*' react-native: '*' react-native-safe-area-context: '>= 3.0.0' @@ -2200,7 +2213,7 @@ packages: '@react-navigation/native@6.1.17': resolution: {integrity: sha512-mer3OvfwWOHoUSMJyLa4vnBH3zpFmCwuzrBPlw7feXklurr/ZDiLjLxUScOot6jLRMz/67GyilEYMmP99LL0RQ==} peerDependencies: - react: 18.2.0 + react: '*' react-native: '*' '@react-navigation/routers@6.1.9': @@ -6337,6 +6350,11 @@ packages: peerDependencies: react: '>=16.8' + use-sync-external-store@1.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-sync-external-store@1.2.2: resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: @@ -6599,6 +6617,21 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zustand@4.5.4: + resolution: {integrity: sha512-/BPMyLKJPtFEvVL0E9E9BTUM63MNyhPGlvxk1XjrfWTUlV+BR8jufjsovHzrtR6YNcBEcL7cMHovL1n9xHawEg==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -8646,6 +8679,8 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.4': optional: true + '@noble/hashes@1.4.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8662,6 +8697,10 @@ snapshots: dependencies: semver: 7.6.2 + '@paralleldrive/cuid2@2.2.2': + dependencies: + '@noble/hashes': 1.4.0 + '@pkgjs/parseargs@0.11.0': optional: true @@ -10681,8 +10720,8 @@ snapshots: dependencies: invariant: 2.2.4 - ? expo-router@3.5.16(expo-constants@16.0.2(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-linking@6.3.1(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-modules-autolinking@1.11.1)(expo-status-bar@1.12.1)(expo@51.0.14(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)))(react-native-reanimated@3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-screens@3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(typescript@5.3.3) - : dependencies: + expo-router@3.5.16(4mulymdxn7fmzuvb6n5wc7wvuq): + dependencies: '@expo/metro-runtime': 3.2.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.3.1)) '@expo/server': 0.4.3(typescript@5.3.3) '@radix-ui/react-slot': 1.0.1(react@18.3.1) @@ -13938,6 +13977,10 @@ snapshots: dependencies: react: 18.3.1 + use-sync-external-store@1.2.0(react@18.3.1): + dependencies: + react: 18.3.1 + use-sync-external-store@1.2.2(react@18.3.1): dependencies: react: 18.3.1 @@ -14178,3 +14221,10 @@ snapshots: zod: 3.23.8 zod@3.23.8: {} + + zustand@4.5.4(@types/react@18.2.79)(react@18.3.1): + dependencies: + use-sync-external-store: 1.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.79 + react: 18.3.1