• Stars
    star
    4,131
  • Rank 10,019 (Top 0.3 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 6 years ago
  • Updated 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Next.js-like framework for server-rendered React apps built with React Router

repo-banner

After.js

npm bundle size (scoped) npm Known Vulnerabilities Github Actions GitHub version After-status license Discord

If Next.js and React Router had a baby...

Project Goals / Philosophy / Requirements

Next.js is awesome. However, its routing system isn't for me. IMHO React Router is a better foundation upon which such a framework should be built....and that's the goal here:

  • Routes are just components and don't / should not have anything to do with folder structure. Static route configs are fine.
  • Next.js's getInitialProps was/is a brilliant idea.
  • Route-based code-splitting should come for free or be easy to opt into.
  • Route-based transitions / analytics / data loading / preloading etc. , should either come for free or be trivial to implement on your own.

Table of Contents

Getting Started with After.js

After.js enables Next.js-like data fetching with any React SSR app that uses React Router.

Quickstart

You can quickly bootstrap an SSR React app with After.js using Razzle. While Razzle is not required, this documentation assumes you have the tooling setup for an isomorphic React application.

yarn global add create-after-app
create-after-app myapp
cd myapp
yarn start

Refer to Razzle's docs for tooling, babel, and webpack customization.

Data Fetching

For page components, you can add a static async getInitialProps function. This will be called on both initial server render, and then client mounts. Results are made available on this.props.

// ./src/About.js
import React from 'react';
import { NavLink } from 'react-router-dom';

class About extends React.Component {
  static async getInitialProps({ req, res, match }) {
    const stuff = await CallMyApi();
    return { stuff };
  }

  render() {
    return (
      <div>
        <NavLink to="/">Home</NavLink>
        <NavLink to="/about">About</NavLink>
        <h1>About</h1>
        {this.props.stuff}
      </div>
    );
  }
}

export default About;

getInitialProps: (ctx) => Data

Within getInitialProps, you have access to all you need to fetch data on both the client and the server:

  • req?: Request: (server-only) An Express.js request object.
  • res?: Response: (server-only) An Express.js response object.
  • match: React Router's match object.
  • history: React Router's history object.
  • location: (client-only) React Router's location object (you can only use location.pathname on server).
  • scrollToTop: React Ref object that controls scroll behavior when URL changes.

Add Params to getInitialProps: (ctx) => Data

You can extend ctx, and pass your custom params to it. this is useful when you want to fetch some data by condition or store fetched data in a global state managment system (like redux) or you may need to pass those params as props to your component from server.js (e.g result of user agent parsing).

// ./src/server.js
...
try {
  const html = await render({
    req,
    res,
    routes,
    chunks,
    // Anything else you add here will be made available
    // within getInitialProps(ctx)
    // e.g a redux store...
    customThing: 'thing',
  });
  res.send(html);
} catch (error) {
  console.error(error);
  res.json({ message: error.message, stack: error.stack });
}
...

Don't forget to pass your custom params to <After/> in client.js:

// ./src/client.js
...
ensureReady(routes).then(data =>
  hydrate(
    <BrowserRouter>
      {/*
        Anything else you pass to <After/> will be made available
        within getInitialProps(ctx)
        e.g a redux store...
      */}
      <After data={data} routes={routes} customThing="thing" />
    </BrowserRouter>,
    document.getElementById('root')
  )
);
...

Injected Page Props

  • Whatever you have returned in getInitialProps
  • prefetch: (pathname: string) => void - Imperatively prefetch and cache data for a path. Under the hood this will map through your route tree, call the matching route's getInitialProps, store it, and then provide it to your page component. If the user ultimately navigates to that path, the data and component will be ready ahead of time. In the future, there may be more options to control cache behavior in the form of a function or time in milliseconds to keep that data around.
  • refetch: (nextCtx?: any) => void - Imperatively call getInitialProps again
  • isLoading - It shows that if the returned promise from getInitialProps is in the pending state or not

Routing

As you have probably figured out, React Router powers all of After.js's routing. You can use any and all parts of RR.

Parameterized Routing

// ./src/routes.js
import Home from './Home';
import About from './About';
import Detail from './Detail';

// Internally these will become:
// <Route path={path} exact={exact} render={props => <component {...props} data={data} />} />
const routes = [
  {
    path: '/',
    exact: true,
    component: Home,
  },
  {
    path: '/about',
    component: About,
  },
  {
    path: '/detail/:id',
    component: Detail,
  },
];

export default routes;
// ./src/Detail.js
import React from 'react';
import { Route } from 'react-router-dom';

class Detail extends React.Component {
  // Notice that this will be called for
  // /detail/:id
  // /detail/:id/more
  // /detail/:id/other
  static async getInitialProps({ req, res, match }) {
    const item = await CallMyApi(`/v1/item${match.params.id}`);
    return { item };
  }

  render() {
    return (
      <div>
        <h1>Detail</h1>
        {this.props.item}
        <Route
          path="/detail/:id/more"
          exact
          render={() => <div>{this.props.item.more}</div>}
        />
        <Route
          path="/detail/:id/other"
          exact
          render={() => <div>{this.props.item.other}</div>}
        />
      </div>
    );
  }
}

export default Detail;

Client Only Data and Routing

In some parts of your application, you may not need server data fetching at all (e.g. settings). With After.js, you just use React Router 4 as you normally would in client land: You can fetch data (in componentDidMount) and do routing the same exact way.

Transition Behavior

By default, after.js will wait for getInitialProps to get resolved or rejected, so when the getInitialProps job is complete, it will show the next page. We call this behavior blocked.

You may want to show the next page with a skeleton or a spinner while getInitialProps is pending. We call this behavior instant.

you can switch to instant behavior by passing a prop to <After />.

// ./src/client.js

// transitionBehavior = blocked | instant

ensureReady(routes).then(data =>
  hydrate(
    <BrowserRouter>
      <After data={data} routes={routes} transitionBehavior="instant" />
    </BrowserRouter>,
    document.getElementById('root')
  )
);

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

Code Splitting

After.js lets you easily define lazy-loaded or code-split routes in your _routes.js file. To do this, you'll need to modify the relevant route's component definition like so:

// ./src/_routes.js
import React from 'react';
import Home from './Home';
import { asyncComponent } from '@jaredpalmer/after';

export default [
  // normal route
  {
    path: '/',
    exact: true,
    component: Home,
  },
  // codesplit route
  {
    path: '/about',
    exact: true,
    component: asyncComponent({
      loader: () => import('./About'), // required
      Placeholder: () => <div>...LOADING...</div>, // this is optional, just returns null by default
    }),
  },
];

Static Site Generation (SSG)

After.js has first class support for SSG and allows you to create super fast static webapps and serve them over CDN.

renderStatic will return the data from getInitialProps and this data will get saved by razzle into a file called page-data.json. After.js won't call getInitialProps at runtime, instead it will read the page-data.json and pass it as a prop to your component.

from ./src/static_export.js you should export render and routes function.

  • async render(req, res) should render your app into html and at the end it should return html and data.
  • async routes() should return path for pages you want to statically generate.
// ./src/static_export.js

import { renderStatic } from '@jaredpalmer/after';
import appRoutes from './routes';

const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);
const chunks = require(process.env.RAZZLE_CHUNKS_MANIFEST);

export const render = async (req, res) => {
  const { html, data } = await renderStatic({
    req,
    res,
    routes: appRoutes,
    assets,
    chunks,
  });
  res.json({ html, data });
};

export const routes = async () => {
  return ['/', '/about'];
};

after setting up this file you can build your app and run export script to generate your static site:

yarn build
yarn export

for full documentation and advanced configuration visit: https://razzlejs.org/docs/static-export

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,
  chunks,
  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
  }
}

Custom <Document>

After.js works similarly to Next.js with respect to overriding HTML document structure. This comes in handy if you are using a CSS-in-JS library or just want to collect data out of react context before or after render. To do this, create a file in ./src/Document.js like so:

// ./src/Document.js
import React from 'react';
import {
  AfterRoot,
  AfterData,
  AfterScripts,
  AfterStyles,
} 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>
    );
  }
}

export default Document;

If you were using something like styled-components, and you need to wrap you entire app with some sort of additional provider or function, you can do this with renderPage().

// ./src/Document.js
import React from 'react';
import { ServerStyleSheet } from 'styled-components';
import { AfterRoot, AfterData, AfterScripts } from '@jaredpalmer/after';

export default class Document extends React.Component {
  static async getInitialProps({ renderPage }) {
    const sheet = new ServerStyleSheet();
    const page = await renderPage(App => props =>
      sheet.collectStyles(<App {...props} />)
    );
    const styleTags = sheet.getStyleElement();
    return { ...page, styleTags };
  }

  render() {
    const { helmet, styleTags } = this.props;
    // get attributes from React Helmet
    const htmlAttrs = helmet.htmlAttributes.toComponent();
    const bodyAttrs = helmet.bodyAttributes.toComponent();

    return (
      <html {...htmlAttrs}>
        <head>
          <meta charSet="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          {helmet.title.toComponent()}
          {helmet.meta.toComponent()}
          {helmet.link.toComponent()}
          {/* here is where we put our Styled Components styleTags... */}
          {styleTags}
        </head>
        <body {...bodyAttrs}>
          <AfterRoot />
          <AfterData />
          <AfterScripts />
        </body>
      </html>
    );
  }
}

To use your custom <Document>, pass it to the Document option of your After.js render function.

// ./src/server.js
import express from 'express';
import { render } from '@jaredpalmer/after';
import routes from './routes';
import MyDocument from './Document';

const chunks = require(process.env.RAZZLE_CHUNKS_MANIFEST);

const server = express();
server
  .disable('x-powered-by')
  .use(express.static(process.env.RAZZLE_PUBLIC_DIR))
  .get('/*', async (req, res) => {
    try {
      // Pass document in here.
      const html = await render({
        req,
        res,
        document: MyDocument,
        chunks,
        routes,
      });
      res.send(html);
    } catch (error) {
      console.error(error);
      res.json({ message: error.message, stack: error.stack });
    }
  });

export default server;

Custom/Async Rendering

You can provide a custom (potentially async) rendering function as an option to After.js render function.

If present, it will be used instead of the default ReactDOMServer renderToString function.

It has to return an object of shape { html : string!, ...otherProps }, in which html will be used as the rendered string

Thus, setting customRenderer = (node) => ({ html: ReactDOMServer.renderToString(node) }) is the the same as default option.

otherProps will be passed as props to the rendered Document

Example :

// ./src/server.js
import React from 'react';
import express from 'express';
import { render } from '@jaredpalmer/after';
import { renderToString } from 'react-dom/server';
import { ApolloProvider, getDataFromTree } from 'react-apollo';
import routes from './routes';
import createApolloClient from './createApolloClient';
import Document from './Document';

const chunks = require(process.env.RAZZLE_CHUNKS_MANIFEST);

const server = express();
server
  .disable('x-powered-by')
  .use(express.static(process.env.RAZZLE_PUBLIC_DIR))
  .get('/*', async (req, res) => {
    const client = createApolloClient({ ssrMode: true });

    const customRenderer = node => {
      const App = <ApolloProvider client={client}>{node}</ApolloProvider>;
      return getDataFromTree(App).then(() => {
        const initialApolloState = client.extract();
        const html = renderToString(App);
        return { html, initialApolloState };
      });
    };

    try {
      const html = await render({
        req,
        res,
        routes,
        chunks,
        customRenderer,
        document: Document,
      });
      res.send(html);
    } catch (error) {
      console.error(error);
      res.json({ message: error.message, stack: error.stack });
    }
  });

export default server;

Author

Inspiration


MIT License

More Repositories

1

formik

Build forms in React, without the tears ๐Ÿ˜ญ
TypeScript
33,550
star
2

tsdx

Zero-config CLI for TypeScript package development
JavaScript
11,163
star
3

razzle

โœจ Create server-rendered universal JavaScript applications with no configuration
JavaScript
11,089
star
4

backpack

๐ŸŽ’ Backpack is a minimalistic build system for Node.js projects.
JavaScript
4,470
star
5

the-platform

Web. Components. ๐Ÿ˜‚
TypeScript
4,406
star
6

react-fns

Browser API's turned into declarative React components and HoC's
TypeScript
3,739
star
7

awesome-react-render-props

Awesome list of React components with render props
1,366
star
8

cypress-image-snapshot

Catch visual regressions in Cypress
JavaScript
874
star
9

presspack

๐Ÿ’ป Wordpress like it's 2022 with Webpack and Docker
JavaScript
689
star
10

react-parcel-example

Minimum viable React app with Parcel Bundler
JavaScript
485
star
11

minimum-viable-saas

A multi-tier membership SaaS in less than 500 lines of code w/Stripe and Firebase
JavaScript
442
star
12

formik-persist

๐Ÿ’พ Persist and rehydrate a Formik form to localStorage
TypeScript
375
star
13

mutik

A tiny (495B) immutable state management library based on Immer
TypeScript
325
star
14

typescript

TypeScript coding guidelines & configs for Formik
JavaScript
284
star
15

react-conf-2018

React Conf 2018 Source Code for "Moving to Suspense" Demo
JavaScript
226
star
16

tsdx-monorepo

A really good starting point for your next React x TypeScript monorepo
TypeScript
176
star
17

react-email-workflow

Newsletter design tool
JavaScript
175
star
18

formik-effect

Declarative component for managing side-effects in Formik forms. 580 bytes
TypeScript
166
star
19

formover

Build forms that pop bottles ๐Ÿพwith Formik and React Popper
TypeScript
160
star
20

react-simple-infinite-scroll

A brutally simple react infinite scroll component
TypeScript
141
star
21

react-persist

๐Ÿ’พ Persist and rehydrate React state to localStorage.
JavaScript
124
star
22

nextra-blank-custom-theme

A forkable Next.js site w/ a blank custom Nextra theme (w/Tailwind)
JavaScript
108
star
23

hyperhue

๐ŸŒˆ A fun HyperTerm theme that responds to your Philips Hue lights
JavaScript
103
star
24

reason-react-native-web-example

Razzle + Reason-React + React-Native-Web. Damn that's a lot of R's.
Reason
100
star
25

disco.chat

Add real-time ephemeral chat to any webpage.
TypeScript
93
star
26

react-router-nextjs-like-data-fetching

Demonstrating React Router 4's SSR awesomeness
JavaScript
89
star
27

dotfiles

My setup
Shell
50
star
28

razzle-react-vue-elm-php-lol

๐Ÿ”ฅ Blazing fast Razzle app with React, Vue, PHP, and Elm + HMR
JavaScript
50
star
29

formik-alicante

Formik slides & demos from React Alicante
JavaScript
46
star
30

react-suspense-playground

Stock Market News app w/ React Suspense
JavaScript
43
star
31

razzle-unrouted

Blazingly fast server-rendered MVC Webapps with Preact and Webpack
JavaScript
42
star
32

nextjs-langchain-example

Demo of using LangChain.js with Next.js and Vercel Edge Functions (to stream the response)
TypeScript
42
star
33

codemods

Collection of codemods for TypeScript and JavaScript codebases
JavaScript
41
star
34

framer-electron-preview

Quickly run Framer prototypes within Electron.
JavaScript
35
star
35

country-fns

๐ŸŒ Useful country data for forms and stuff.
JavaScript
34
star
36

TIL

๐Ÿ“–Trying to document some of my learnings
30
star
37

squeezy

1 kB React component for accessible accordions / collapse UI
TypeScript
30
star
38

react-europe-2019

Slides and demo app from my keynote
JavaScript
29
star
39

jpjs

Some TypeScript utils
TypeScript
21
star
40

emotion-jsxstyle

jsxstyle primitives powered by emotion
JavaScript
20
star
41

react-router-suspense-demo

React Suspense x React Router Exploration
JavaScript
17
star
42

nextjs-route-handler-email

Experimenting with react.email and Next.js 13 Route Handlers
TypeScript
16
star
43

react-snippets

My React snippets for JavaScript and TypeScript
13
star
44

jaredpalmer.github.io

TypeScript
12
star
45

framer-router

A little routing solution for Framer.js
CoffeeScript
11
star
46

thinkaboutthis.fm

Source code for thinkaboutthis.fm
TypeScript
11
star
47

formik-bloomberg-talk

Slides and examples from my talk at Bloomberg on October 31, 2019
JavaScript
10
star
48

jaredpalmer-vscode-extensionpack

All of my VS Code extensions .... in one extension pack
9
star
49

electron-starter

A minimal Electron starter
JavaScript
8
star
50

babel-preset-react-ts

Create React App's Babel 7 Preset, plus TypeScript
JavaScript
7
star
51

noirny

Website for Noir New York (formerly the lately). Opening 11/21/19
TypeScript
7
star
52

jest-jsxstyle

๐Ÿƒ Jest utilities for JSXStyle
JavaScript
6
star
53

flask-vercel

Python
6
star
54

jaredpalmer

it me.
5
star
55

saas-subdomain-nginx-node-docker

Example setup of SaaS-like user subdomains using Express, NGINX, and Docker Compose
JavaScript
5
star
56

juice.now.sh

Automattic's Juice CSS inliner as a microservice
HTML
4
star
57

datocms-next-js-blog-demo-9687

JavaScript
4
star
58

react-error-overlay-razzle-bug

JavaScript
3
star
59

glamor-jsxstyle

Future home of glamor-jsxstyle
JavaScript
3
star
60

reactnyc-formik

๐Ÿ“ˆ Formik presentation at the August React NYC meetup @Spotify
JavaScript
3
star
61

bf-solid-addons

Helpful addons to Buzzfeed's Solid CSS Framework
CSS
2
star
62

codesandbox-template-next.js

Next.js template for CodeSandbox Projects
TypeScript
2
star
63

framer-electron

Prototype desktop apps with Framer.js and Electron with your own editor.
JavaScript
2
star
64

formik-docs

WIP. Formik docs website
TypeScript
2
star
65

oss-deck

JavaScript
1
star
66

downshift-razzle-bug

JavaScript
1
star
67

next-back-button-error

JavaScript
1
star
68

flask-pipenv-example

Python
1
star
69

os-today

A list a of the GitHub repos you should be using.
JavaScript
1
star
70

blocks

TypeScript
1
star
71

ci-info

Go
1
star
72

hyperneon

Hyperterm theme that rotates neon colors
JavaScript
1
star