Skip to content

Commit

Permalink
Merge pull request #9 from AlexStack/nextjs14
Browse files Browse the repository at this point in the history
Nextjs14
  • Loading branch information
AlexStack authored Aug 9, 2024
2 parents 3d4e6bd + 04654ce commit 7d8da1a
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 57 deletions.
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

0 comments on commit 7d8da1a

Please sign in to comment.