Releases: jaredpalmer/after.js
Load Assets Faster!
Load Assets Faster!
Upgrading to version 2 should not take more than 10 minutes.
In v1, with asyncComponent
you split part of your application into a new chunk and on BROWSER when you need that part of your code it gets downloaded automatically. when page rendered on the server there was no way to understand which chunks needed for the current request so After.js only sends client.js
and styles.css
file, then on BROWSER with ensureReady
method it tries to fetch chunks (split CSS and JS files) needed for the current request. and it's slow!
WHY?
-
browser must download
client.js
, then parse it and at the end, it executes the code. when code gets executedensureReady
method gets called,ensureReady
finds and download chunks needed to render the current page and when all files get downloaded it start to re-hydrate. -
browser will render the page without CSS styles (because we split them and it will get them when
ensureReady
called), this makes the site look ugly for 2,3 seconds (bad UX). -
have you ever think about why CSS is render blocking?
if browser finds a<link rel="stylesheet">
tag, it would stop rendering page and waits until CSS file be downloaded and parsed completely (this mechanism is necessary to have fast page renders), if CSS files attach to dom after page gets rendered, the browser must repaint the whole page. (painting is too much job for browser and it's slow)
in After.js 2 this problem is solved and it sends all JS and CSS files needed for current request in the initial server response.
Fix Typo: <SerializeData /> name
- fix(serialize-data): fix component name d07d127
Tree Shaking, Auto Scroll Control, <SerializeData />
Tree Shaking 🌲
After.js is now 60KB smaller
closes #238
Auto-Scroll Control 📜
Disable Auto-Scroll Globally
By default, After.js will scroll to top when URL changes, you can change that by passing scrollToTop: false
to render().
// ./src/server.js
const scrollToTop = false;
const html = await render({
req,
res,
routes,
assets,
scrollToTop,
});
Disable Auto-Scroll for a Specific Page
We are using a ref object to minimize unnecessary re-renders, you can mutate scrollToTop.current and component will not re-rendered but its scroll behavior will change immediately.
You can control auto-scroll behavior from getInitialProps
.
class MyComponent extends React.Component {
static async getInitialProps({ scrollToTop }) {
scrollToTop.current = false;
return { scrollToTop, stuff: 'whatevs' };
}
render() {
return <h1>Hello, World!</h1>;
}
componentWillUnmount() {
this.props.scrollToTop.current = true; // at the end restore scroll behavior
}
}
closes #258
<SerializeData />
and getSerializedData()
📃
when you do SSR and you use redux, mobx, ... in document.js
you have to pass store data to the client and on the client before ensure ready
get called you have to read that data and put in the redux store.
<SeriallizeData />
will do that for you.
// ./src/document.js
import React from 'react';
import {
AfterRoot,
AfterData,
AfterScripts,
AfterStyles,
SerializeData,
__AfterContext,
} from '@jaredpalmer/after';
import { Provider } from 'react-redux';
import serialize from 'serialize-javascript';
class Document extends React.Component {
static async getInitialProps({ renderPage, store }) {
const page = await renderPage(App => props => (
<Provider store={store}>
<App {...props} />
</Provider>
));
return { ...page };
}
render() {
const { helmet } = this.props;
// get attributes from React Helmet
const htmlAttrs = helmet.htmlAttributes.toComponent();
const bodyAttrs = helmet.bodyAttributes.toComponent();
return (
<html {...htmlAttrs}>
<head>
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta charSet="utf-8" />
<title>Welcome to the Afterparty</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
{helmet.title.toComponent()}
{helmet.meta.toComponent()}
{helmet.link.toComponent()}
<AfterStyles />
</head>
<body {...bodyAttrs}>
<AfterRoot />
<AfterData />
<ReduxData />
<AfterScripts />
</body>
</html>
);
}
}
function ReduxData() {
const { store } = React.useContext(__AfterContext);
return <SerializeData name="preloaded_state" data={store.getState()} />;
}
export default Document;
to get data in the client use getSerializedData
method:
const preloadedState = getSerializedData('preloaded_state');
const store = configureStore(preloadedState);
getSerializedData
will read data from window object and then it will remove object from window, you can change this behavior by passing a second argument to the method:
function getSerializedData(name, remove = true) {
const data = window[`_${name.toUpperCase()}_`];
if (remove) {
delete window[`_${name.toUpperCase()}_`];
}
return data;
};
- feat(with-redux): update with-redux example abcd759
- Rename SerializeData.tsx to serializeData.tsx e0fd440
- Merge branch 'canary' into feat/serialize-data c29f833
- fix(serialize-data): change file names 71707ff
- Merge branch 'master' into canary f23a6c1
- feat(seriallize-data): pass props to <script /> tag cc66e46
- feat(serialize-data): add serilizeData.js to package.json 008c110
- feat(serilize-data): add e68d29e
- feat(esm-build): update build scripts 526b703
- feat(esm-build): fix imports 2fd854c
- Update README.md 863c516
- Feature - Auto Scroll Control (#282) 615468c
- Feature - Create After App (#283) 33e288a
FIX app crash after return 404 from getInitialProps
- bugfix: app break after return 404 from component 3990c16
- [ImgBot] Optimize images (#286) 0d1e144
- add layout-component example (#285) 69f6df3
- add redux example (#284) 3618b08
- fix version 1448dfd
- update Document.js in examples 50d6b4f
- add material-ui-rtl example 635a165
- update material-ui example e4e702f
- update material-ui example e22901a
- update README.md d51ae56
- add material-ui example 5e73a81
- add test command 22be19f
Suspense Like Data Fetching 🚀
Changed getInitialProps
Behavior
Old Behavior
URL Change -> getInitialProps get called -> matched component get renderd on screen -> getInitialProps resolved -> component gets re-renered and can acess getInitialProps data from it's props
this is very bad and there are some problems:
- very bad UX, this will cause page jank since matched component gets rendered on-screen without any data, and we have to show
<Spinner />
tillgetInitialProps
resolved - scroll to top happen after
getInitialProps
resolved, so the user will see nothing (or footer) for a while - if we use data from
getInitialProps
with hooks, we have to write very complicated code
function PageB({ data })
const [count, setCount] = React.useState(data)
if (!data) return <Spinner />
return <span>{count}</span>
}
PageB.getInitialProps = () => {
return new Promise(reslove => setTimeout(() => reslove({ data: 1 }) , 3000))
}
data
is undefined so count is undefined, and to fix it we have to write an effect like below:
React.useEffect(() => {
setCount(data)
}, [ data ])
New Behavior:
URL Change -> getInitialProps get called -> wait's on current location until getInitialProps resolved -> render matched component with data from getInitialProps
When a user moves from page /a
to page /b
, URL changes but the user still sees /a
page on the screen till /b
page data get fetched.
Custom Document.js
Now we have a simpler custom Document with <AfterScripts />
and <AfterStyles />
.
Any params that passed to getInitialProps
and return values of it are available from __AfterContext
.
import { __AfterContext } from "@jaredpalmer/after"
Use this context to build components for your custom Document.
import React from 'react';
import { AfterScripts, AfterStyles, AfterRoot, AfterData } from "@jaredpalmer/after"
class Document extends React.Component {
static async getInitialProps({ renderPage }) {
const page = await renderPage();
return { ...page };
}
render() {
const { helmet } = this.props;
// get attributes from React Helmet
const htmlAttrs = helmet.htmlAttributes.toComponent();
const bodyAttrs = helmet.bodyAttributes.toComponent();
return (
<html {...htmlAttrs}>
<head>
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta charSet="utf-8" />
<title>Welcome to the Afterparty</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
{helmet.title.toComponent()}
{helmet.meta.toComponent()}
{helmet.link.toComponent()}
<AfterStyles />
</head>
<body {...bodyAttrs}>
<AfterRoot />
<AfterData />
<AfterScripts />
</body>
</html>
);
}
}
- use yarn 9a26b30
- ts-jest ignore diagnostics 3ae6831
<AfterScripts />
,<AfterStyles />
(#279) 287c7c8- update package dependecies (#278) 7a9eb5b
- Fix always positive result (#230) 54a14a2
- Update docs (#270) b4329f7
- fix typo (#213) c446608
- Create main.yml 22407ab
- Merge pull request #234 from nimaa77/code-cleanup 4647374
- Merge branch 'master' into code-cleanup 2063ac2
- Merge pull request #264 from nimaa77/fix-data-fetching-fix-1 c9b8a03
- Merge pull request #235 from nimaa77/feature/error-pages-fixes bba8d5a
- Merge branch 'master' into code-cleanup 7088e27
- fix AfterParty 95144f8
- Fix data fetching (🚀 SUSPENSE but not suspense 🚀) (#251) 3112cd4
- a better title for 404 Page 4866edc
- fix test case for notfoundcomponent db85fb4
- remove "*" from readme.md 2ab908d
- format code with prettier c3143db
- remove with space after OS: e856d36
- add prettier ignore file ecbdfb2
- allow doctoc with prettier 4653ba4
- allow prettier to format all supported file extentions 695e22c
Dynamic 404 and Redirects
Dynamic 404 and Redirects
404 Page
React Router can detect No Match (404) Routes and show a fallback component, you can define your custom fallback component in routes.js
file.
// ./src/routes.js
import React from 'react';
import Home from './Home';
import Notfound from './Notfound';
import { asyncComponent } from '@jaredpalmer/after';
export default [
// normal route
{
path: '/',
exact: true,
component: Home,
},
// 404 route
{
// there is no need to declare path variable
// react router will pick this component as fallback
component: Notfound,
},
];
Notfound component must set staticContext.statusCode
to 404 so express can set response status code more info.
// ./src/Notfound.js
import React from 'react';
import { Route } from 'react-router-dom';
function NotFound() {
return (
<Route
render={({ staticContext }) => {
if (staticContext) staticContext.statusCode = 404;
return <div>The Page You Were Looking For Was Not Found</div>;
}}
/>
);
}
export default NotFound;
if you don't declare 404 component in routes.js
After.js will use its default fallback.
Dynamic 404
Sometimes you may need to send a 404 response based on some API response, in this case, react-router don't show fallback and you have to check for that in your component.
import Notfound from './Notfound';
function ProductPage({ product, error }) {
if (error) {
if (error.response.status === 404) {
return <Notfound />;
}
return <p>Something went Wrong !</p>;
}
{ /* if there were no errors we have our data */ }
return <h1>{product.name}</h1>;
}
ProductPage.getInitialProps = async ({ match }) => {
try {
const { data } = await fetchProduct(match.params.slug);
return { product: data };
} catch (error) {
return { error };
}
};
this makes code unreadable and hard to maintain. after.js makes this easy by providing an API for handling Dynamic 404 pages. you can return { statusCode: 404 }
from getInitialProps
and after.js will show 404 fallback components that you defined in routes.js
for you.
function ProductPage({ product }) {
return <h1>{product.name}</h1>;
}
ProductPage.getInitialProps = async ({ match }) => {
try {
const { data } = await fetchProduct(match.params.slug);
return { product: data };
} catch (error) {
if (error.response.status === 404) return { statusCode: 404 };
return { error };
}
};
Redirect
You can redirect the user to another route by using Redirect
from react-router, but it can make your code unreadable and hard to maintain.
with after.js you can redirect client to other route by returning { redirectTo: "/new-location" }
from getInitialProps
.
this can become handy for authorization when user does not have permission to access a specific route and you can redirect him/her to the login page.
Dashboard.getInitialProps = async ({ match }) => {
try {
const { data } = await fetchProfile();
return { data };
} catch (error) {
if (error.response.status === 401)
return { statusCode: 401, redirectTo: '/login' };
return { error };
}
};
The redirect will happen before after.js start renders react to string soo it's fast.
when using redirectTo
default value for statusCode
is 301, but you can use any numeric value you want.
Commits:
- Dynamic 404 and Redirects (#231) 2daea6c
- #226: Fix anchor scroll behaviour (#227) 9868004
- Fix scroll to top behaviour (#223) 1bf1e87
- fix: examples/basic/package.json to reduce vulnerabilities (#200) ac1bb58
- A Express.js -> An Express.js (#187) 5418910
- Update render.tsx (#202) 2c03aaa
- add first jest unit tests and fix another bug in loadInitialProps (#178) 3e4f2f4
- Change NavLink import in code snippet in README.md (#180) 4bf6dfe
- fix: examples/basic/package.json to reduce vulnerabilities (#181) 9626742
- change examples basic references to build and not package (#169) cd468f6
- fix: examples/basic/package.json to reduce vulnerabilities (#171) 3a267cf
- remove as any in withRouter in after (#176) 57301bb
- fix getInitialProps match, add ctx type, update deps (#174) 4606542
- Fix: browser error on deserializing a state with Date (#164) 97e17a2
- make options optional for render (#166) 7d6f5fe
- remove any from AfterRenderOptions (#163) 41d1719
- remove as many any types is as sane right now (#162) e3f1526
- Stop swallowing getInitialProps errors (#151) 0ec6932
- Create stale.yml 61c76ad
- Add missing comma (#159) 7eb1e5c
- Fix "}" in final example code - styled-components (#138) 23dfc40
- Pass custom args to getInitialProps() (#157) f96e293
- fix: package.json to reduce vulnerabilities (#137) e713035
- Update README.md 55a12eb
- Remove console.log 26fc5e1
v1.3.1
Security Patch
- Protect against XSS vulnerability by using Yahoo's
serialize-javascript
to escape instead ofJSON.stringify
+ simple regex
v0.5.2
- Fix infinite recompile bug in
start.js
- Remove lockfiles from examples
v0.4.0
New stuff
- Customizable and overridable
<Document>
(html). - Updated documentation
- Add documentation about
prefetch
- Add
after test
command. It works like CRA's / Razzle's. - Add nicer experience if 3000 is in use already.
Breaking
- Remove razzle references from the codebase and docs.
razzle.config.js
->after.config.js
. .env
variables now must start withAFTER_XXXXXX