Skip to content

Releases: jaredpalmer/after.js

Load Assets Faster!

07 Apr 21:20
Compare
Choose a tag to compare

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?

  1. browser must download client.js, then parse it and at the end, it executes the code. when code gets executed ensureReady 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.

  2. 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).

  3. 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.

READ MORE

Fix Typo: <SerializeData /> name

08 Mar 23:03
Compare
Choose a tag to compare
  • fix(serialize-data): fix component name d07d127

v1.6.0...v1.6.1

Tree Shaking, Auto Scroll Control, <SerializeData />

08 Mar 22:34
Compare
Choose a tag to compare

Tree Shaking 🌲

After.js is now 60KB smaller

Before: image

After: image

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

v1.5.1...v1.6.0

FIX app crash after return 404 from getInitialProps

05 Feb 15:44
Compare
Choose a tag to compare

v1.5.0...v1.5.1

Suspense Like Data Fetching 🚀

18 Jan 21:56
Compare
Choose a tag to compare

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:

  1. very bad UX, this will cause page jank since matched component gets rendered on-screen without any data, and we have to show <Spinner /> till getInitialProps resolved
  2. scroll to top happen after getInitialProps resolved, so the user will see nothing (or footer) for a while
  3. 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 ])

not-concurrent

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.

concurrent

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>
    );
  }
}

v1.4.0...v1.5.0

Dynamic 404 and Redirects

24 Oct 14:03
Compare
Choose a tag to compare

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

19 Apr 19:12
Compare
Choose a tag to compare

Security Patch

  • Protect against XSS vulnerability by using Yahoo's serialize-javascript to escape instead of JSON.stringify + simple regex

v0.5.2

17 Jan 16:31
Compare
Choose a tag to compare
  • Fix infinite recompile bug in start.js
  • Remove lockfiles from examples

v0.4.0

15 Jan 19:15
Compare
Choose a tag to compare

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 with AFTER_XXXXXX