Skip to content

Commit

Permalink
Merge pull request #6 from AlexStack/nextjs14
Browse files Browse the repository at this point in the history
feat: add hook useClientContext
  • Loading branch information
AlexStack authored Jul 19, 2024
2 parents ed9b155 + 08c8663 commit 660c1f6
Show file tree
Hide file tree
Showing 9 changed files with 2,274 additions and 1,838 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<h2>2024/2025: 🔋 NextJs 14.x + MUI 5.x + TypeScript Starter</h2>
<p>The scaffold for NextJs 14.x (App Router), React Hook Form, Material UI(MUI 5.x),Typescript and ESLint, and TypeScript with Absolute Import, Seo, Link component, pre-configured with Husky.</p>

<p>With simple example of NextJs API, React-hook-form with zod, fetch remote api, 404/500 error pages, MUI SSR usage, Styled component, MUI AlertBar, MUI confirmation dialog, Loading button, Client-side component</p>
<p>With simple example of NextJs API, React-hook-form with zod, fetch remote api, 404/500 error pages, MUI SSR usage, Styled component, MUI AlertBar, MUI confirmation dialog, Loading button, Client-side component & React Context update hook</p>

🚘🚘🚘 [**Click here to see an online demo**](https://mui-nextjs-ts.vercel.app) 🚘🚘🚘

Expand Down
23 changes: 0 additions & 23 deletions src/__tests__/pages/HomePage.test.tsx

This file was deleted.

38 changes: 23 additions & 15 deletions src/components/Homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import PinDropIcon from '@mui/icons-material/PinDrop';
import { Box, Typography } from '@mui/material';
import Link from 'next/link';

import { ClientProvider } from '@/hooks/useClientContext';

import DisplayRandomPicture from '@/components/shared/DisplayRandomPicture';
import PageFooter from '@/components/shared/PageFooter';
import ReactHookForm from '@/components/shared/ReactHookForm';

Expand Down Expand Up @@ -49,38 +52,43 @@ export default function Homepage({
</Box>
</Typography>

<Box sx={{ m: 5 }}>
<Box sx={{ m: 5, a: { color: 'blue' } }}>
<Link
href='https://github.com/AlexStack/nextjs-materia-mui-typescript-hook-form-scaffold-boilerplate-starter'
href='/api/test?from=github&nextjs=yes&mui=yes&tailwind=no'
target='_blank'
>
See the Github repository page
Test local NextJs API /api/test method GET with parameters
</Link>
</Box>

<Box sx={{ m: 5 }}>
<h4>
Test local NextJs API /api/test POST method (client-side
component)
</h4>
<ClientProvider>
<ReactHookForm />
<DisplayRandomPicture />
</ClientProvider>
</Box>

<Box sx={{ m: 5 }}>
<Link
href='https://vercel.com/new/clone?s=https%3A%2F%2Fgithub.com%2FAlexStack%2Fnextjs-materia-mui-typescript-hook-form-scaffold-boilerplate-starter&showOptionalTeamCreation=false'
href='https://github.com/AlexStack/nextjs-materia-mui-typescript-hook-form-scaffold-boilerplate-starter'
target='_blank'
>
Click here to deploy a demo site to your Vercel in 1 minute
See the Github repository page
</Link>
</Box>
<Box sx={{ m: 5, a: { color: 'blue' } }}>
<Box sx={{ m: 5, a: { color: 'red' } }}>
<Link
href='/api/test?from=github&nextjs=yes&mui=yes&tailwind=no'
href='https://vercel.com/new/clone?s=https%3A%2F%2Fgithub.com%2FAlexStack%2Fnextjs-materia-mui-typescript-hook-form-scaffold-boilerplate-starter&showOptionalTeamCreation=false'
target='_blank'
>
Test local NextJs API /api/test method GET with parameters
Click here to deploy a demo site to your Vercel in 1 minute
</Link>
</Box>

<Box sx={{ m: 5 }}>
<h4>
Test local NextJs API /api/tes POST method (client-side component)
</h4>
<ReactHookForm />
</Box>

<Box sx={{ m: 5 }}>
<Link href='/test-page-not-exists'>
Test 404 page not found (mock file not exists)
Expand Down
96 changes: 96 additions & 0 deletions src/components/shared/DisplayRandomPicture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* eslint-disable @next/next/no-img-element */

'use client';
import { Send } from '@mui/icons-material';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import React, { useEffect, useState } from 'react';

import { useAlertBar } from '@/hooks/useAlertBar';
import { useClientContext } from '@/hooks/useClientContext';

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

const DisplayRandomPicture = () => {
const [imageUrl, setImageUrl] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const { fetchCount, updateClientCtx } = useClientContext();
const { setAlertBarProps, renderAlertBar } = useAlertBar();
const renderCountRef = React.useRef(0);

const fetchRandomPicture = async () => {
setLoading(true);
setError('');

try {
const response = await fetch('https://picsum.photos/300/150');
if (!response.ok) {
throw new Error('Error fetching the image');
}
setImageUrl(response.url);
updateClientCtx({ fetchCount: fetchCount + 1 });
setAlertBarProps({
message: 'A random picture fetched successfully',
severity: 'info',
});
} catch (error) {
setError('Error fetching the image');
setAlertBarProps({
message: 'Error fetching the image',
severity: 'error',
});
} finally {
setLoading(false);
}
};

useEffect(() => {
if (renderCountRef.current === 0) {
fetchRandomPicture();
}
renderCountRef.current += 1;
});

return (
<Stack
direction='column'
justifyContent='center'
alignItems='center'
spacing={2}
>
{error && <p>{error}</p>}
{imageUrl && (
<Avatar
alt='DisplayRandomPicture'
src={imageUrl}
variant='square'
sx={{ width: 300, height: 150, borderRadius: '10px' }}
/>
)}
<div>
{loading && <span>Loading...</span>} Component Render Count:{' '}
{renderCountRef.current}
</div>

<SubmitButton
isSubmitting={loading}
submittingText='Fetching Picture ...'
>
<Button
variant='contained'
endIcon={<Send />}
onClick={fetchRandomPicture}
disabled={loading}
color='secondary'
>
Get Another Picture
</Button>
</SubmitButton>
{renderAlertBar()}
</Stack>
);
};

export default DisplayRandomPicture;
57 changes: 42 additions & 15 deletions src/components/shared/ReactHookForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,23 @@
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { NextPlan, Save } from '@mui/icons-material';
import { Box, Button, FormHelperText, TextField } from '@mui/material';
import React, { useEffect, useState } from 'react';
import {
Avatar,
Box,
Button,
FormHelperText,
Stack,
TextField,
} from '@mui/material';
import { purple } from '@mui/material/colors';
import React, { useEffect } from 'react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { z } from 'zod';

import { useAlertBar } from '@/hooks/useAlertBar';
import { useClientContext } from '@/hooks/useClientContext';
import useConfirmationDialog from '@/hooks/useConfirmDialog';

import { AlertBar, AlertBarProps } from '@/components/shared/AlertBar';
import SubmitButton from '@/components/shared/SubmitButton';

import { consoleLog } from '@/utils/shared/console-log';
Expand Down Expand Up @@ -42,12 +51,10 @@ const ReactHookForm: React.FC = () => {
const [apiResult, setApiResult] = React.useState<FormValues>();
const [isSubmitting, setIsSubmitting] = React.useState(false);

const [alertBarProps, setAlertBarProps] = useState<AlertBarProps>({
message: '',
severity: 'info',
});
const { setAlertBarProps, renderAlertBar } = useAlertBar();

const dialog = useConfirmationDialog();
const { openConfirmDialog, renderConfirmationDialog } =
useConfirmationDialog();

const {
handleSubmit,
Expand All @@ -58,6 +65,8 @@ const ReactHookForm: React.FC = () => {
resolver: zodResolver(zodSchema),
});

const { fetchCount, updateClientCtx } = useClientContext();

const onSubmit: SubmitHandler<FormValues> = async (data) => {
try {
setIsSubmitting(true);
Expand All @@ -77,6 +86,7 @@ const ReactHookForm: React.FC = () => {
message: 'Form submitted successfully',
severity: 'success',
});
updateClientCtx({ fetchCount: fetchCount + 1 });
} catch (error) {
consoleLog('handleSubmit ERROR', error);
setIsSubmitting(false);
Expand All @@ -95,7 +105,7 @@ const ReactHookForm: React.FC = () => {
autoHideSeconds: 4,
});
}
}, [isValid, errors]);
}, [isValid, errors, setAlertBarProps]);

return (
<StyledForm onSubmit={handleSubmit(onSubmit)}>
Expand Down Expand Up @@ -146,11 +156,31 @@ const ReactHookForm: React.FC = () => {
</SubmitButton>

<Box sx={{ m: 5 }}>
<Stack
sx={{ mb: 3 }}
direction='row'
spacing={1}
justifyContent='center'
alignItems='center'
>
<div>Total fetch count from React Context:</div>
<Avatar
sx={{
bgcolor: purple[500],
width: 22,
height: 22,
fontSize: '0.8rem',
}}
variant='circular'
>
{fetchCount}
</Avatar>
</Stack>
<Button
variant='outlined'
onClick={() => {
const randomNumber = Math.floor(Math.random() * 90) + 10;
dialog.openConfirmDialog({
openConfirmDialog({
title: 'Change form name',
content: `Are you sure to change above form name to Alex ${randomNumber} and submit?`,
onConfirm: () => {
Expand All @@ -166,12 +196,9 @@ const ReactHookForm: React.FC = () => {
</Button>
</Box>

<AlertBar
onClose={() => setAlertBarProps({ message: '' })}
{...alertBarProps}
/>
{renderAlertBar()}

{dialog.renderConfirmationDialog()}
{renderConfirmationDialog()}
</StyledForm>
);
};
Expand Down
24 changes: 24 additions & 0 deletions src/hooks/useAlertBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import React, { useState } from 'react';

import { AlertBar, AlertBarProps } from '@/components/shared/AlertBar';

export const useAlertBar = () => {
const [alertBarProps, setAlertBarProps] = useState<AlertBarProps>({
message: '',
severity: 'info',
});

const renderAlertBar = () => (
<AlertBar
onClose={() => setAlertBarProps({ message: '' })}
{...alertBarProps}
/>
);

return {
setAlertBarProps,
renderAlertBar,
};
};
56 changes: 56 additions & 0 deletions src/hooks/useClientContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { renderHook } from '@testing-library/react';
import React, { act } from 'react';

import { ClientProvider, 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');
});

it('should provide the correct initial context values', () => {
const ctxValue = {
topError: 'SWW Error',
bmStatus: 'Live',
fetchCount: 85,
};
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ClientProvider value={ctxValue}>{children}</ClientProvider>
);

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 ctxValue = {
topError: 'SWW Error',
fetchCount: 85,
};
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ClientProvider value={ctxValue}>{children}</ClientProvider>
);

const { result } = renderHook(() => useClientContext(), {
wrapper,
});

const newCtxValue = {
topError: '',
};

act(() => {
result.current.updateClientCtx(newCtxValue);
});

expect(result.current.topError).toBe(newCtxValue.topError);
expect(result.current.fetchCount).toBe(ctxValue.fetchCount);
});
});
Loading

0 comments on commit 660c1f6

Please sign in to comment.