-
Notifications
You must be signed in to change notification settings - Fork 26.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a self-hosted shared cache example
- Loading branch information
1 parent
d3756fa
commit eaee86a
Showing
15 changed files
with
619 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
.yarn/install-state.gz | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# Example of the self-hosted app with Redis as cache storage | ||
|
||
This example demonstrates using Redis as a shared cache for hosting multiple instances of a Next.js app. It supports caching at both the App and Pages routes in default and standalone modes, as well as Partial Pre-rendering. This functionality is made possible by the [`@neshca/cache-handler`](https://github.com/caching-tools/next-shared-cache/tree/canary/packages/cache-handler) package, which offers an API to customize cache handlers and seamlessly replace Next.js' default cache. | ||
|
||
This particular example is designed to be self-hosted. | ||
|
||
## How to use | ||
|
||
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: | ||
|
||
```bash | ||
npx create-next-app --example self-hosted-shared-cache self-hosted-shared-cache-app | ||
``` | ||
|
||
```bash | ||
yarn create next-app --example self-hosted-shared-cache self-hosted-shared-cache-app | ||
``` | ||
|
||
```bash | ||
pnpm create next-app --example self-hosted-shared-cache self-hosted-shared-cache-app | ||
``` | ||
|
||
Specify the Redis connection string using the `REDIS_URL` environment variable. This can be a URL or a connection string formatted as `redis[s]://[[username][:password]@][host][:port][/db-number]`. Learn more about connecting to Redis with Node.js [here](https://redis.io/docs/connect/clients/nodejs/). | ||
|
||
Once you have installed the dependencies, you can begin running the example Redis Stack server by using the following command: | ||
|
||
```bash | ||
docker-compose up -d | ||
``` | ||
|
||
Then, build and start the Next.js app as usual. | ||
|
||
## Notes | ||
|
||
Provided `docker-compose.yml` is for local development only. It is not suitable for production use. Read more about [Redis installation](https://redis.io/docs/install/) and [maganement](https://redis.io/docs/management/) before deploying your application to production. | ||
|
||
### Cache of your application lives outside of the Next.js app | ||
|
||
If you need to clear the Redis cache, use RedisInsight Workbench or run the command below: | ||
|
||
```bash | ||
docker exec -it redis-stack redis-cli | ||
127.0.0.1:6379> flushall | ||
OK | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
'use server' | ||
|
||
import { revalidateTag } from 'next/cache' | ||
|
||
export default async function revalidate() { | ||
revalidateTag('time-data') | ||
} |
50 changes: 50 additions & 0 deletions
50
examples/self-hosted-shared-cache/app/cache-state-watcher.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
'use client' | ||
|
||
import { useEffect, useState } from 'react' | ||
|
||
type CacheStateWatcherProps = { time: number; revalidateAfter: number } | ||
|
||
export function CacheStateWatcher({ | ||
time, | ||
revalidateAfter, | ||
}: CacheStateWatcherProps): JSX.Element { | ||
const [cacheState, setCacheState] = useState('') | ||
const [countDown, setCountDown] = useState('') | ||
|
||
useEffect(() => { | ||
let id = -1 | ||
|
||
function check(): void { | ||
const now = Date.now() | ||
|
||
setCountDown( | ||
Math.max(0, (time + revalidateAfter - now) / 1000).toFixed(3) | ||
) | ||
|
||
if (now > time + revalidateAfter) { | ||
setCacheState('stale') | ||
|
||
return | ||
} | ||
|
||
setCacheState('fresh') | ||
|
||
id = requestAnimationFrame(check) | ||
} | ||
|
||
id = requestAnimationFrame(check) | ||
|
||
return () => { | ||
cancelAnimationFrame(id) | ||
} | ||
}, [revalidateAfter, time]) | ||
|
||
return ( | ||
<> | ||
<div className={`cache-state ${cacheState}`}> | ||
Cache state: {cacheState} | ||
</div> | ||
<div className="stale-after">Stale after: {countDown}</div> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
*, | ||
*:before, | ||
*:after { | ||
box-sizing: border-box; | ||
} | ||
|
||
body, | ||
html { | ||
margin: 0; | ||
padding: 0; | ||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||
color: #333; | ||
line-height: 1.6; | ||
background-color: #f4f4f4; | ||
} | ||
|
||
.widget { | ||
background-color: #ffffff; | ||
border-radius: 8px; | ||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | ||
margin: 20px auto; | ||
padding: 20px; | ||
max-width: 600px; | ||
text-align: center; | ||
} | ||
|
||
.pre-rendered-at, | ||
.cache-state, | ||
.stale-after { | ||
font-size: 0.9em; | ||
color: #666; | ||
margin: 5px 0; | ||
} | ||
|
||
.cache-state.fresh { | ||
color: #4caf50; | ||
} | ||
|
||
.cache-state.stale { | ||
color: #f44336; | ||
} | ||
|
||
.revalidate-from { | ||
margin-top: 20px; | ||
} | ||
|
||
.revalidate-from-button { | ||
background-color: #008cba; | ||
color: white; | ||
border: none; | ||
border-radius: 4px; | ||
padding: 10px 20px; | ||
cursor: pointer; | ||
transition: background-color 0.3s ease; | ||
} | ||
|
||
.revalidate-from-button:hover { | ||
background-color: #005f73; | ||
} | ||
|
||
.revalidate-from-button:active { | ||
transform: translateY(2px); | ||
} | ||
|
||
.revalidate-from-button[aria-disabled='true'] { | ||
background-color: #ccc; | ||
cursor: not-allowed; | ||
} | ||
|
||
.footer { | ||
text-align: center; | ||
padding: 10px; | ||
position: relative; | ||
bottom: 0; | ||
width: 100%; | ||
color: white; | ||
} | ||
|
||
.external-link { | ||
color: #09f; | ||
text-decoration: none; | ||
transition: color 0.3s ease; | ||
} | ||
|
||
.external-link:hover { | ||
color: #07c; | ||
} | ||
|
||
@media (max-width: 768px) { | ||
.widget { | ||
width: 90%; | ||
margin: 20px auto; | ||
} | ||
|
||
.footer { | ||
padding: 20px; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import './global.css' | ||
|
||
export default function RootLayout({ | ||
children, | ||
}: { | ||
children: React.ReactNode | ||
}) { | ||
return ( | ||
<html lang="en"> | ||
<body>{children}</body> | ||
</html> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
'use server' | ||
|
||
import { notFound } from 'next/navigation' | ||
import { CacheStateWatcher } from './cache-state-watcher' | ||
import { formatTime } from '../utils/format-time' | ||
import { Suspense } from 'react' | ||
import { RevalidateFrom } from './revalidate-from' | ||
import Link from 'next/link' | ||
|
||
type TimeData = { | ||
unixtime: number | ||
} | ||
|
||
const revalidate = 10 | ||
|
||
export default async function Page() { | ||
const data = await fetch('https://worldtimeapi.org/api/timezone/UTC', { | ||
next: { revalidate, tags: ['time-data'] }, | ||
}) | ||
|
||
if (!data.ok) { | ||
notFound() | ||
} | ||
|
||
const timeData: TimeData = await data.json() | ||
|
||
const unixTimeMs = timeData.unixtime * 1000 | ||
|
||
return ( | ||
<> | ||
<main className="widget"> | ||
<div className="pre-rendered-at"> | ||
Pre-rendered at {formatTime(unixTimeMs)} | ||
</div> | ||
<Suspense fallback={null}> | ||
<CacheStateWatcher | ||
revalidateAfter={revalidate * 1000} | ||
time={unixTimeMs} | ||
/> | ||
</Suspense> | ||
<RevalidateFrom /> | ||
</main> | ||
<footer className="footer"> | ||
<Link | ||
href={process.env.NEXT_PUBLIC_REDIS_INSIGHT_URL} | ||
className="external-link" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
View RedisInsight ↪ | ||
</Link> | ||
</footer> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
'use client' | ||
|
||
import { useFormStatus } from 'react-dom' | ||
import revalidate from './actions' | ||
|
||
function SubmitButton() { | ||
const { pending } = useFormStatus() | ||
|
||
return ( | ||
<button | ||
className="revalidate-from-button" | ||
type="submit" | ||
aria-disabled={pending} | ||
> | ||
Revalidate | ||
</button> | ||
) | ||
} | ||
|
||
export function RevalidateFrom() { | ||
return ( | ||
<form className="revalidate-from" action={revalidate}> | ||
<SubmitButton /> | ||
</form> | ||
) | ||
} |
Oops, something went wrong.