Skip to content

Commit a8030de

Browse files
committed
Merge remote-tracking branch 'origin/main' into react19-nextjs15
2 parents 3d1005f + 7d8da1a commit a8030de

File tree

7 files changed

+121
-56
lines changed

7 files changed

+121
-56
lines changed

src/components/Homepage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import PageFooter from '@/components/shared/PageFooter';
99
import ReactActionForm from '@/components/shared/ReactActionForm';
1010
import ReactHookForm from '@/components/shared/ReactHookForm';
1111

12-
import { SITE_CONFIG } from '@/constants';
12+
import { FETCH_API_CTX_VALUE, SITE_CONFIG } from '@/constants';
1313

1414
export default function Homepage({
1515
reactVersion = 'unknown',
@@ -67,7 +67,7 @@ export default function Homepage({
6767
Test local NextJs API /api/test POST method (client-side
6868
component)
6969
</h4>
70-
<ClientProvider>
70+
<ClientProvider defaultValue={FETCH_API_CTX_VALUE}>
7171
<ReactActionForm />
7272
<ReactHookForm />
7373
<DisplayRandomPicture />

src/components/shared/DisplayRandomPicture.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import { useClientContext } from '@/hooks/useClientContext';
1515

1616
import SubmitButton from '@/components/shared/SubmitButton';
1717

18+
import { FetchApiContext } from '@/constants';
1819
import { getApiResponse } from '@/utils/shared/get-api-response';
1920

2021
const DisplayRandomPicture = () => {
2122
const [imageUrl, setImageUrl] = useState('');
2223
const [error, setError] = useState('');
23-
const { fetchCount, updateClientCtx } = useClientContext();
24+
const { fetchCount, updateClientCtx } = useClientContext<FetchApiContext>();
2425
const { setAlertBarProps, renderAlertBar } = useAlertBar();
2526
const renderCountRef = React.useRef(0);
2627
const [isPending, startTransition] = useTransition();

src/components/shared/ReactHookForm.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import useConfirmationDialog from '@/hooks/useConfirmDialog';
2222

2323
import SubmitButton from '@/components/shared/SubmitButton';
2424

25+
import { FetchApiContext } from '@/constants';
2526
import { consoleLog } from '@/utils/shared/console-log';
2627
import { getApiResponse } from '@/utils/shared/get-api-response';
2728

@@ -65,7 +66,7 @@ const ReactHookForm: React.FC = () => {
6566
resolver: zodResolver(zodSchema),
6667
});
6768

68-
const { fetchCount, updateClientCtx } = useClientContext();
69+
const { fetchCount, updateClientCtx } = useClientContext<FetchApiContext>();
6970

7071
const onSubmit: SubmitHandler<FormValues> = async (data) => {
7172
try {

src/constants/context.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ReactNode } from 'react';
2+
3+
export interface FetchApiContext {
4+
topError: ReactNode;
5+
fetchCount: number;
6+
}
7+
8+
export const FETCH_API_CTX_VALUE: FetchApiContext = {
9+
topError: null,
10+
fetchCount: 0,
11+
};
12+
13+
// You can add more context interface & values here and use them in different places
14+
export interface AnotherContext {
15+
someValue: string;
16+
secondValue?: number;
17+
}
18+
19+
export const ANOTHER_CTX_VALUE: AnotherContext = {
20+
someValue: 'default value',
21+
secondValue: 0,
22+
};

src/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './config';
2+
export * from './context';
23
export * from './env';

src/hooks/useClientContext.test.tsx

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,84 @@
11
import { renderHook } from '@testing-library/react';
22
import React, { act } from 'react';
33

4-
import { ClientProvider, useClientContext } from './useClientContext';
4+
import {
5+
ClientProvider,
6+
OUTSIDE_CLIENT_PROVIDER_ERROR,
7+
useClientContext,
8+
} from './useClientContext';
59

610
describe('useClientContext', () => {
711
it('should not be used outside ClientProvider', () => {
8-
const { result } = renderHook(() => useClientContext());
9-
expect(() => {
10-
result.current.updateClientCtx({ fetchCount: 66 });
11-
}).toThrow('Cannot be used outside ClientProvider');
12+
try {
13+
renderHook(() => useClientContext());
14+
} catch (error) {
15+
expect(error).toEqual(new Error(OUTSIDE_CLIENT_PROVIDER_ERROR));
16+
}
1217
});
1318

1419
it('should provide the correct initial context values', () => {
20+
const defaultCtxValue = {
21+
status: 'Pending',
22+
topError: '',
23+
fetchCount: 0,
24+
};
1525
const ctxValue = {
1626
topError: 'SWW Error',
17-
bmStatus: 'Live',
27+
status: 'Live',
1828
fetchCount: 85,
1929
};
2030
const wrapper = ({ children }: { children: React.ReactNode }) => (
21-
<ClientProvider value={ctxValue}>{children}</ClientProvider>
31+
<ClientProvider value={ctxValue} defaultValue={defaultCtxValue}>
32+
{children}
33+
</ClientProvider>
2234
);
2335

24-
const { result } = renderHook(() => useClientContext(), {
25-
wrapper,
26-
});
36+
const { result } = renderHook(
37+
() => useClientContext<typeof defaultCtxValue>(),
38+
{
39+
wrapper,
40+
}
41+
);
2742

2843
expect(result.current.topError).toBe(ctxValue.topError);
2944
expect(result.current.fetchCount).toBe(ctxValue.fetchCount);
3045
});
3146

3247
it('should update the context values', () => {
48+
const defaultCtxValue = {
49+
picUrl: '',
50+
loading: false,
51+
total: 0,
52+
};
3353
const ctxValue = {
34-
topError: 'SWW Error',
35-
fetchCount: 85,
54+
picUrl: 'https://picsum.photos/300/160',
55+
loading: true,
56+
total: 3,
3657
};
3758
const wrapper = ({ children }: { children: React.ReactNode }) => (
38-
<ClientProvider value={ctxValue}>{children}</ClientProvider>
59+
<ClientProvider value={ctxValue} defaultValue={defaultCtxValue}>
60+
{children}
61+
</ClientProvider>
3962
);
4063

41-
const { result } = renderHook(() => useClientContext(), {
42-
wrapper,
43-
});
64+
const { result } = renderHook(
65+
() => useClientContext<typeof defaultCtxValue>(),
66+
{
67+
wrapper,
68+
}
69+
);
4470

4571
const newCtxValue = {
46-
topError: '',
72+
picUrl: 'https://picsum.photos/200/150',
73+
loading: false,
4774
};
4875

4976
act(() => {
5077
result.current.updateClientCtx(newCtxValue);
5178
});
5279

53-
expect(result.current.topError).toBe(newCtxValue.topError);
54-
expect(result.current.fetchCount).toBe(ctxValue.fetchCount);
80+
expect(result.current.picUrl).toBe(newCtxValue.picUrl);
81+
expect(result.current.total).toBe(ctxValue.total); // not updated
82+
expect(result.current.loading).toBe(newCtxValue.loading);
5583
});
5684
});

src/hooks/useClientContext.tsx

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,65 @@
11
'use client';
22

3-
import React, { ReactNode, useCallback, useState } from 'react';
3+
import React, {
4+
createContext,
5+
ReactNode,
6+
useCallback,
7+
useContext,
8+
useState,
9+
} from 'react';
410

5-
export interface ClientContextData {
6-
topError: ReactNode;
7-
fetchCount: number;
8-
updateClientCtx: (props: Partial<ClientContextData>) => void;
11+
/**
12+
* This is a generic custom hook for updating the client context
13+
* It can be used in multiple places from any client-side component
14+
* Please change the per-defined type & default value in constants/context.ts
15+
*/
16+
17+
export const OUTSIDE_CLIENT_PROVIDER_ERROR =
18+
'Cannot be used outside ClientProvider!';
19+
20+
export interface UpdateClientCtxType<T> {
21+
updateClientCtx: (props: Partial<T>) => void;
922
}
1023

11-
const CLIENT_CTX_VALUE: ClientContextData = {
12-
topError: null,
13-
fetchCount: 0,
14-
updateClientCtx: () => {
15-
// console.error('Cannot be used outside ClientProvider');
16-
throw new Error('Cannot be used outside ClientProvider');
17-
},
24+
export const ClientContext = createContext<unknown | undefined>(undefined);
25+
26+
export const useClientContext = <T,>(): T & UpdateClientCtxType<T> => {
27+
const context = useContext(ClientContext);
28+
if (context === undefined) {
29+
throw new Error(OUTSIDE_CLIENT_PROVIDER_ERROR);
30+
}
31+
32+
return context as T & UpdateClientCtxType<T>;
1833
};
1934

2035
/**
21-
* You should change the above interface and default value as per your requirement
22-
* No need to change the below code
36+
* You should pass the default value to the ClientProvider first
37+
* e.g. <ClientProvider defaultValue={FETCH_API_CTX_VALUE} value={dynamicValue}>
2338
* Client-side component usage example:
24-
* const clientContext = useClientContext();
39+
* const clientContext = useClientContext<FetchApiContext>();
2540
* clientContext.updateClientCtx({ topError: 'Error message' });
26-
* clientContext.updateClientCtx({ totalRenderCount: 10 });
27-
* The total render count is: clientContext.totalRenderCount
41+
* clientContext.updateClientCtx({ fetchCount: 10 });
42+
* The total fetch count is: clientContext.fetchCount
2843
*/
29-
export const ClientContext =
30-
React.createContext<ClientContextData>(CLIENT_CTX_VALUE);
31-
32-
export const useClientContext = (): ClientContextData => {
33-
const context = React.useContext(ClientContext);
34-
if (!context) throw new Error('Cannot be used outside ClientProvider');
35-
36-
return context;
37-
};
38-
39-
export const ClientProvider = ({
44+
export const ClientProvider = <T,>({
4045
children,
41-
value = CLIENT_CTX_VALUE,
46+
value,
47+
defaultValue,
4248
}: {
4349
children: ReactNode;
44-
value?: Partial<ClientContextData>;
50+
value?: Partial<T>;
51+
defaultValue: T;
4552
}) => {
46-
const [contextValue, setContextValue] = useState(value);
53+
const [contextValue, setContextValue] = useState({
54+
...defaultValue,
55+
...value,
56+
updateClientCtx: (_: Partial<T>): void => {
57+
throw new Error(OUTSIDE_CLIENT_PROVIDER_ERROR);
58+
},
59+
});
4760

4861
const updateContext = useCallback(
49-
(newCtxValue: Partial<ClientContextData>) => {
62+
(newCtxValue: Partial<T>) => {
5063
setContextValue((prevContextValue) => ({
5164
...prevContextValue,
5265
...newCtxValue,
@@ -58,7 +71,6 @@ export const ClientProvider = ({
5871
return (
5972
<ClientContext.Provider
6073
value={{
61-
...CLIENT_CTX_VALUE,
6274
...contextValue,
6375
updateClientCtx: updateContext,
6476
}}

0 commit comments

Comments
 (0)