diff --git a/src/components/Homepage.tsx b/src/components/Homepage.tsx index f52e658..bfe643c 100644 --- a/src/components/Homepage.tsx +++ b/src/components/Homepage.tsx @@ -8,7 +8,7 @@ import DisplayRandomPicture from '@/components/shared/DisplayRandomPicture'; import PageFooter from '@/components/shared/PageFooter'; import ReactHookForm from '@/components/shared/ReactHookForm'; -import { SITE_CONFIG } from '@/constants'; +import { FETCH_API_CTX_VALUE, SITE_CONFIG } from '@/constants'; export default function Homepage({ reactVersion = 'unknown', @@ -66,7 +66,7 @@ export default function Homepage({ Test local NextJs API /api/test POST method (client-side component) - + diff --git a/src/components/shared/DisplayRandomPicture.tsx b/src/components/shared/DisplayRandomPicture.tsx index 6cb18d7..6d18a6b 100644 --- a/src/components/shared/DisplayRandomPicture.tsx +++ b/src/components/shared/DisplayRandomPicture.tsx @@ -15,13 +15,14 @@ import { useClientContext } from '@/hooks/useClientContext'; import SubmitButton from '@/components/shared/SubmitButton'; +import { FetchApiContext } from '@/constants'; import { getApiResponse } from '@/utils/shared/get-api-response'; const DisplayRandomPicture = () => { const [imageUrl, setImageUrl] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); - const { fetchCount, updateClientCtx } = useClientContext(); + const { fetchCount, updateClientCtx } = useClientContext(); const { setAlertBarProps, renderAlertBar } = useAlertBar(); const renderCountRef = React.useRef(0); @@ -92,7 +93,7 @@ const DisplayRandomPicture = () => { /> )}
- {loading && Loading...} Component Render Count:{' '} + {loading ? Loading... : null} Component Render Count:{' '} {renderCountRef.current + 1}
diff --git a/src/components/shared/ReactHookForm.tsx b/src/components/shared/ReactHookForm.tsx index 6ecd369..136f246 100644 --- a/src/components/shared/ReactHookForm.tsx +++ b/src/components/shared/ReactHookForm.tsx @@ -22,6 +22,7 @@ import useConfirmationDialog from '@/hooks/useConfirmDialog'; import SubmitButton from '@/components/shared/SubmitButton'; +import { FetchApiContext } from '@/constants'; import { consoleLog } from '@/utils/shared/console-log'; import { getApiResponse } from '@/utils/shared/get-api-response'; @@ -65,7 +66,7 @@ const ReactHookForm: React.FC = () => { resolver: zodResolver(zodSchema), }); - const { fetchCount, updateClientCtx } = useClientContext(); + const { fetchCount, updateClientCtx } = useClientContext(); const onSubmit: SubmitHandler = async (data) => { try { diff --git a/src/constants/context.ts b/src/constants/context.ts new file mode 100644 index 0000000..2393ff0 --- /dev/null +++ b/src/constants/context.ts @@ -0,0 +1,22 @@ +import { ReactNode } from 'react'; + +export interface FetchApiContext { + topError: ReactNode; + fetchCount: number; +} + +export const FETCH_API_CTX_VALUE: FetchApiContext = { + topError: null, + fetchCount: 0, +}; + +// You can add more context interface & values here and use them in different places +export interface AnotherContext { + someValue: string; + secondValue?: number; +} + +export const ANOTHER_CTX_VALUE: AnotherContext = { + someValue: 'default value', + secondValue: 0, +}; diff --git a/src/constants/index.ts b/src/constants/index.ts index 01e26b3..95b390a 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,2 +1,3 @@ export * from './config'; +export * from './context'; export * from './env'; diff --git a/src/hooks/useClientContext.test.tsx b/src/hooks/useClientContext.test.tsx index 1baca03..e8effb9 100644 --- a/src/hooks/useClientContext.test.tsx +++ b/src/hooks/useClientContext.test.tsx @@ -1,56 +1,84 @@ import { renderHook } from '@testing-library/react'; import React, { act } from 'react'; -import { ClientProvider, useClientContext } from './useClientContext'; +import { + ClientProvider, + OUTSIDE_CLIENT_PROVIDER_ERROR, + useClientContext, +} from './useClientContext'; describe('useClientContext', () => { it('should not be used outside ClientProvider', () => { - const { result } = renderHook(() => useClientContext()); - expect(() => { - result.current.updateClientCtx({ fetchCount: 66 }); - }).toThrow('Cannot be used outside ClientProvider'); + try { + renderHook(() => useClientContext()); + } catch (error) { + expect(error).toEqual(new Error(OUTSIDE_CLIENT_PROVIDER_ERROR)); + } }); it('should provide the correct initial context values', () => { + const defaultCtxValue = { + status: 'Pending', + topError: '', + fetchCount: 0, + }; const ctxValue = { topError: 'SWW Error', - bmStatus: 'Live', + status: 'Live', fetchCount: 85, }; const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + + {children} + ); - const { result } = renderHook(() => useClientContext(), { - wrapper, - }); + const { result } = renderHook( + () => useClientContext(), + { + wrapper, + } + ); expect(result.current.topError).toBe(ctxValue.topError); expect(result.current.fetchCount).toBe(ctxValue.fetchCount); }); it('should update the context values', () => { + const defaultCtxValue = { + picUrl: '', + loading: false, + total: 0, + }; const ctxValue = { - topError: 'SWW Error', - fetchCount: 85, + picUrl: 'https://picsum.photos/300/160', + loading: true, + total: 3, }; const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} + + {children} + ); - const { result } = renderHook(() => useClientContext(), { - wrapper, - }); + const { result } = renderHook( + () => useClientContext(), + { + wrapper, + } + ); const newCtxValue = { - topError: '', + picUrl: 'https://picsum.photos/200/150', + loading: false, }; act(() => { result.current.updateClientCtx(newCtxValue); }); - expect(result.current.topError).toBe(newCtxValue.topError); - expect(result.current.fetchCount).toBe(ctxValue.fetchCount); + expect(result.current.picUrl).toBe(newCtxValue.picUrl); + expect(result.current.total).toBe(ctxValue.total); // not updated + expect(result.current.loading).toBe(newCtxValue.loading); }); }); diff --git a/src/hooks/useClientContext.tsx b/src/hooks/useClientContext.tsx index 9fceb82..dd57532 100644 --- a/src/hooks/useClientContext.tsx +++ b/src/hooks/useClientContext.tsx @@ -1,52 +1,65 @@ 'use client'; -import React, { ReactNode, useCallback, useState } from 'react'; +import React, { + createContext, + ReactNode, + useCallback, + useContext, + useState, +} from 'react'; -export interface ClientContextData { - topError: ReactNode; - fetchCount: number; - updateClientCtx: (props: Partial) => void; +/** + * This is a generic custom hook for updating the client context + * It can be used in multiple places from any client-side component + * Please change the per-defined type & default value in constants/context.ts + */ + +export const OUTSIDE_CLIENT_PROVIDER_ERROR = + 'Cannot be used outside ClientProvider!'; + +export interface UpdateClientCtxType { + updateClientCtx: (props: Partial) => void; } -const CLIENT_CTX_VALUE: ClientContextData = { - topError: null, - fetchCount: 0, - updateClientCtx: () => { - // console.error('Cannot be used outside ClientProvider'); - throw new Error('Cannot be used outside ClientProvider'); - }, +export const ClientContext = createContext(undefined); + +export const useClientContext = (): T & UpdateClientCtxType => { + const context = useContext(ClientContext); + if (context === undefined) { + throw new Error(OUTSIDE_CLIENT_PROVIDER_ERROR); + } + + return context as T & UpdateClientCtxType; }; /** - * You should change the above interface and default value as per your requirement - * No need to change the below code + * You should pass the default value to the ClientProvider first + * e.g. * Client-side component usage example: - * const clientContext = useClientContext(); + * const clientContext = useClientContext(); * clientContext.updateClientCtx({ topError: 'Error message' }); - * clientContext.updateClientCtx({ totalRenderCount: 10 }); - * The total render count is: clientContext.totalRenderCount + * clientContext.updateClientCtx({ fetchCount: 10 }); + * The total fetch count is: clientContext.fetchCount */ -export const ClientContext = - React.createContext(CLIENT_CTX_VALUE); - -export const useClientContext = (): ClientContextData => { - const context = React.useContext(ClientContext); - if (!context) throw new Error('Cannot be used outside ClientProvider'); - - return context; -}; - -export const ClientProvider = ({ +export const ClientProvider = ({ children, - value = CLIENT_CTX_VALUE, + value, + defaultValue, }: { children: ReactNode; - value?: Partial; + value?: Partial; + defaultValue: T; }) => { - const [contextValue, setContextValue] = useState(value); + const [contextValue, setContextValue] = useState({ + ...defaultValue, + ...value, + updateClientCtx: (_: Partial): void => { + throw new Error(OUTSIDE_CLIENT_PROVIDER_ERROR); + }, + }); const updateContext = useCallback( - (newCtxValue: Partial) => { + (newCtxValue: Partial) => { setContextValue((prevContextValue) => ({ ...prevContextValue, ...newCtxValue, @@ -58,7 +71,6 @@ export const ClientProvider = ({ return (