Skip to content

Commit

Permalink
Merge pull request #8 from AlexStack/nextjs14
Browse files Browse the repository at this point in the history
add timeout to getApiResponse
  • Loading branch information
AlexStack authored Jul 21, 2024
2 parents 660c1f6 + 0a525f7 commit 3d4e6bd
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 11 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ If you prefer Tailwind css, check this: [Tailwind-CSS-Version](https://github.co

## Demo

[<img src="./public/images/cover.png">](https://mui-nextjs-ts.vercel.app)
[<img src="https://alexstack.github.io/reactStarter/asset/NextJs14-mui5.gif">](https://mui-nextjs-ts.vercel.app)

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

Expand Down
2 changes: 2 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ const loadDataFromApi = async (slug?: string) => {
getApiResponse<NpmData>({
apiEndpoint: 'https://registry.npmjs.org/react/latest',
revalidate: 60 * 60 * 24, // 24 hours cache
timeout: 5000, // 5 seconds
}),
getApiResponse<NpmData>({
apiEndpoint: 'https://registry.npmjs.org/next/latest',
revalidate: 0, // no cache
timeout: 5000, // 5 seconds
}),
]);

Expand Down
85 changes: 76 additions & 9 deletions src/components/shared/DisplayRandomPicture.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/* eslint-disable @next/next/no-img-element */

'use client';
import { Send } from '@mui/icons-material';
import styled from '@emotion/styled';
import { Autorenew, Send } from '@mui/icons-material';
import { css, keyframes } from '@mui/material';
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';

Expand All @@ -12,42 +15,60 @@ import { useClientContext } from '@/hooks/useClientContext';

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

import { getApiResponse } from '@/utils/shared/get-api-response';

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

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

try {
const response = await fetch('https://picsum.photos/300/150');
if (!response.ok) {
throw new Error('Error fetching the image');
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');
}

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

setError(errorMsg);
setAlertBarProps({
message: 'Error fetching the image',
message: errorMsg,
severity: 'error',
});
setLoading(false);
} finally {
setLoading(false);
}
};

useEffect(() => {
if (renderCountRef.current === 0) {
if (renderCountRef.current === 0 && !loading) {
fetchRandomPicture();
}
renderCountRef.current += 1;
Expand All @@ -59,6 +80,7 @@ const DisplayRandomPicture = () => {
justifyContent='center'
alignItems='center'
spacing={2}
sx={{ position: 'relative', width: '300px', margin: '0 auto' }}
>
{error && <p>{error}</p>}
{imageUrl && (
Expand All @@ -71,7 +93,7 @@ const DisplayRandomPicture = () => {
)}
<div>
{loading && <span>Loading...</span>} Component Render Count:{' '}
{renderCountRef.current}
{renderCountRef.current + 1}
</div>

<SubmitButton
Expand All @@ -88,9 +110,54 @@ const DisplayRandomPicture = () => {
Get Another Picture
</Button>
</SubmitButton>
{imageUrl && (
<StyledRefreshButton onClick={fetchRandomPicture} loading={loading}>
<Avatar sx={{ width: 24, height: 24 }}>
<Autorenew />
</Avatar>
</StyledRefreshButton>
)}
{renderAlertBar()}
</Stack>
);
};

const spin = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const StyledRefreshButton = styled.div<{ loading?: boolean }>`
position: absolute;
right: 0;
top: 0;
margin: 0.5rem !important;
pointer-events: ${({ loading }) => (loading ? 'none' : 'auto')};
opacity: ${({ loading }) => (loading ? '0.6' : '1')};
cursor: ${({ loading }) => (loading ? 'not-allowed' : 'pointer')};
svg {
width: 20px;
height: 20px;
animation: ${({ loading }) =>
loading
? css`
${spin} 2s linear infinite
`
: 'none'};
}
:hover {
svg {
path {
fill: ${purple[500]};
}
}
.MuiAvatar-circular {
background-color: ${purple[50]};
}
}
`;

export default DisplayRandomPicture;
31 changes: 30 additions & 1 deletion src/utils/shared/get-api-response.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,48 @@
import { IS_PROD } from '@/constants';
import { consoleLog } from '@/utils/shared/console-log';

/**
* Makes an API request and returns the response data.
*
* @param apiEndpoint - The API endpoint URL.
* @param requestData - The request data to be sent in the request body.
* @param method - The HTTP method for the request (default: 'GET').
* @param revalidate - The time in seconds to cache the data (default: 3600 seconds in production, 120 seconds otherwise).
* @param headers - The headers to be included in the request.
* @param timeout - The timeout in milliseconds for the request (default: 100000 = 100 seconds).
* @returns The response data from the API.
* @throws An error if the API request fails or times out.
*/
export const getApiResponse = async <T>({
apiEndpoint,
requestData,
method = 'GET',
revalidate = IS_PROD ? 3600 : 120, // cache data in seconds
headers,
timeout = 100000, // 100 seconds
}: {
apiEndpoint: string;
requestData?: BodyInit;
method?: 'POST' | 'GET' | 'PUT' | 'DELETE';
revalidate?: number;
headers?: HeadersInit;
timeout?: number;
}) => {
try {
const startTime = Date.now();
const controller = new AbortController();
const signal = controller.signal;

const timeoutId = setTimeout(() => controller.abort(), timeout);

const response = await fetch(apiEndpoint, {
method,
body: requestData,
headers,
next: {
revalidate,
},
signal,
});
if (!response.ok) {
consoleLog('🚀 Debug getApiResponse requestData:', requestData);
Expand All @@ -38,9 +58,18 @@ export const getApiResponse = async <T>({
duration > 2000 ? '💔' : '-'
} ${apiEndpoint}`
);

clearTimeout(timeoutId);
// if is not valid JSON, return response
if (!response.headers.get('content-type')?.includes('application/json')) {
return response as T;
}
return (await response.json()) as T;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(
'Fetch request timed out: ' + (timeout / 1000).toFixed(1) + ' s'
);
}
consoleLog('getApiResponse error:', error);
}

Expand Down

0 comments on commit 3d4e6bd

Please sign in to comment.