Skip to content

Nextjs14 #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/Homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -66,7 +66,7 @@ export default function Homepage({
Test local NextJs API /api/test POST method (client-side
component)
</h4>
<ClientProvider>
<ClientProvider defaultValue={FETCH_API_CTX_VALUE}>
<ReactHookForm />
<DisplayRandomPicture />
</ClientProvider>
Expand Down
5 changes: 3 additions & 2 deletions src/components/shared/DisplayRandomPicture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FetchApiContext>();
const { setAlertBarProps, renderAlertBar } = useAlertBar();
const renderCountRef = React.useRef(0);

Expand Down Expand Up @@ -92,7 +93,7 @@ const DisplayRandomPicture = () => {
/>
)}
<div>
{loading && <span>Loading...</span>} Component Render Count:{' '}
{loading ? <span>Loading...</span> : null} Component Render Count:{' '}
{renderCountRef.current + 1}
</div>

Expand Down
3 changes: 2 additions & 1 deletion src/components/shared/ReactHookForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -65,7 +66,7 @@ const ReactHookForm: React.FC = () => {
resolver: zodResolver(zodSchema),
});

const { fetchCount, updateClientCtx } = useClientContext();
const { fetchCount, updateClientCtx } = useClientContext<FetchApiContext>();

const onSubmit: SubmitHandler<FormValues> = async (data) => {
try {
Expand Down
22 changes: 22 additions & 0 deletions src/constants/context.ts
Original file line number Diff line number Diff line change
@@ -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,
};
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './config';
export * from './context';
export * from './env';
66 changes: 47 additions & 19 deletions src/hooks/useClientContext.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<ClientProvider value={ctxValue}>{children}</ClientProvider>
<ClientProvider value={ctxValue} defaultValue={defaultCtxValue}>
{children}
</ClientProvider>
);

const { result } = renderHook(() => useClientContext(), {
wrapper,
});
const { result } = renderHook(
() => useClientContext<typeof defaultCtxValue>(),
{
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 }) => (
<ClientProvider value={ctxValue}>{children}</ClientProvider>
<ClientProvider value={ctxValue} defaultValue={defaultCtxValue}>
{children}
</ClientProvider>
);

const { result } = renderHook(() => useClientContext(), {
wrapper,
});
const { result } = renderHook(
() => useClientContext<typeof defaultCtxValue>(),
{
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);
});
});
78 changes: 45 additions & 33 deletions src/hooks/useClientContext.tsx
Original file line number Diff line number Diff line change
@@ -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<ClientContextData>) => 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<T> {
updateClientCtx: (props: Partial<T>) => 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<unknown | undefined>(undefined);

export const useClientContext = <T,>(): T & UpdateClientCtxType<T> => {
const context = useContext(ClientContext);
if (context === undefined) {
throw new Error(OUTSIDE_CLIENT_PROVIDER_ERROR);
}

return context as T & UpdateClientCtxType<T>;
};

/**
* 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. <ClientProvider defaultValue={FETCH_API_CTX_VALUE} value={dynamicValue}>
* Client-side component usage example:
* const clientContext = useClientContext();
* const clientContext = useClientContext<FetchApiContext>();
* 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<ClientContextData>(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 = <T,>({
children,
value = CLIENT_CTX_VALUE,
value,
defaultValue,
}: {
children: ReactNode;
value?: Partial<ClientContextData>;
value?: Partial<T>;
defaultValue: T;
}) => {
const [contextValue, setContextValue] = useState(value);
const [contextValue, setContextValue] = useState({
...defaultValue,
...value,
updateClientCtx: (_: Partial<T>): void => {
throw new Error(OUTSIDE_CLIENT_PROVIDER_ERROR);
},
});

const updateContext = useCallback(
(newCtxValue: Partial<ClientContextData>) => {
(newCtxValue: Partial<T>) => {
setContextValue((prevContextValue) => ({
...prevContextValue,
...newCtxValue,
Expand All @@ -58,7 +71,6 @@ export const ClientProvider = ({
return (
<ClientContext.Provider
value={{
...CLIENT_CTX_VALUE,
...contextValue,
updateClientCtx: updateContext,
}}
Expand Down
Loading