jtart / react-universal-app

Library for building a single-page application with Universal React component(s) and React Router.
MIT License
14 stars 2 forks source link
components react react-router server-rendering universal

react-universal-app

Greenkeeper Code Coverage Known Vulnerabilities gzip size CircleCI status

react-universal-app is a library that enables the building of a single-page application with universal React components and React Router.

Project goals

The goals of react-universal-app are very simple:

Simplicity

react-universal-app provides 2 React components:

  1. for the server, just pass it some initial data and routes, and render the thing
  2. for the client, just pass it the data your rendered on the serve and the same routes you passed to the server component, and hydrate the thing

It also provides a single data-fetching API. This data-fetching API is defined next to your your routes.

Flexibility

react-universal-app doesn't do much.

It gives you a couple of React components for rendering your routes, and doesn't force you to render or hydrate your app in any particular way. You can render however you want!

It gives you a single data-fetching API, which is defined on your routes. This means you can build your app components in anyway you want, and your React components are just React components.

Getting Started

To get started quickly, view the example applications. These applications give an example of how you might create an application using react-universal-app.

Alternatively, follow the steps below to get started.

Installation

npm install react-universal-app react

App

The following gives information on how to setup routing and data fetching in your main React application. Once you have setup your routes, you can pass them to React components that react-universal-app provides for server-side rendering and client-side hydration.

Routing

react-universal-app uses React Router 4, which is a great foundation for serving pages as components and providing route configuration. To define your routes, create some route configuration and export them.

Note: react-universal-app currently only supports a single top-level of routing.

// ./app/routes.js
import Home from './components/Home';

const routes = [
  {
    path: '/',
    exact: true,
    component: Home,
  },
];

export default routes;
// ./app/components/Home.js

const Home = () => (
  <div>
    <h1>Home!</h1>
  </div>
)

export default Home;

Data Fetching

react-universal-app provides a very familiar getInitialData for data fetching, which is defined in the route configuration.

This provides a clear seperation of concerns and agnosticism between route configuration/data fetching and components. Your React components are just React components, and you can swap components on routes as much as you please.

This has an implicit benefit of reducing the barrier to entry for development for new React developers as the flow of data in the application is clear and defined.

await getInitialData(ctx): { data }

getInitialData is an optional asynchronous function that is defined on the configuration of a route. This API is used for fetching data from an external API, or just returning some initial data to a route's component based on some other variable.

A ctx object is passed to getInitialData, which includes:

On the server, getInitialData is called explicitly by you through loadInitialData (see below), with the response passed to the server component.

On the client, it is called internally and the returned data is passed to the route's defined component. On the client, react-universal-app has 3 states in the lifecycle of getInitialData that are passed to the route's component:

// app/routes.js
import Home from './components/Home';

const routes = [
  {
    path: '/',
    exact: true,
    component: Home,
    getInitialData: await (ctx) => {
      return { title: 'Home!' };;
    },
  },
];

export default routes;
// app/components/Home.js

const Home = ({ loading, error, data }) => {
  if(loading) return 'Loading...'
  if(error) return 'Oh no, something went wrong!'
  if(data) {
    return (
      <div>
        <h1>{data.title}</h1>
      </div>
    )
  }
};

export default Home;

Parameterized Routing

react-universal-app supports parameterized routing from react-router. As data fetching is defined on the route, parameterized routing is a breeze, and can be handled very cleanly.

// ./app/routes.js
import Home from './components/Home';

const routes = [
  {
    path: '/:id',
    exact: true,
    component: Home,
    getInitialProps: await ({ match }) => {
      const { id } = match.params;
      const response = await fetch(`/someApi/${id}`);
      const apiResponse = await response.json();

      return { title: 'Home!', apiResponse };;
    },
  },
];

export default routes;
// ./app/components/Home.js

const Home = ({ loading, error, data }) => {
  if(loading) return 'Loading...'
  if(error) return 'Oh no, something went wrong!'
  if(data) {
    const { title, apiResponse } = data;
    return (
      <div>
        <h1>{title}</h1>
        {
          apiResponse.map(({ title, description }) => (
            <div>
              <h2>{title}</h2>
              <p>{description}</p>
            </div>
          ))
        }
      </div>
    )
  }
};

export default Home;

Server

For rendering your app on a server, react-universal-app provides you a React component (ServerApp) and a data-fetching API (loadInitialData). react-universal-app could fetch the initial data internally for you if it was more opinionated. However, react-universal-app doesn't make any assumptions about how or where you will render your React application on the server, so it can't! Read Client for a clear example of why not.

Then, take a look at ReactDOMServers's methods for rendering a React application on a server!

await loadInitialData(url, routes): { data }

loadInitialData is an optional asynchronous function that matches the route based on the passed URL, calls getInitialData, and returns the response. If the route does not have a getInitialData an empty object will be returned. Takes the following arguments:

A React component that renders a route with some initial data. It can take the following props:

Client

To hydrate your React application on a client, react-universal-app provides a React component called ClientApp. You must then call react-dom's hydrate method.

The client-side application needs access to the data that was used to render the application on the server, and so should be injected into the HTML document that the server wrapped the rendered React application in and sent to the client. This data could be inside a script tag, that injects the data onto the global window object, like so:

<script>
  window.__APP_DATA__ = data;
</script>

Then, take a look at ReactDOM's methods for hydrating a React application on a client!

A React component that renders your routes and application on the client. Takes the following props: