Skip to content

NextJs15 with React19 #10

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 5 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
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# NextJs 14.x + MUI 5.x + React Hook Form + TypeScript Starter and Boilerplate
# ReactJs 19.x + NextJs 15.x + MUI 6.x + TypeScript Starter and Boilerplate

<div align="center">
<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>
<h2>2024/2025: 🔋 ReactJs 19.x + NextJs 15.x + MUI 6.x + TypeScript Starter</h2>
<p>The scaffold for NextJs 15.x (App Router), React Hook Form, Material UI(MUI 6.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 & React Context update hook</p>
<p>With simple example of ReactJs 19.x, NextJs 15.x 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 All @@ -18,12 +18,19 @@ If you prefer Tailwind css, check this: [Tailwind-CSS-Version](https://github.co

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

## Clone this repository for React 19.x with NextJs 15.x or React 18.x with NextJs 14.x

- Clone React19-Next15-MUI6-TS-Starter:
- `git clone -b react19-nextjs15 https://github.com/AlexStack/nextjs-materia-mui-typescript-hook-form-scaffold-boilerplate-starter.git react19-nextjs15-mui6-ts-starter`
- Clone React18-Next14-MUI5-TS-Starter:
- `git clone -b nextjs14 https://github.com/AlexStack/nextjs-materia-mui-typescript-hook-form-scaffold-boilerplate-starter.git react18-nextjs14-mui5-ts-starter`

## Features

This repository is 🔋 battery packed with:

- ⚡️ Next.js 14.x with App Router
- ⚛️ React 18.x
- ⚡️ Next.js 15.x with App Router
- ⚛️ React 19.x
- ✨ TypeScript
- 💨 Material UI — Ready to use Material Design components [check here for the usage](https://mui.com/material-ui/getting-started/usage/)
- 🎨 React Hook Form — Performant, flexible and extensible forms with easy-to-use validation
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nextjs-materia-mui-typescript-hook-form-scaffold-boilerplate-starter",
"version": "0.1.0",
"name": "react19-nextjs15-materia-mui6-typescript-hook-form-scaffold-boilerplate-starter",
"version": "2.0",
"private": true,
"scripts": {
"dev": "next dev",
Expand All @@ -24,9 +24,9 @@
"@mui/icons-material": "^5.14.9",
"@mui/material": "^5.14.10",
"dayjs": "^1.11.10",
"next": "^14.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"next": "^15.0.0-rc.0",
"react": "^19.0.0-rc-512b09b2-20240718",
"react-dom": "^19.0.0-rc-512b09b2-20240718",
"react-hook-form": "^7.46.2",
"react-icons": "^4.10.1",
"zod": "^3.22.4"
Expand All @@ -42,7 +42,7 @@
"@typescript-eslint/parser": "^5.62.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-config-next": "^14.0.1",
"eslint-config-next": "^15.0.0-rc.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
Expand Down
4 changes: 2 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ const loadDataFromApi = async (slug?: string) => {
// Fetch & cache data from 2 remote APIs test
const [reactNpmData, nextJsNpmData] = await Promise.all([
getApiResponse<NpmData>({
apiEndpoint: 'https://registry.npmjs.org/react/latest',
apiEndpoint: 'https://registry.npmjs.org/react/rc',
revalidate: 60 * 60 * 24, // 24 hours cache
timeout: 5000, // 5 seconds
}),
getApiResponse<NpmData>({
apiEndpoint: 'https://registry.npmjs.org/next/latest',
apiEndpoint: 'https://registry.npmjs.org/next/rc',
revalidate: 0, // no cache
timeout: 5000, // 5 seconds
}),
Expand Down
10 changes: 6 additions & 4 deletions src/components/Homepage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import PinDropIcon from '@mui/icons-material/PinDrop';
import AutoAwesome from '@mui/icons-material/AutoAwesome';
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 ReactActionForm from '@/components/shared/ReactActionForm';
import ReactHookForm from '@/components/shared/ReactHookForm';

import { FETCH_API_CTX_VALUE, SITE_CONFIG } from '@/constants';
Expand All @@ -18,7 +19,7 @@ export default function Homepage({
<main>
<section>
<Box sx={{ textAlign: 'center' }}>
<PinDropIcon
<AutoAwesome
className='page-title'
sx={{ width: '3rem', height: '3rem' }}
/>
Expand All @@ -44,8 +45,8 @@ export default function Homepage({
sx={{ color: 'green', mt: 3 }}
>
Fetch & cache data from 2 remote APIs test: <br />
The latest React version is {reactVersion}, and the latest NextJs
version is {nextJsVersion}
The React RC version is {reactVersion}, and the NextJs RC version is{' '}
{nextJsVersion}
<Box sx={{ color: 'grey', fontSize: 10 }}>
On dev environment, you can see how long it takes on console, e.g.
getApiResponse: 0.05s
Expand All @@ -67,6 +68,7 @@ export default function Homepage({
component)
</h4>
<ClientProvider defaultValue={FETCH_API_CTX_VALUE}>
<ReactActionForm />
<ReactHookForm />
<DisplayRandomPicture />
</ClientProvider>
Expand Down
91 changes: 47 additions & 44 deletions src/components/shared/DisplayRandomPicture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import { purple } from '@mui/material/colors';
import Stack from '@mui/material/Stack';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useTransition } from 'react';

import { useAlertBar } from '@/hooks/useAlertBar';
import { useClientContext } from '@/hooks/useClientContext';
Expand All @@ -20,56 +20,56 @@ 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<FetchApiContext>();
const { setAlertBarProps, renderAlertBar } = useAlertBar();
const renderCountRef = React.useRef(0);
const [isPending, startTransition] = useTransition();

const fetchRandomPicture = async () => {
if (loading) {
setAlertBarProps({
message: 'Please wait for the current fetch to complete',
severity: 'warning',
});
return;
}
setLoading(true);
setError('');
startTransition(async () => {
if (isPending) {
setAlertBarProps({
message: 'Please wait for the current fetch to complete',
severity: 'warning',
});
return;
}
setError('');

try {
const response = await getApiResponse<Response & { url: string }>({
apiEndpoint: 'https://picsum.photos/300/160',
timeout: 5001,
});
try {
const response = await getApiResponse<Response & { url: string }>({
apiEndpoint: 'https://picsum.photos/300/160',
timeout: 5001,
});

if (!response?.url) {
throw new Error('Error fetching the image, no response url');
}
if (!response?.url) {
throw new Error('Error fetching the image, no response url');
}

setImageUrl(response.url);
updateClientCtx({ fetchCount: fetchCount + 1 });
setAlertBarProps({
message: 'A random picture fetched successfully',
severity: 'info',
});
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : 'Error fetching the image';
setImageUrl(response.url);
updateClientCtx({ fetchCount: fetchCount + 1 });
setAlertBarProps({
message: 'A random picture fetched successfully',
severity: 'info',
});
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : 'Error fetching the image';

setError(errorMsg);
setAlertBarProps({
message: errorMsg,
severity: 'error',
});
setLoading(false);
} finally {
setLoading(false);
}
setError(errorMsg);
setAlertBarProps({
message: errorMsg,
severity: 'error',
});
} finally {
// setLoading(false);
}
});
};

useEffect(() => {
if (renderCountRef.current === 0 && !loading) {
if (renderCountRef.current === 0 && !isPending) {
fetchRandomPicture();
}
renderCountRef.current += 1;
Expand All @@ -83,7 +83,6 @@ const DisplayRandomPicture = () => {
spacing={2}
sx={{ position: 'relative', width: '300px', margin: '0 auto' }}
>
{error && <p>{error}</p>}
{imageUrl && (
<Avatar
alt='DisplayRandomPicture'
Expand All @@ -92,27 +91,31 @@ const DisplayRandomPicture = () => {
sx={{ width: 300, height: 150, borderRadius: '10px' }}
/>
)}
{error && <p>{error}</p>}
<div>
{loading ? <span>Loading...</span> : null} Component Render Count:{' '}
{isPending && <span>Loading...</span>} Component Render Count:{' '}
{renderCountRef.current + 1}
</div>

<SubmitButton
isSubmitting={loading}
isSubmitting={isPending}
submittingText='Fetching Picture ...'
>
<Button
variant='contained'
endIcon={<Send />}
onClick={fetchRandomPicture}
disabled={loading}
disabled={isPending}
color='secondary'
>
Get Another Picture
</Button>
</SubmitButton>
{imageUrl && (
<StyledRefreshButton onClick={fetchRandomPicture} loading={loading}>
<StyledRefreshButton
onClick={fetchRandomPicture}
loading={isPending ? 1 : 0}
>
<Avatar sx={{ width: 24, height: 24 }}>
<Autorenew />
</Avatar>
Expand All @@ -131,7 +134,7 @@ const spin = keyframes`
transform: rotate(360deg);
}
`;
const StyledRefreshButton = styled.div<{ loading?: boolean }>`
const StyledRefreshButton = styled.div<{ loading: number }>`
position: absolute;
right: 0;
top: 0;
Expand Down
Loading
Loading