Skip to content
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

feat: compatibility with latest versions of React and Next #55

Merged
merged 29 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
25008c6
refactor(server.tsx): organize imports, remove some anys, adapt types
Aug 3, 2023
1b92401
chore: update non-critical deps
Aug 3, 2023
71d5d1a
test: move client and server tests in different folders with their ow…
Aug 4, 2023
a7e38a0
chore: update react packages and their types, add ts-node for jest ts…
Aug 4, 2023
c245e23
refactor: use renderToPipeableStream instead of deprecated renderToNo…
Aug 4, 2023
f011e29
chore: update deps, add exports field in package.json, update eslint …
Aug 10, 2023
cdb303a
fix(createIncludeElement): fix react hydration error
Aug 10, 2023
d844a66
test: adapt and fix tests
Aug 10, 2023
42a0e84
docs(WIP): add express and next + express example
Aug 10, 2023
9fba89c
docs(examples/express): cleanup, formating, improve setup, add compose
Aug 11, 2023
b7bd742
docs(examples/next): formating, fixed next version
Aug 11, 2023
d888d6a
refactor(lib): make browser detection bundler agnostic
Aug 11, 2023
49fd7a0
chore(lib): add missing exports in package.json, add dist and yarn-er…
Aug 11, 2023
9570f2f
docs(readme): adapt examples, update and add links, add line breaks
Aug 11, 2023
ff9cf72
refactor(lib): bring back original createIncludeElement behavior
Aug 15, 2023
014e843
chore: update prettier and eslint config and scripts, format/fix, upd…
Aug 15, 2023
cebd391
refactor: review fixes
Aug 17, 2023
2bd766d
refactor: more review fixes
Aug 17, 2023
fe76b95
chore: cleanup default.vcl configs and fix varnish compose images ver…
Sep 5, 2023
569522d
docs: add readme to express example, update next and main readme
Sep 5, 2023
c933f03
docs: improve examples readme
Sep 5, 2023
d4da2c0
chore(examples): update deps
Sep 6, 2023
5fe117d
refactor(lib): don't escape attributes as react do it by itself now
Sep 6, 2023
60f53d7
refactor(express): use default export for App and improve server html
Sep 6, 2023
e587da0
Fix some style issues in README.md
dunglas Jan 3, 2024
1a0179c
Update compose.yaml
dunglas Jan 3, 2024
9bcb5a1
EOF in default.vcl
dunglas Jan 3, 2024
828fbf5
Update compose.yaml
dunglas Jan 3, 2024
18b55ed
Fix indentation in default.vcl
dunglas Jan 3, 2024
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
9 changes: 6 additions & 3 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"prettier"
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"ecmaFeatures": {
"jsx": true
},
Expand All @@ -26,5 +28,6 @@
"react": {
"version": "detect"
}
}
},
"ignorePatterns": ["node_modules/", "dist/", ".next/", "lib/"]
}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/lib
/node_modules
node_modules
/coverage
yarn-error.log
dist
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**/.git
**/.svn
**/.hg
dunglas marked this conversation as resolved.
Show resolved Hide resolved
**/node_modules
**/.next
150 changes: 95 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,29 @@
[![MIT Licence](https://badges.frapsoft.com/os/mit/mit.svg?v=103)](https://opensource.org/licenses/mit-license.php)

React ESI is a super powerful cache library for vanilla [React](https://reactjs.org/) and [Next.js](https://nextjs.org/) applications, that can make highly dynamic applications as fast as static sites.
It provides a straightforward way to boost your application's performance by storing **fragments** of server-side rendered pages in **edge cache servers**.
It means that after the first rendering, fragments of your pages will be served in a few milliseconds by servers close to your end users!

It provides a straightforward way to boost your application's performance by storing **fragments** of server-side rendered pages in **edge cache servers**.
It means that after the first rendering, fragments of your pages will be served in a few milliseconds by servers close to your end users!
It's a very efficient way to improve the performance and the SEO of your websites and to dramatically reduce both your hosting costs and the energy consumption of these applications. Help the planet, use React ESI!

Because it is built on top of the [Edge Side Includes (ESI)](https://www.w3.org/TR/esi-lang) W3C specification,
React ESI natively supports most of the well-known cloud cache providers including [Cloudflare Workers](https://blog.cloudflare.com/edge-side-includes-with-cloudflare-workers/), [Akamai](https://www.akamai.com/us/en/support/esi.jsp) and [Fastly](https://docs.fastly.com/guides/performance-tuning/using-edge-side-includes).
Because it is built on top of the [Edge Side Includes (ESI)](https://www.w3.org/TR/esi-lang) W3C specification,
React ESI natively supports most of the well-known cloud cache providers including [Cloudflare Workers](https://blog.cloudflare.com/edge-side-includes-with-cloudflare-workers/), [Akamai](https://www.akamai.com/us/en/support/esi.jsp) and [Fastly](https://docs.fastly.com/guides/performance-tuning/using-edge-side-includes).
J3m5 marked this conversation as resolved.
Show resolved Hide resolved
Of course, React ESI also supports the open source [Varnish cache server](https://varnish-cache.org/intro/index.html#intro) that you can use in your own infrastructure for free ([configuration example](https://github.com/zeit/next.js/blob/canary/examples/with-react-esi/docker/varnish/default.vcl)).

Also, React ESI allows to specify of different Time To Live (TTL) per React component and generating the corresponding HTML asynchronously using a secure (signed) URL.
The cache server fetches and stores in the cache all the needed fragments (the HTML corresponding to every React component), builds the final page, and sends it to the browser.
Also, React ESI allows to specify of different Time To Live (TTL) per React component and generating the corresponding HTML asynchronously using a secure (signed) URL.
dunglas marked this conversation as resolved.
Show resolved Hide resolved
The cache server fetches and stores in the cache all the needed fragments (the HTML corresponding to every React component), builds the final page, and sends it to the browser.
J3m5 marked this conversation as resolved.
Show resolved Hide resolved
React ESI also allows components to (re-)render client-side without any specific configuration.

![ESI example](https://book.varnish-software.com/4.0/_images/esi.png)

> Schema from [The Varnish Book](https://book.varnish-software.com/4.0/chapters/Content_Composition.html)
J3m5 marked this conversation as resolved.
Show resolved Hide resolved

**[Discover React ESI in depth with this presentation](https://dunglas.fr/2019/04/react-esi-blazing-fast-ssr/)**

## Examples

* [Next.js and Varnish example](https://github.com/zeit/next.js/pull/6225)
- [Next.js, Express and Varnish](https://github.com/dunglas/react-esi/tree/main/examples/next)
- [React, Express and Varnish](https://github.com/dunglas/react-esi/tree/main/examples/express)

## Install

Expand All @@ -40,8 +43,9 @@ Or using NPM:
## Usage

React ESI provides a convenient [Higher Order Component](https://reactjs.org/docs/higher-order-components.html) that will:
* replace the wrapped component by an ESI tag server-side (don't worry React ESI also provides the tooling to generate the corresponding fragment);
* render the wrapped component client-side, and feed it with the server-side computed props (if any).

- replace the wrapped component by an ESI tag server-side (don't worry React ESI also provides the tooling to generate the corresponding fragment);
- render the wrapped component client-side, and feed it with the server-side computed props (if any).

React ESI automatically calls a `static async` method named `getInitialProps()` to populate the initial props of the component. Server-side, this method can access to the HTTP request and response, for instance, to set the `Cache-Control` header, or some [cache tags](https://api-platform.com/docs/core/performance/#enabling-the-built-in-http-cache-invalidation-system).

Expand All @@ -52,16 +56,16 @@ If the method hasn't been called server-side, then it will be called client-side
### The Higher Order Component

```javascript
// pages/index.js
import React from 'react';
import withESI from 'react-esi';
import MyFragment from 'components/MyFragment';
// pages/App.jsx
import React from "react";
import withESI from "react-esi/lib/withESI";
import MyFragment from "../components/MyFragment";

const MyFragmentESI = withESI(MyFragment, 'MyFragment');
const MyFragmentESI = withESI(MyFragment, "MyFragment");
// The second parameter is an unique ID identifying this fragment.
// If you use different instances of the same component, use a different ID per instance.

const Index = () => (
export const App = () => (
<div>
<h1>React ESI demo app</h1>
<MyFragmentESI greeting="Hello!" />
Expand All @@ -70,8 +74,8 @@ const Index = () => (
```

```javascript
// components/MyFragment.js
import React from 'react';
// components/MyFragment.jsx
import React from "react";

export default class MyFragment extends React.Component {
render() {
Expand All @@ -85,19 +89,19 @@ export default class MyFragment extends React.Component {
);
}

static async getInitialProps({ props, req, res }) {
return new Promise(resolve => {
static async getInitialProps({ props, res }) {
return new Promise((resolve) => {
if (res) {
// Set a TTL for this fragment
res.set('Cache-Control', 's-maxage=60, max-age=30');
res.set("Cache-Control", "s-maxage=60, max-age=30");
}

// Simulate a delay (call to a remote service such as a web API)
setTimeout(
() =>
resolve({
...props, // Props coming from index.js, passed through the internal URL
dataFromAnAPI: 'Hello there'
dataFromAnAPI: "Hello there"
}),
2000
);
Expand All @@ -113,80 +117,116 @@ However, it's a totally independent and standalone implementation (you don't nee

### Serving the Fragments

To serve the fragments, React ESI provides a ready to use controller compatible with [Express](https://expressjs.com/):
To serve the fragments, React ESI provides a ready-to-use controller compatible with [Express](https://expressjs.com/), check out [the full example](https://github.com/dunglas/react-esi/tree/main/examples/express).

```javascript
// server.js
import express from 'express';
import { path, serveFragment } from 'react-esi/lib/server';
// server.jsx
import express from "express";
import { path, serveFragment } from "react-esi/lib/server";
import { renderToString } from "react-dom/server";
import { App } from "./pages/App";
import React from "react";

const port = Number.parseInt(process.env.PORT || "3000", 10);

const server = express();
server.use((req, res, next) => {
// Send the Surrogate-Control header to announce ESI support to proxies (optional with Varnish, depending of your config)
res.set('Surrogate-Control', 'content="ESI/1.0"');
res.set("Surrogate-Control", 'content="ESI/1.0"');
next();
});

server.get(path, (req, res) =>
// "path" default to /_fragment, change it using the REACT_ESI_PATH env var
serveFragment(
server.get("/", (req, res) => {
const app = renderToString(<App />);

const html = `
<html lang="en">
<head>
<script src="app.js" async defer></script>
</head>
<body>
<div id="root">${app}</div>
</body>
</html>
`;
res.send(html);
});

// "path" default to /_fragment, change it using the REACT_ESI_PATH env var
server.get(path, (req, res) => {
return serveFragment(
req,
res,
// "fragmentID" is the second parameter passed to the "WithESI" HOC, the root component used for this fragment must be returned
fragmentID => require(`./components/${fragmentID}`).default)
);
(fragmentID) => require(`./components/${fragmentID}`).default
);
});

// ...
// Other Express routes come here

server.listen(80);
server.use(express.static("./dist"));

server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
```

Alternatively, here is a full example using [a Next.js server](https://github.com/zeit/next.js/tree/master/examples/custom-server-express):
Alternatively, here is a full example using [a Next.js server](https://github.com/dunglas/react-esi/tree/main/examples/next):

```javascript
J3m5 marked this conversation as resolved.
Show resolved Hide resolved
// server.js
import express from 'express';
import next from 'next';
import { path, serveFragment } from 'react-esi/lib/server';
// server.ts
import express from "express";
import next from "next";
import { parse } from "url";
import { path, serveFragment } from "react-esi/lib/server";

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
const port = Number.parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
const server = express();

server.use((req, res, next) => {
// Send the Surrogate-Control header to announce ESI support to proxies (optional with Varnish)
res.set('Surrogate-Control', 'content="ESI/1.0"');
res.set("Surrogate-Control", 'content="ESI/1.0"');
next();
});

server.get(path, (req, res) =>
serveFragment(req, res, fragmentID => require(`./components/${fragmentID}`).default)
);
server.get('*', handle); // Next.js routes
server.get(path, (req, res) => {
try {
return serveFragment(req, res, (fragmentID) => {
return require(`./components/${fragmentID}`).default;
});
} catch (error) {
res.status(500);
res.send(error.message);
}
});

// Next.js routes
server.get("*", (req, res) => handle(req, res, parse(req.url!, true)));

server.listen(port, err => {
if (err) throw err;
dunglas marked this conversation as resolved.
Show resolved Hide resolved
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
```

## Features

* Support Varnish, Cloudflare Workers, Akamai, Fastly and any other cache systems having ESI support
* Written in TypeScript
* Next.js-friendly API
- Support Varnish, Cloudflare Workers, Akamai, Fastly and any other cache systems having ESI support
- Written in TypeScript
- Next.js-friendly API

## Environment Variables

React ESI can be configured using environment variables:

* `REACT_ESI_SECRET`: a secret key used to sign the fragment URL (default to a random string, **it's highly recommended to set it to prevent problems when the server restart, or when using multiple servers**)
* `REACT_ESI_PATH`: the internal path used to generate the fragment, should not be exposed publicly (default: `/_fragment`)
- `REACT_ESI_SECRET`: a secret key used to sign the fragment URL (default to a random string, **it's highly recommended to set it to prevent problems when the server restart, or when using multiple servers**)
- `REACT_ESI_PATH`: the internal path used to generate the fragment, should not be exposed publicly (default: `/_fragment`)

## Passing Attributes to the `<esi:include>` Element

Expand Down Expand Up @@ -221,8 +261,8 @@ These tags are automatically removed from the DOM before the rendering phase.

React ESI plays very well with advanced cache strategies including:

* Cache invalidation (purge) with cache tags ([Varnish](https://github.com/varnish/varnish-modules/blob/master/docs/vmod_xkey.rst) / [Cloudflare](https://blog.cloudflare.com/introducing-a-powerful-way-to-purge-cache-on-cloudflare-purge-by-cache-tag/))
* Warming the cache when data changes in the persistence layer ([Varnish](https://blog.theodo.fr/2015/11/auto-warm-up-varnish4-cache/))
- Cache invalidation (purge) with cache tags ([Varnish](https://github.com/varnish/varnish-modules/blob/master/docs/vmod_xkey.rst) / [Cloudflare](https://blog.cloudflare.com/introducing-a-powerful-way-to-purge-cache-on-cloudflare-purge-by-cache-tag/))
- Warming the cache when data changes in the persistence layer ([Varnish](https://blog.theodo.fr/2015/11/auto-warm-up-varnish4-cache/))

Give them a try!

Expand Down
27 changes: 27 additions & 0 deletions examples/express/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
services:
J3m5 marked this conversation as resolved.
Show resolved Hide resolved
varnish:
image: varnish:7
volumes:
- ./default.vcl:/etc/varnish/default.vcl:ro
tmpfs:
- /var/lib/varnish/varnishd:exec
ports:
- "8080:80"
restart: always
depends_on:
- node

node:
image: node:18
working_dir: /home/node/app
user: node
init: true
volumes:
- ./:/home/node/app
command: yarn dev
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- REACT_ESI_SECRET=secret
restart: always
55 changes: 55 additions & 0 deletions examples/express/default.vcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
vcl 4.1;

backend node {
.host = "node";
.port = "3000";
}

sub vcl_recv {
if (req.http.upgrade ~ "(?i)websocket") {
return (pipe);
}

# Announce ESI support to Node (optional)
set req.http.Surrogate-Capability = "key=ESI/1.0";
}

sub vcl_backend_response {
# Enable ESI support
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
}

sub vcl_backend_error {
if (beresp.status == 400 && beresp.reason ~ "Bad signature") {
set beresp.http.Content-Type = "text/html; charset=utf-8";
synthetic("HTTP/1.1 400 Bad Request" + " " +
"<h1>Bad Request</h1><p>Bad signature. Please check your request and try again.</p>");
return(deliver);
}
// Handle backend errors here
set beresp.http.Content-Type = "text/html; charset=utf-8";
set beresp.status = 503; // You can set a custom status code
synthetic("HTTP/1.1 503 Service Unavailable" + " " +
"<h1>Backend Error</h1><p>Something went wrong on our end. Please try again later.</p>");
return(deliver);
}

sub vcl_pipe {
if (req.http.upgrade) {
set bereq.http.upgrade = req.http.upgrade;
set bereq.http.connection = req.http.connection;
}
}

sub vcl_deliver {
if (obj.hits > 0) {
J3m5 marked this conversation as resolved.
Show resolved Hide resolved
set resp.http.X-Varnish-Cache = "HIT";
} else {
set resp.http.X-Varnish-Cache = "MISS";
}

return (deliver);
}
5 changes: 5 additions & 0 deletions examples/express/nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"exec": "ts-node --project tsconfig.server.json src/server.jsx",
"ext": "js ts",
"watch": ["server.jsx"]
}
Loading