refactor: Add custom hooks for context access in various contexts
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled

This commit is contained in:
2025-12-14 20:24:18 -08:00
parent 8274f4c871
commit 82b7ce82e7
21 changed files with 78 additions and 42 deletions

View File

@@ -1,5 +1,5 @@
// src/contexts/AuthContext.tsx
import React, { createContext, useState, useEffect, useCallback, ReactNode } from 'react';
import React, { createContext, useState, useEffect, useCallback, ReactNode, useContext } from 'react';
import type { User, UserProfile } from '../types';
import * as apiClient from '../services/apiClient';
import { useApi } from '../hooks/useApi';
@@ -120,4 +120,17 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const value = { user, profile, authStatus, isLoading, login, logout, updateProfile };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
/**
* Custom hook to access the authentication context.
* This is what components will use to get auth state and methods.
* It also ensures that it's used within an AuthProvider.
*/
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -1,5 +1,5 @@
// src/contexts/FlyersContext.tsx
import React, { createContext, ReactNode } from 'react';
import React, { createContext, ReactNode, useContext } from 'react';
import type { Flyer } from '../types';
import * as apiClient from '../services/apiClient';
import { useInfiniteQuery } from '../hooks/useInfiniteQuery';
@@ -38,4 +38,12 @@ export const FlyersProvider: React.FC<{ children: ReactNode }> = ({ children })
};
return <FlyersContext.Provider value={value}>{children}</FlyersContext.Provider>;
};
export const useFlyers = (): FlyersContextType => {
const context = useContext(FlyersContext);
if (context === undefined) {
throw new Error('useFlyers must be used within a FlyersProvider');
}
return context;
};

View File

@@ -1,5 +1,5 @@
// src/contexts/MasterItemsContext.tsx
import React, { createContext, ReactNode, useMemo } from 'react';
import React, { createContext, ReactNode, useMemo, useContext } from 'react';
import type { MasterGroceryItem } from '../types';
import * as apiClient from '../services/apiClient';
import { useApiOnMount } from '../hooks/useApiOnMount';
@@ -24,4 +24,12 @@ export const MasterItemsProvider: React.FC<{ children: ReactNode }> = ({ childre
}), [data, loading, error]);
return <MasterItemsContext.Provider value={value}>{children}</MasterItemsContext.Provider>;
};
export const useMasterItems = (): MasterItemsContextType => {
const context = useContext(MasterItemsContext);
if (context === undefined) {
throw new Error('useMasterItems must be used within a MasterItemsProvider');
}
return context;
};

View File

@@ -1,5 +1,5 @@
// src/contexts/ModalContext.tsx
import React, { createContext, useState, useMemo, useCallback } from 'react';
import React, { createContext, useState, useMemo, useCallback, useContext } from 'react';
/**
* Defines the names of all modals used in the application.
@@ -40,4 +40,16 @@ export const ModalProvider: React.FC<{ children: React.ReactNode }> = ({ childre
const value = useMemo(() => ({ openModal, closeModal, isModalOpen }), [openModal, closeModal, isModalOpen]);
return <ModalContext.Provider value={value}>{children}</ModalContext.Provider>;
};
/**
* The custom hook that components will use to access the modal context.
* It provides a clean and simple API for interacting with modals.
*/
export const useModal = (): ModalContextType => {
const context = useContext(ModalContext);
if (context === undefined) {
throw new Error('useModal must be used within a ModalProvider');
}
return context;
};

View File

@@ -1,9 +1,9 @@
// src/contexts/UserDataContext.tsx
import React, { createContext, useState, useEffect, useMemo, ReactNode } from 'react';
import React, { createContext, useState, useEffect, useMemo, ReactNode, useContext } from 'react';
import type { MasterGroceryItem, ShoppingList } from '../types';
import * as apiClient from '../services/apiClient';
import { useApiOnMount } from '../hooks/useApiOnMount';
import { useAuth } from '../hooks/useAuth';
import { useAuth } from './AuthContext';
export interface UserDataContextType {
watchedItems: MasterGroceryItem[];
@@ -54,4 +54,12 @@ export const UserDataProvider: React.FC<{ children: ReactNode }> = ({ children }
}), [watchedItems, shoppingLists, user, isLoadingWatched, isLoadingShoppingLists, watchedItemsError, shoppingListsError]);
return <UserDataContext.Provider value={value}>{children}</UserDataContext.Provider>;
};
export const useUserData = (): UserDataContextType => {
const context = useContext(UserDataContext);
if (context === undefined) {
throw new Error('useUserData must be used within a UserDataProvider');
}
return context;
};

0
src/contexts/useAuth.tsx Normal file
View File

View File

View File

View File

View File

View File

@@ -2,8 +2,8 @@
import { renderHook, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { useActiveDeals } from './useActiveDeals';
import * as apiClient from '../services/apiClient';
import { useFlyers } from './useFlyers';
import * as apiClient from '../services/apiClient'; // Correct path
import { useFlyers } from '../contexts/FlyersContext';
import { useUserData } from './useUserData';
import type { Flyer, MasterGroceryItem, FlyerItem, DealItem } from '../types';
@@ -11,7 +11,7 @@ import type { Flyer, MasterGroceryItem, FlyerItem, DealItem } from '../types';
const mockedApiClient = vi.mocked(apiClient);
// Mock the new data provider hooks
vi.mock('./useFlyers');
vi.mock('../contexts/FlyersContext');
vi.mock('./useUserData');
// Create typed mocks for easier usage

View File

@@ -1,6 +1,6 @@
// src/hooks/useActiveDeals.tsx
import { useEffect, useMemo } from 'react';
import { useFlyers } from './useFlyers';
import { useFlyers } from '../contexts/FlyersContext';
import { useUserData } from './useUserData';
import { useApi } from './useApi';
import type { FlyerItem, DealItem } from '../types';

View File

@@ -1,8 +1,7 @@
import React, { ReactNode } from 'react';
import { renderHook, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { useAuth } from './useAuth';
import { AuthProvider } from '../contexts/AuthContext';
import { AuthProvider, useAuth } from '../contexts/AuthContext';
import * as apiClient from '../services/apiClient';
import type { User, UserProfile } from '../types';

View File

@@ -2,8 +2,7 @@
import React, { ReactNode } from 'react';
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useFlyers } from './useFlyers';
import { FlyersProvider } from '../contexts/FlyersContext';
import { FlyersProvider, useFlyers } from '../contexts/FlyersContext';
import { useInfiniteQuery } from './useInfiniteQuery';
import type { Flyer } from '../types';

View File

@@ -2,8 +2,7 @@
import React, { ReactNode } from 'react';
import { renderHook } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useMasterItems } from './useMasterItems';
import { MasterItemsProvider } from '../contexts/MasterItemsContext';
import { MasterItemsProvider, useMasterItems } from '../contexts/MasterItemsContext';
import { useApiOnMount } from './useApiOnMount';
import type { MasterGroceryItem } from '../types';

View File

@@ -2,8 +2,7 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import React from 'react';
import { useModal } from './useModal';
import { ModalProvider } from '../contexts/ModalContext';
import { ModalProvider, useModal } from '../contexts/ModalContext';
// Create a wrapper component that includes the ModalProvider.
// This is necessary because the useModal hook depends on the context provided by ModalProvider.

View File

@@ -1,6 +1,6 @@
// src/hooks/useShoppingLists.tsx
import { useState, useCallback, useEffect, useMemo } from 'react';
import { useAuth } from './useAuth';
import { useAuth } from '../contexts/AuthContext';
import { useUserData } from './useUserData';
import { useApi } from './useApi';
import * as apiClient from '../services/apiClient';

View File

@@ -2,14 +2,13 @@
import React, { ReactNode } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useUserData } from './useUserData';
import { UserDataProvider } from '../contexts/UserDataContext';
import { useAuth } from './useAuth';
import { UserDataProvider, useUserData } from '../contexts/UserDataContext';
import { useAuth } from '../contexts/AuthContext';
import { useApiOnMount } from './useApiOnMount';
import type { MasterGroceryItem, ShoppingList, UserProfile } from '../types';
// 1. Mock the hook's dependencies
vi.mock('./useAuth');
vi.mock('../contexts/AuthContext');
vi.mock('./useApiOnMount');
// 2. Create typed mocks for type safety and autocompletion

View File

@@ -2,14 +2,14 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useWatchedItems } from './useWatchedItems';
import { useApi } from './useApi';
import { useAuth } from './useAuth';
import { useApi } from '../hooks/useApi';
import { useAuth } from '../contexts/AuthContext';
import { useUserData } from './useUserData';
import type { MasterGroceryItem, User } from '../types';
// Mock the hooks that useWatchedItems depends on
vi.mock('./useApi');
vi.mock('./useAuth');
vi.mock('../hooks/useApi');
vi.mock('../contexts/AuthContext');
vi.mock('./useUserData');
// The apiClient is globally mocked in our test setup, so we just need to cast it
@@ -35,8 +35,8 @@ describe('useWatchedItems Hook', () => {
// Provide a default implementation for useApi
mockedUseApi
.mockReturnValueOnce({ execute: mockAddWatchedItemApi, error: null, data: null, loading: false, isRefetching: false })
.mockReturnValueOnce({ execute: mockRemoveWatchedItemApi, error: null, data: null, loading: false, isRefetching: false });
.mockReturnValueOnce({ execute: mockAddWatchedItemApi, error: null, data: null, loading: false, isRefetching: false, reset: vi.fn() })
.mockReturnValueOnce({ execute: mockRemoveWatchedItemApi, error: null, data: null, loading: false, isRefetching: false, reset: vi.fn() });
// Provide a default implementation for the mocked hooks
@@ -98,6 +98,7 @@ describe('useWatchedItems Hook', () => {
data: null,
loading: false,
isRefetching: false,
reset: vi.fn(),
});
const { result } = renderHook(() => useWatchedItems());
@@ -134,8 +135,8 @@ describe('useWatchedItems Hook', () => {
it('should set an error message if the API call fails', async () => {
// Re-mock useApi for this specific error test case
mockedUseApi.mockReturnValueOnce({ execute: vi.fn(), error: null, data: null, loading: false, isRefetching: false }) // for add
.mockReturnValueOnce({ execute: vi.fn(), error: new Error('Deletion Failed'), data: null, loading: false, isRefetching: false }); // for remove
mockedUseApi.mockReturnValueOnce({ execute: vi.fn(), error: null, data: null, loading: false, isRefetching: false, reset: vi.fn() }) // for add
.mockReturnValueOnce({ execute: vi.fn(), error: new Error('Deletion Failed'), data: null, loading: false, isRefetching: false, reset: vi.fn() }); // for remove
const { result } = renderHook(() => useWatchedItems());

View File

@@ -1,6 +1,6 @@
// src/hooks/useWatchedItems.tsx
import { useMemo, useCallback } from 'react';
import { useAuth } from './useAuth';
import { useAuth } from '../contexts/AuthContext';
import { useApi } from './useApi';
import { useUserData } from './useUserData';
import * as apiClient from '../services/apiClient';

Some files were not shown because too many files have changed in this diff Show More