GithubHelp home page GithubHelp logo

catamphetamine / react-pages Goto Github PK

View Code? Open in Web Editor NEW
178.0 16.0 30.0 5.46 MB

A complete solution for building a React/Redux application: routing, page preloading, (optional) server-side rendering, asynchronous HTTP requests, document metadata, etc.

License: MIT License

JavaScript 100.00%
react redux isomorphic react-router

react-pages's Introduction

react-pages

npm version npm downloads

A complete solution for building a React/Redux application

Introduction

Getting started

First, install Redux.

$ yarn add redux react-redux

or:

$ npm install redux react-redux --save

Then, install react-pages:

$ yarn add react-pages

or:

$ npm install react-pages --save

Then, create a react-pages configuration file.

The configuration file:

./src/react-pages.js

import routes from './routes.js'

export default {
  routes
}

The routes:

./src/routes.js

import React from 'react'
import { Route } from 'react-pages'

import App from '../pages/App.js'
import Item from '../pages/Item.js'
import Items from '../pages/Items.js'

export default [{
  Component: App,
  path: '/',
  children: [
    { Component: App },
    { Component: Items, path: 'items' },
    { Component: Item, path: 'items/:id' }
  ]
}]

The page components:

./src/pages/App.js

import React from 'react'
import { Link } from 'react-pages'

export default ({ children }) => (
  <section>
    <header>
      Web Application
    </header>
    {children}
    <footer>
      Copyright
    </footer>
  </section>
)

./src/pages/Items.js

import React from 'react'

export default () => <div> This is the list of items </div>

./src/pages/Item.js

import React from 'react'

export default ({ params }) => <div> Item #{params.id} </div>

Finally, call render() in the main client-side javascript file of the app.

The main client-side javascript file of the app:

./src/index.js

import { render } from 'react-pages/client'
import settings from './react-pages.js'

// Render the page in a web browser.
render(settings)

The index.html file of the app usually looks like this:

<html>
  <head>
    <title>Example</title>
    <!-- Fix encoding. -->
    <meta charset="utf-8">
    <!-- Fix document width for mobile devices. -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  </head>
  <body>
    <script src="/bundle.js"></script>
  </body>
</html>

Where bundle.js is the ./src/index.js file built with Webpack (or you could use any other javascript bundler).

And make sure that the output files are accessible from a web browser.

The index.html and bundle.js files must be served over HTTP(S).

If you're using Webpack then add HtmlWebpackPlugin to generate index.html, and run webpack-dev-server with historyApiFallback to serve the generated index.html and bundle.js files over HTTP on localhost:8080.

See HtmlWebpackPlugin configuration example

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')

const buildOutputPath = '...'
const devServerPort = 8080 // Any port number.

module.exports = {
  output: {
    path: buildOutputPath,
    publicPath: `http://localhost:${devServerPort}`,
    ...
  },
  ...,
  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html' // Path to `index.html` file.
    }),
    ...
  ],
  devServer: {
    port: devServerPort,
    contentBase: buildOutputPath,
    historyApiFallback : true
  }
}

src/index.html

<html>
  <head>
    <title>Example</title>
    <!-- Fix encoding. -->
    <meta charset="utf-8">
    <!-- Fix document width for mobile devices. -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  </head>
  <body>
    <!-- HtmlWebpackPlugin will insert a <script> tag here. -->
  </body>
</html>
webpack-dev-server --hot --config webpack.config.js

Or see the Webpack example project.

If you're using Parcel instead of Webpack then see the basic example project for the setup required in order to generate and serve index.html and bundle.js files over HTTP on localhost:1234.

Done

So now the website should be fully working.

The website (index.html, bundle.js, CSS stylesheets and images, etc) can now be deployed as-is in a cloud (e.g. on Amazon S3) and served statically for a very low price. The API can be hosted "serverlessly" in a cloud (e.g. Amazon Lambda) which is also considered cheap. No running Node.js server is required.

Yes, it's not a Server-Side Rendered approach because a user is given a blank page first, then bundle.js script is loaded by the web browser, then bundle.js script is executed fetching some data from the API via an HTTP request, and only when that HTTP request comes back โ€” only then the page is rendered (in the browser). Google won't index such websites, but if searchability is not a requirement (at all or yet) then that would be the way to go (e.g. startup "MVP"s or "internal applications"). Server-Side Rendering can be easily added to such setup should the need arise.

Adding Server Side Rendering

Adding server-side rendering to the setup is quite simple, although I'd consider it an "advanced" topic.

While client-side rendering could be done entirely in a web browser, server-side rendering would require running a Node.js process somewhere in a cloud which slightly increases the complexity of the whole setup.

So in case of server-side rendering, index.html file is being generated on-the-fly by a page rendering server (a Node.js process) for each incoming HTTP request, so the index.html file that was used previously for client-side rendering may be deleted now as it's of no longer use.

A Node.js script for running a "rendering server" process would look like this:

./rendering-server.js

import webpageServer from 'react-pages/server'
import settings from './react-pages'

// Create webpage rendering server
const server = webpageServer(settings, {
  // Pass `secure: true` flag to listen on `https://` rather than `http://`.
  // secure: true,

  // These are the URLs of the "static" javascript and CSS files
  // which are injected into the resulting HTML webpage in the form of
  // <script src="..."/> and <link rel="style" href="..."/> tags.
  //
  // The javascript file should be the javascript "bundle" of the website
  // and the CSS file should be the CSS "bundle" of the website.
  //
  // P.S.: To inject other types of javascript or CSS files
  // (for example, files of 3rd-party libraries),
  // use a separate configuration parameter called `html`:
  // https://gitlab.com/catamphetamine/react-pages/blob/master/README-ADVANCED.md#all-webpage-rendering-server-options)
  //
  assets() {
    return {
      // This should be the URL for the application's javascript bundle.
      // In this case, the configuration assumes that the website is being run
      // on `localhost` domain with "static file hosting" enabled for its files.
      javascript: 'http://localhost:8080/bundle.js',

      // (optional)
      // This should be the URL for the application's CSS bundle.
      style: 'http://localhost:8080/bundle.css'
    }
  }
})

// Start webpage rendering server on port 3000.
// Syntax: `server.listen(port, [host], [callback])`.
server.listen(3000, function(error) {
  if (error) {
    throw error
  }
  console.log(`Webpage rendering server is listening at http://localhost:3000`)
})

Run the rendering server:

$ npm install npx --global
$ npm install babel-cli
$ npx babel-node rendering-server.js

Now disable javascript in Chrome DevTools, go to localhost:3000 and the server should respond with a fully server-side-rendered page.

Conclusion

This concludes the introductory part of the README and the rest is the description of the various tools and techniques which come prepackaged with this library.

A working example illustrating Server-Side Rendering and all other things can be found here: webpack-react-redux-isomorphic-render-example.

Another minimalistic example using Parcel instead of Webpack can be found here: react-pages-basic-example.

Documentation

Root component

react-pages configuration file supports a rootComponent parameter. It should be the root component of the application. It receives properties: children and store (Redux store).

The default (and minimal) rootComponent is simply a Redux Provider wrapped around the children. The Redux Provider enables Redux, because this library uses Redux internally.

import { Provider as ReduxProvider } from 'react-redux'

export default function DefaultRootComponent({ store, children }) {
  return (
    <ReduxProvider store={store}>
      {children}
    </ReduxProvider>
  )
}

Redux

If you plan on using Redux in your application, provide a reducers object in the react-pages configuration file.

./src/react-pages.js

import routes from './routes.js'

// The `reducers` parameter should be an object containing
// Redux reducers that will be combined into a single Redux reducer
// using the standard `combineReducers()` function of Redux.
import * as reducers from './redux/index.js'

export default {
  routes,
  reducers
}

Where the reducers object should be:

./src/redux/index.js

// For those who're unfamiliar with Redux concepts,
// a "reducer" is a function `(state, action) => state`.
//
// The main (or "root") "reducer" usually consists of "sub-reducers",
// in which case it's an object rather than a function,
// and each property of such object is a "sub-reducer" function.
//
// There's no official name for "sub-reducer".
// For example, Redux Toolkit [calls](https://redux.js.org/usage/structuring-reducers/splitting-reducer-logic) them "slices".
//
export { default as subReducer1 } from './subReducer1.js'
export { default as subReducer2 } from './subReducer2.js'
...

Middleware

To add custom Redux "middleware", specify a reduxMiddleware parameter in the react-pages configuration file.

export default {
  ...,

  // `reduxMiddleware` should be an array of custom Redux middlewares.
  reduxMiddleware: [
    middleware1,
    middleware2
  ]
}

Loading pages

To "load" a page before it's rendered (both on server side and on client side), define a static load property function on the page component.

The load function receives a "utility" object as its only argument:

function Page({ data }) {
  return (
    <div>
      {data}
    </div>
  )
}

Page.load = async (utility) => {
  const {
    // Can `dispatch()` Redux actions.
    dispatch,

    // Can be used to get a slice of Redux state.
    useSelector,

    // (optional)
    //
    // "Load Context" could hold any custom developer-defined variables
    // that could then be accessed inside `.load()` functions.
    //
    // To define a "load context":
    //
    // * Pass `getLoadContext()` function as an option to the client-side `render()` function.
    //   The options are the second argument of that function.
    //   The result of the function will be passed to each `load()` function as `context` parameter.
    //   The result of the function will be reused within the scope of a given web browser tab,
    //   i.e. `getLoadContext()` function will only be called once for a given web browser tab.
    //
    // * (if also using server-side rendering)
    //   Pass `getLoadContext()` function as an option to the server-side `webpageServer()` function.
    //   The options are the second argument of that function.
    //   The result of the function will be passed to each `load()` function as `context` parameter.
    //   The result of the function will be reused within the scope of a given HTTP request,
    //   i.e. `getLoadContext()` function will only be called once for a given HTTP request.
    //
    // `getLoadContext()` function recevies an argument object: `{ dispatch }`.
    // `getLoadContext()` function should return a "load context" object.
    //
    // Miscellaneous: `context` parameter will also be passed to `onNavigate()`/`onBeforeNavigate()` functions.
    //
    context,

    // Current page location (object).
    location,

    // Route URL parameters.
    // For example, for route "/users/:id" and URL "/users/barackobama",
    // `params` will be `{ id: "barackobama" }`.
    params,

    // Navigation history.
    // Each entry is an object having properties:
    // * `route: string` โ€” Example: "/user/:userId/post/:postId".
    // * `action: string` โ€” One of: "start", "push", "redirect", "back", "forward".
    history,

    // Is this server-side rendering?
    server,

    // (utility)
    // Returns a cookie value by name.
    getCookie
  } = utility

  // Send HTTP request and wait for response.
  // For example, it could just be using the standard `fetch()` function.
  const data = await fetch(`https://data-source.com/data/${params.id}`)

  // Optionally return an object containing page component `props`.
  // If returned, these props will be available in the page component,
  // same way it works in Next.js in its `getServerSideProps()` function.
  return {
    // `data` prop will be available in the page component.
    props: {
      data
    }
  }
}

The load property function could additionally be defined on the application's root React component. In that case, the application would first execute the load function of the application's root React component, and then, after it finishes, it would proceed to executing the page component's load function. This behavior allows the root React component's load function to perform the "initialization" of the application: for example, it could authenticate the user.

Catching errors in load function

To catch all errors originating in load() functions, specify an onLoadError() parameter in react-pages.js settings file.

{
  onLoadError: (error, { location, url, redirect, useSelector, server }) => {
    redirect(`/error?url=${encodeURIComponent(url)}&error=${error.status}`)
  }
}
Redirecting from load function

To redirect from a load function, return an object with redirect property, similar to how it works in Next.js in its getServerSideProps() function.

UserPage.load = async ({ params }) => {
  const user = await fetch(`/api/users/${params.id}`)
  if (user.wasDeleted) {
    return {
      redirect: {
        url: '/not-found'
      }
    }
  }
  return {
    props: {
      user
    }
  }
}
Permanent redirects in routes configuration

To permanently redirect from one URL to another URL, specify a permanentRedirectTo parameter on the "from" route.

{
  path: '/old-path/:id',
  permanentRedirectTo: '/new-path/:id'
}
Advanced topic: client-side page load indication (during navigation).

While the application is performing a load as a result of navigating to another page, a developer might prefer to show some kind of a loading indicator. Such loading indicator could be implemented as a React component that listens to the boolean value returned from useLoading() hook.

import { useLoading } from 'react-pages'
import LoadingIndicator from './LoadingIndicator.js'

export default function PageLoading() {
  const isLoading = useLoading()
  return (
    <LoadingIndicator show={isLoading}/>
  )
}
export default function App({ children }) {
  return (
    <div>
      <PageLoading/>
      {children}
    </div>
  )
}
Advanced topic: client-side page load indication (initial).

Initial client-side (non-server-side) load is different from client-side load during navigation: during the initial client-side load, the <App/> element is not rendered yet. Therefore, while the application is performing an initial client-side load, a blank screen is shown.

There're two possible workarounds for that:

  • Perform the initial load on server side (not on client side).
  • Show some kind of a loading indicator instead of a blank screen during the initial load.

To show a loading indicator instead of a blank screen during the initial load, one could specify some additional react-pages configuration parameters:

  • InitialLoadComponent โ€” A React component that shows an initial page loading indicator. Receives properties:

    • initial: true โ€” This is just a flag that is always true.
    • show: boolean โ€” Is true when the component should be shown. Is false when the component should no longer be shown.
      • When false is passed, the component could either hide itself immediately or show some kind of a hiding animation (for example, fade out). The duration of such hiding animation should be passed as initialLoadHideAnimationDuration: number parameter (see below) so that the library knows when can it unmount the InitialLoadComponent.
    • hideAnimationDuration: number โ€” This is just a copy of initialLoadHideAnimationDuration: number parameter (see below) for convenience.
  • initialLoadShowDelay: number โ€” When supplying InitialLoadComponent, one should also specify the delay before showing the InitialLoadComponent. For example, such delay could be used to only show InitialLoadComponent for initial loads that aren't fast enough. For "no delay", the value should be 0.

  • initialLoadHideAnimationDuration: number โ€” When supplying InitialLoadComponent, one should also specify the duration of the hide animation of InitialLoadComponent, if it has a hide animation. If there's no hide animation, the value should be 0.

On client side, in order for load to work, all links must be created using the <Link/> component imported from react-pages package. Upon a click on a <Link/>, first it waits for the next page to load, and then, when the next page is fully loaded, the navigation itself takes place.

load also works for Back/Forward navigation. To disable page load on Back navigation, pass instantBack property to a <Link/>.

For example, consider a search results page loading some data (could be search results themselves, could be anything else unrelated). A user navigates to this page, waits for load to finish and then sees a list of items. Without instantBack if the user clicks on an item he's taken to the item's page. Then the user clicks "Back" and is taken back to the search results page but has to wait for that load again. With instantBack though the "Back" transition occurs instantly without having to wait for that load again. Same goes then for the reverse "Forward" navigation from the search results page back to the item's page, but that's just a small complementary feature. The main benefit is the instantaneous "Back" navigation creating a much better UX where a user can freely explore a list of results without getting penalized for it with a waiting period on each click.

import React from 'react'
import { useSelector } from 'react-redux'
import { Link } from 'react-pages'

function SearchResultsPage() {
  const results = useSelector(state => state.searchPage.results)
  return (
    <ul>
      { results.map((item) => (
        <li>
          <Link to="/items/{item.id}" instantBack>
            {item.name}
          </Link>
        </li>
      ))) }
    </ul>
  )
}

SearchResultsPage.load = async () => await fetchSomeData()

There's also instantBack: true option that could be passed to navigate(location, options) function which is returned from useNavigate() hook. The behavior of the option is the same.

instantBack is ignored when navigating to the same route: for example, if there's an <Article/> page component having a <Link instantBack/> to another <Article/> then instantBack is ignored โ€” this feature was originally added for Redux because it made sense that way (in Redux there's only one slot for data of a route that gets rewritten every time the route is navigated to). For other data fetching frameworks like Relay I guess it would make sense to turn that off. Create an issue if that's the case.

One can also use the exported wasInstantNavigation() function (on client side) to find out if the current page was navigated to "instantly". This can be used, for example, to restore a "state" of a widget on instant "Back" navigation so that it renders immediately with the previously cached "results" or something.

There's also a canGoBackInstantly() function (on client side) that tells if the currently page can be navigated "Back" from instantly. This function can be used to render a custom "Go Back" button on a page only when an instant "Back" transition could be performed.

There's also a canGoForwardInstantly() function (analogous to canGoBackInstantly()).

There's also an isInstantBackAbleNavigation() function (on client side) which tells if the currently ongoing navigation process is performed with instantBack option: for example, if <Link instantBack/> is clicked, or when navigate(location, { instantBack: true }) returned from useNavigate() hook is called. It can be used in a useNavigationStartEffect() hook to save the current page state for later restoring it if the user navigates "Back" instantly.

import { useNavigationStartEffect, isInstantBackAbleNavigation } from 'react-pages'

function Page() {
  useNavigationStartEffect(() => {
    if (isInstantBackAbleNavigation()) {
      // Save the current page state.
    }
  })
}

Fetching Data

Fetching data in an application could be done using several approaches:

  • Using fetch() for making HTTP requests and then storing the result in React Component state using useState() hook setter.
  • Using fetch() for making HTTP requests and then storing the result in Redux state by dispatch()-ing a "setter" action.
  • Using "asynchronous actions" framework provided by this library, which is described in detail in the next section of this document. This is the most sophisticated variant of the three and it comes with many useful features such as:
    • Handling cookies
    • CORS utilities
    • Authentication utilities
    • File upload progress support
    • Persisting the result in Redux state

Asynchronous actions

Implementing synchronous actions in Redux is straightforward. But what about asynchronous actions like HTTP requests? Redux itself doesn't provide any built-in solution for that leaving it to 3rd party middlewares. Therefore this library provides one.

Pure Promises

This is the lowest-level approach to asynchronous actions. It is described here just for academic purposes and most likely won't be used directly in any app.

If a Redux "action creator" returns an object with a promise (function) and events (array) then dispatch()ing such an action results in the following steps:

  • An event of type = events[0] is dispatched
  • promise function gets called and returns a Promise
  • If the Promise succeeds then an event of type = events[1] is dispatched having result property set to the Promise result
  • If the Promise fails then an event of type = events[2] is dispatched having error property set to the Promise error
function asynchronousAction() {
  return {
    promise: () => Promise.resolve({ success: true }),
    events: ['PROMISE_PENDING', 'PROMISE_SUCCESS', 'PROMISE_ERROR']
  }
}

dispatch(asynchronousAction()) call returns the Promise itself:

Page.load = async ({ dispatch }) => {
  await dispatch(asynchronousAction())
}

HTTP utility

Because in almost all cases dispatching an "asynchronous action" in practice means "making an HTTP request", the promise function used in asynchronousAction()s always receives an { http } argument: promise: ({ http }) => ....

The http utility has the following methods:

  • head
  • get
  • post
  • put
  • patch
  • delete

Each of these methods returns a Promise and takes three arguments:

  • the url of the HTTP request
  • data object (e.g. HTTP GET query or HTTP POST body)
  • options (described further)

So, API endpoints can be queried using http and ES6 async/await syntax like so:

function fetchFriends(personId, gender) {
  return {
    promise: ({ http }) => http.get(`/api/person/${personId}/friends`, { gender }),
    events: ['GET_FRIENDS_PENDING', 'GET_FRIENDS_SUCCESS', 'GET_FRIENDS_FAILURE']
  }
}

The possible options (the third argument of all http methods) are

  • headers โ€” HTTP Headers JSON object.
  • authentication โ€” Set to false to disable sending the authentication token as part of the HTTP request. Set to a String to pass it as an Authorization: Bearer ${token} token (no need to supply the token explicitly for every http method call, it is supposed to be set globally, see below).
  • progress(percent, event) โ€”ย Use for tracking HTTP request progress (e.g. file upload).
  • onResponseHeaders(headers) โ€“ย Use for examining HTTP response headers (e.g. Amazon S3 file upload).
To set custom HTTP headers or to change HTTP request Content-Type

For that use the http.onRequest(request, { url, originalUrl, useSelector }) setting in ./react-pages.js where:

  • request is a superagent request that can be modified. For example, to set an HTTP header: request.set(headerName, headerValue).
  • originalUrl is the URL argument of the http utility call.
  • url is the originalUrl transformed by http.transformUrl() settings function. If no http.transformUrl() is configured then url is the same as the originalUrl.

Redux module

Once one starts writing a lot of promise/http Redux actions it becomes obvious that there's a lot of copy-pasting and verbosity involved. To reduce those tremendous amounts of copy-pasta "redux module" tool may be used which:

  • Gives access to http.
  • Autogenerates Redux action status events (${actionName}_PENDING, ${actionName}_SUCCESS, ${actionName}_ERROR).
  • Automatically adds Redux reducers for the action status events.
  • Automatically populates the corresponding action status properties (${actionName}Pending: true/false, ${actionName}Error: Error) in Redux state.

For example, the fetchFriends() action from the previous section can be rewritten as:

Before:

// ./actions/friends.js
function fetchFriends(personId, gender) {
  return {
    promise: ({ http }) => http.get(`/api/person/${personId}/friends`, { gender }),
    events: ['FETCH_FRIENDS_PENDING', 'FETCH_FRIENDS_SUCCESS', 'FETCH_FRIENDS_FAILURE']
  }
}

// ./reducers/friends.js
export default function(state = {}, action = {}) {
  switch (action.type) {
    case 'FETCH_FRIENDS_PENDING':
      return {
        ...state,
        fetchFriendsPending: true,
        fetchFriendsError: null
      }
    case 'FETCH_FRIENDS_SUCCESS':
      return {
        ...state,
        fetchFriendsPending: false,
        friends: action.value
      }
    case 'FETCH_FRIENDS_ERROR':
      return {
        ...state,
        fetchFriendsPending: false,
        fetchFriendsError: action.error
      }
    default
      return state
  }
}

After:

import { ReduxModule } from 'react-pages'

const redux = new ReduxModule('FRIENDS')

export const fetchFriends = redux.action(
  'FETCH_FRIENDS',
  (personId, gender) => http => {
    return http.get(`/api/person/${personId}/friends`, { gender })
  },
  // The fetched friends list will be placed
  // into the `friends` Redux state property.
  'friends'
  //
  // Or write it like this:
  // { friends: result => result }
  //
  // Or write it as a Redux reducer:
  // (state, result) => ({ ...state, friends: result })
)

// This is the Redux reducer which now
// handles the asynchronous action defined above.
export default redux.reducer()

Much cleaner.

Also, when the namespace or the action name argument is omitted it is autogenerated, so this

const redux = new ReduxModule('FRIENDS')
...
redux.action('FETCH_ITEM', id => http => http.get(`/items/${id}`), 'item')

could be written as

const redux = new ReduxModule()
...
redux.action(id => http => http.get(`/items/${id}`), 'item')

and in this case redux will autogenerate the namespace and the action name, something like REACT_WEBSITE_12345 and REACT_WEBSITE_ACTION_12345.

A more complex example: a comments section for a blog post page.

redux/blogPost.js

import { ReduxModule } from 'react-pages'

const redux = new ReduxModule('BLOG_POST')

// Post comment Redux "action creator"
export const postComment = redux.action(
  // 'POST_COMMENT',
  (userId, blogPostId, commentText) => async http => {
    // The original action call looks like:
    // `dispatch(postComment(1, 12345, 'bump'))`
    return await http.post(`/blog/posts/${blogPostId}/comment`, {
      userId: userId,
      text: commentText
    })
  }
)

// Get comments Redux "action creator"
export const getComments = redux.action(
  // 'GET_COMMENTS',
  (blogPostId) => async http => {
    return await http.get(`/blog/posts/${blogPostId}/comments`)
  },
  // The fetched comments will be placed
  // into the `comments` Redux state property.
  'comments'
  //
  // Or write it like this:
  // { comments: result => result }
  //
  // Or write it as a Redux reducer:
  // (state, result) => ({ ...state, comments: result })
)

// A developer can listen to any Redux event via
// `redux.on('EVENT_NAME', (state, action) => state)`.
//
// In this case, it listens to a "success" event of a `redux.action()`.
// There's a section in this document describing this feature in more detail:
// "Redux module can also listen for events from other redux modules via <code>redux.on()</code>"
//
redux.on('BLOG_POST', 'CUSTOM_EVENT', (state, action) => ({
  ...state,
  reduxStateProperty: action.value
}))

// This is the Redux reducer which now
// handles the asynchronous actions defined above
// (and also the `handler.on()` events).
// Export it as part of the "main" reducer.
export default redux.reducer()

redux/index.js

// The "main" reducer is composed of various reducers.
export { default as blogPost } from './blogPost'
...

The React Component would look like this

import React from 'react'
import { getBlogPost, getComments, postComment } from './redux/blogPost'

export default function BlogPostPage() {
  const userId = useSelector(state => state.user.id)
  const blogPost = useSelector(state => state.blogPost.blogPost)
  const comments = useSelector(state => state.blogPost.comments)
  return (
    <div>
      <article>
        { blogPost.text }
      </article>
      <ul>
        { comments.map(comment => <li>{comment}</li>) }
      </ul>
      <button onClick={() => postComment(userId, blogPost.id, 'text')}>
        Post comment
      </button>
    </div>
  )
}

// Load blog post and comments before showing the page
// (see "Page loading" section of this document)
BlogPostPage.load = async ({ dispatch, params }) => {
  // `params` are the URL parameters in route `path`.
  // For example, "/blog/:blogPostId".
  await dispatch(getBlogPost(params.blogPostId))
  await dispatch(getComments(params.blogPostId))
}

Redux module can also handle the conventional "synchronous" actions via export const action = redux.simpleAction()

A simple Redux action that simply updates Redux state.

action = redux.simpleAction((state, actionArgument) => newState)
import { ReduxModule } from 'react-pages'

const redux = new ReduxModule('NOTIFICATIONS')

// Displays a notification.
//
// The Redux "action" creator is gonna be:
//
// function(text) {
//   return {
//     type    : 'NOTIFICATIONS:NOTIFY',
//     message : formatMessage(text)
//   }
// }
//
// And the corresponding reducer is gonna be:
//
// case 'NOTIFICATIONS:NOTIFY':
//   return {
//     ...state,
//     message: action.message
//   }
//
// Call it as `dispatch(notify(text))`.
//
export const notify = redux.simpleAction(
  // (optional) Redux event name.
  'NOTIFY',
  // The Redux reducer:
  (state, message) => ({ ...state, message }),
  // The Redux reducer above could be also defined as:
  // 'message'
)

// This is the Redux reducer which now
// handles the actions defined above.
export default redux.reducer()
dispatch(notify('Test'))

Redux module can also listen for events from other redux modules via redux.on()

// A developer can listen to any Redux event via
// `redux.on('EVENT_NAME', (state, action) => state)`.
//
// If one string argument is passed then it will listen for
// an exact Redux `action.type`.
//
// If two string arguments are passed then the first argument should be
// a `ReduxModule` namespace (the argument to `ReduxModule()` function)
// and the second argument should be a name of an asynchronous `redux.action()`.
// In that case, it will listen only for a "success" event of that `redux.action()`.
//
// To listen for a non-"success" event of a `redux.action()`,
// specify the full Redux event name.
// Example for a "pending" event: 'BLOG_POST: CUSTOM_EVENT_PENDING'.
//
redux.on('BLOG_POST', 'CUSTOM_EVENT', (state, action) => ({
  ...state,
  reduxStateProperty: action.value
}))

HTTP cookies

To enable sending and receiving cookies when making cross-domain HTTP requests, specify http.useCrossDomainCookies() function in react-pages.js configuration file. If that function returns true, then it has the same effect as changing credentials: "same-origin" to credentials: "include" in a fetch() call.

When enabling cross-domain cookies on front end, don't forget to make the relevant backend changes:

  • Change Access-Control-Allow-Origin HTTP header from * to an explict comma-separated list of the allowed domain names.
  • Add Access-Control-Allow-Credentials: true HTTP header.
{
  http: {
    // Allows sending cookies to and receiving cookies from
    // "trusted.com" domain or any of its sub-domains.
    useCrossDomainCookies({ getDomain, belongsToDomain, url, originalUrl }) {
      return belongsToDomain('trusted.com')
    }
  }
}

HTTP authentication

In order to send an authentication token in the form of an Authorization: Bearer ${token} HTTP header, specify http.authentication.accessToken() function in react-pages.js configuration file.

{
  http: {
    authentication: {
      // If a token is returned from this function, it gets sent as
      // `Authorization: Bearer {token}` HTTP header.
      accessToken({ useSelector, getCookie }) {
        return localStorage.getItem('accessToken')
      }
    }
  }
}
Protecting the access token from being leaked to a 3rd party

{
  http: {
    authentication: {
      // If a token is returned from this function, it gets sent as
      // `Authorization: Bearer {token}` HTTP header.
      accessToken({ useSelector, getCookie, url, originalUrl }) {
        // It's recommended to check the URL to make sure that the access token
        // is not leaked to a third party: only send it to your own servers.
        //
        // `originalUrl` is the URL argument of the `http` utility call.
        // `url` is the `originalUrl` transformed by `http.transformUrl()` settings function.
        // If no `http.transformUrl()` is configured then `url` is the same as the `originalUrl`.
        //
        if (url.indexOf('https://my.api.com/') === 0) {
          return localStorage.getItem('accessToken')
        }
      }
    }
  }
}

Authentication and authorization using access tokens

The accessToken is initially obtained when a user signs in: the web browser sends HTTP POST request to /sign-in API endpoint with { email, password } parameters and gets { userInfo, accessToken } as a response, which is then stored in localStorage (or in Redux state, or in a cookie) and all subsequent HTTP requests use that accessToken to call the API endpoints. The accessToken itself is usually a JSON Web Token signed on the server side and holding the list of the user's priviliges ("roles"). Hence authentication and authorization are completely covered. Refresh tokens are also supported.

This kind of an authentication and authorization scheme is self-sufficient and doesn't require "restricting" any routes: if a route's load uses http utility for querying an API endpoint then this API endpoint must check if the user is signed in and if the user has the necessary priviliges. If yes then the route is displayed. If not then the user is redirected to either a "Sign In Required" page or "Access Denied" page.

A real-world (advanced) example for handling "Unauthenticated"/"Unauthorized" errors happening in loads and during http calls:

./react-pages.js

{
  ...,
  // Catches errors thrown from page `load()` functions.
  onLoadError(error, { location, url, redirect, dispatch, useSelector, server }) {
    // Not authenticated
    if (error.status === 401) {
      return handleUnauthenticatedError(error, url, redirect);
    }
    // Not authorized
    if (error.status === 403) {
      return redirect('/unauthorized');
    }
    // Not found
    if (error.status === 404) {
      return redirect('/not-found');
    }
    // Redirect to a generic error page in production
    if (process.env.NODE_ENV === 'production') {
      // Prevents infinite redirect to the error page
      // in case of overall page rendering bugs, etc.
      if (location.pathname !== '/error') {
        // Redirect to a generic error page
        return redirect(`/error?url=${encodeURIComponent(url)}`);
      }
    } else {
      // Report the error
      console.error('--------------------------------');
      console.error(`Error while loading "${url}"`);
      console.error('--------------------------------');
      console.error(error.stack);
    }
  },

  http: {
    // Catches all HTTP errors that weren't thrown from `load()` functions.
    onError(error, { location, url, redirect, dispatch, useSelector }) {
      // JWT token expired, the user needs to relogin.
      if (error.status === 401) {
        handleUnauthenticatedError(error, url, redirect);
        // `return true` indicates that the error has been handled by the developer
        // and it shouldn't be re-thrown as an "Unhandled rejection".
        return true
      }
    },
    ...
  }
}

function handleUnauthenticatedError(error, url, redirect) {
  // Prevent double redirection to `/unauthenticated`.
  // (e.g. when two parallel `Promise`s load inside `load`
  //  and both get Status 401 HTTP Response)
  if (typeof window !== 'undefined' && window.location.pathname === '/unauthenticated') {
    return;
  }
  let unauthenticatedURL = '/unauthenticated';
  let parametersDelimiter = '?';
  if (url !== '/') {
    unauthenticatedURL += `${parametersDelimiter}url=${encodeURIComponent(url)}`;
    parametersDelimiter = '&';
  }
  switch (error.message) {
    case 'TokenExpiredError':
      return redirect(`${unauthenticatedURL}${parametersDelimiter}expired=โœ”`);
    case 'AuthenticationError':
      return redirect(`${unauthenticatedURL}`);
    default:
      return redirect(unauthenticatedURL);
  }
}

HTTP errors

This library doesn't force one to dispatch "asynchronous" Redux actions using the http utility in order to fetch data over HTTP. For example, one could use the standard fetch() function instead. But if one chooses to use the http utility, default error handlers for it could be set up.

To listen for http errors, one may specify two functions in react-pages.js configuration file:

  • onLoadError() โ€” Catches all errors thrown from page load() functions.
  • http.onError() โ€” Catches all HTTP errors that weren't thrown from load() functions. Should return true if the error has been handled successfully and shouldn't be printed to the console.
{
  http: {
    // (optional)
    // Catches all HTTP errors that weren't thrown from `load()` functions.
    onError(error, { location, url, redirect, dispatch, useSelector }) {
      if (error.status === 401) {
        redirect('/not-authenticated')
        // `return true` indicates that the error has been handled by the developer
        // and it shouldn't be re-thrown as an "Unhandled rejection".
        return true
      } else {
        // Ignore the error.
      }
    },

    // (optional)
    // (advanced)
    //
    // Creates a Redux state `error` property from an HTTP `Error` instance.
    //
    // By default, returns whatever JSON data was returned in the HTTP response,
    // if any, and adds a couple of properties to it:
    //
    // * `message: string` โ€” `error.message`.
    // * `status: number?` โ€” The HTTP response status. May be `undefined` if no response was received.
    //
    getErrorData(error) {
      return { ... }
    }
  }
}

HTTP request URLs

When sending HTTP requests to API using the http utility it is recommended to set up http.transformUrl(url) configuration setting to make the code a bit cleaner.

Before:

// Actions.

export const getUser = redux.action(
  (id) => http => http.get(`https://my-api.cloud-provider.com/users/${id}`),
  'user'
)

export const updateUser = redux.action(
  (id, values) => http => http.put(`https://my-api.cloud-provider.com/users/${id}`, values)
)

After:

// Actions.

export const getUser = redux.action(
  (id) => http => http.get(`api://users/${id}`),
  'user'
)

export const updateUser = redux.action(
  (id, values) => http => http.put(`api://users/${id}`, values)
)

// Settings.

{
  ...
  http: {
    transformUrl: url => `https://my-api.cloud-provider.com/${url.slice('api://'.length)}`
  }
}

On server side, user's cookies are attached to all relative "original" URLs so http.transformUrl(originalUrl) must not transform relative URLs into absolute URLs, otherwise user's cookies would be leaked to a third party.

File upload

The http utility will also upload files if they're passed as part of data (see example below). The files passed inside data must have one of the following types:

  • In case of a File it will be a single file upload.
  • In case of a FileList with a single File inside it would be treated as a single File.
  • In case of a FileList with multiple Files inside a multiple file upload will be performed.
  • In case of an <input type="file"/> DOM element all its .files will be taken as a FileList parameter.

File upload progress can be metered by passing progress option as part of the options .

See example
// React component.
function ItemPage() {
  const dispatch = useDispatch()

  const onFileSelected = (event) => {
    const file = event.target.files[0]

    // Could also pass just `event.target.files` as `file`
    dispatch(uploadItemPhoto(itemId, file))

    // Reset the selected file
    // so that onChange would trigger again
    // even with the same file.
    event.target.value = null
  }

  return (
    <div>
      ...
      <input type="file" onChange={onFileSelected}/>
    </div>
  )
}

// Redux action creator
function uploadItemPhoto(itemId, file) {
  return {
    promise: ({ http }) => http.post(
      '/item/photo',
      { itemId, file },
      { progress(percent) { console.log(percent) } }
    ),
    events: ['UPLOAD_ITEM_PHOTO_PENDING', 'UPLOAD_ITEM_PHOTO_SUCCESS', 'UPLOAD_ITEM_PHOTO_FAILURE']
  }
}

JSON Date parsing

By default, when using http utility all JSON responses get parsed for javascript Dates which are then automatically converted from Strings to Dates.

This has been a very convenient feature that is also safe in almost all cases because such date Strings have to be in a very specific ISO format in order to get parsed (year-month-dayThours:minutes:seconds[timezone], e.g. 2017-12-22T23:03:48.912Z).

Looking at this feature now, I wouldn't advise enabling it because it could potentially lead to a bug when it accidentally mistakes a string for a date. For example, some user could write a comment with the comment content being an ISO date string. If, when fetching that comment from the server, the application automatically finds and converts the comment text from a string to a Date instance, it will likely lead to a bug when the application attempts to access any string-specific methods of such Date instance, resulting in a possible crash of the application.

Therefore, currenly I'd advise setting http.findAndConvertIsoDateStringsToDateInstances flag to false in react-pages.js settings file to opt out of this feature.

{
  ...
  http: {
    ...
    findAndConvertIsoDateStringsToDateInstances: false
  }
}

Snapshotting

Server-Side Rendering is good for search engine indexing but it's also heavy on CPU not to mention the bother of setting up a Node.js server itself and keeping it running.

In many cases data on a website is "static" (doesn't change between redeployments), e.g. a personal blog or a portfolio website, so in these cases it will be beneficial (much cheaper and faster) to host a statically generated version a website on a CDN as opposed to hosting a Node.js application just for the purpose of real-time webpage rendering. In such cases one should generate a static version of the website by snapshotting it on a local machine and then host the snapshotted pages in a cloud (e.g. Amazon S3) for a very low price.

Snapshotting instructions

First run the website in production mode (for example, on localhost).

Then run the following Node.js script which is gonna snapshot the currently running website and put it in a folder which can then be hosted anywhere.

# If the website will be hosted on Amazon S3
npm install @auth0/s3 --save
import path from 'path'

import {
  // Snapshots website pages.
  snapshot,
  // Uploads files.
  upload,
  // Uploads files to Amazon S3.
  S3Uploader,
  // Copies files/folders into files/folders.
  // Same as Linux `cp [from] [to]`.
  copy,
  // Downloads data from a URL into an object
  // of shape `{ status: Number, content: String }`.
  download
} from 'react-pages/static-site-generator'

import configuration from '../configuration'

// Temporary generated files path.
const generatedSitePath = path.resolve(__dirname, '../static-site')

async function run() {
  // Snapshot the website.
  await snapshot({
    // The host and port on which the website
    // is currently running in production mode.
    // E.g. `localhost` and `3000`.
    host: configuration.host,
    port: configuration.port,
    pages: await generatePageList(),
    outputPath: generatedSitePath,
    //
    // Set this flag to `true` to re-run all `load`s on page load.
    // For example, if the data used on the page can be updated
    // in-between the static site deployments.
    // reloadData: true
  })

  // Copy assets (built by Webpack).
  await copy(path.resolve(__dirname, '../build/assets'), path.resolve(generatedSitePath, 'assets'))
  await copy(path.resolve(__dirname, '../robots.txt'), path.resolve(generatedSitePath, 'robots.txt'))

  // Upload the website to an Amazon S3 bucket.
  await upload(generatedSitePath, S3Uploader({
    // Setting an `ACL` for the files being uploaded is optional.
    // Alternatively a bucket-wide policy could be set up instead:
    //
    // {
    //   "Version": "2012-10-17",
    //   "Statement": [{
    //     "Sid": "AddPerm",
    //     "Effect": "Allow",
    //     "Principal": "*",
    //     "Action": "s3:GetObject",
    //     "Resource": "arn:aws:s3:::[bucket-name]/*"
    //   }]
    // }
    //
    // If not setting a bucket-wide policy then the ACL for the
    // bucket itself should also have "List objects" set to "Yes",
    // otherwise the website would return "403 Forbidden" error.
    //
    ACL: 'public-read',
    bucket: confiugration.s3.bucket,
    accessKeyId: configuration.s3.accessKeyId,
    secretAccessKey: configuration.s3.secretAccessKey,
    region: configuration.s3.region
  }))

  console.log('Done');
}

run().catch((error) => {
  console.error(error)
  process.exit(1)
})

// Get the list of all page URLs.
async function generatePageList() {
  const pages = [
    '/',
    '/about',
    // Error pages need a `status` property
    // to indicate that it shouldn't throw on such errors
    // and should proceed with snapshotting the next pages.
    { url: '/unauthenticated', status: 401 },
    { url: '/unauthorized', status: 403 },
    { url: '/not-found', status: 404 },
    { url: '/error', status: 500 }
  ]

  // (optional) Add some dynamic page URLs, like `/items/123`.

  // Query the database for the list of items.
  const { status, content } = JSON.parse(await download(`https://example.com/api/items`))

  if (status !== 200) {
    throw new Error('Couldn\'t load items')
  }

  // Add item page URLs.
  const items = JSON.parse(content)
  return pages.concat(items.map(item => `/items/${item.id}`))
}

The snapshot() function snapshots the list of pages to .html files and then the upload() function uploads them to the cloud (in this case to Amazon S3). The snapshot() function also snapshots a special base.html page which is an empty page that should be used as the "fallback", i.e. the cloud should respond with base.html file contents when the file for the requested URL is not found: in this case base.html will see the current URL and perform all the routing neccessary on the client side to show the correct page. If the snapshot() function isn't passed the list of pages to snapshot (e.g. if pages argument is null or undefined) then it will only snapshot base.html. The static website will work with just base.html, the only point of snapshotting other pages is for Google indexing.

If the website is hosted on Amazon S3 then the IAM policy should allow:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::<bucket-name>"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::<bucket-name>/*"
            ]
        }
    ]
}

The snapshotting approach works not only for classical web "documents" (a blog, a book, a portfolio, a showcase) but also for dynamic applications. Consider an online education portal where users (students) can search for online courses and the prices are different for each user (student) based on their institution. Now, an online course description itself is static (must be indexed by Google) and the actual course price is dynamic (must not be indexed by Google).

The solution is to add two loads for the course page: one for static data (which runs while snapshotting) and another for dynamic data (which runs only in a user's web browser).
import React from 'react'

export default function CoursePage() {
  ...
}

CoursePage.load = [
  async ({ dispatch }) => await dispatch(loadCourseInfo()),
  {
    load: async ({ dispatch }) => await dispatch(loadCoursePrice()),
    client: true
  }
]

In this example loadCourseInfo() will be executed while snapshotting and therefore course info will be present on the snapshotted page. But course price won't be present on the snapshotted page because it's being loaded inside the client: true load that only gets called in a user's web browser. When a user opens the course page in his web browser it will show the snapshotted page with course info with a "loading" spinner on top of it as it is loading the course price. After the course price has been loaded the "loading" spinner disappears and the user sees the fully rendered course page.

Page HTTP response status code

To set a custom HTTP response status code for a specific route set the status property of that route.

export default [{
  path: '/',
  Component: Application,
  children: [
    { Component: Home },
    { path: 'blog', Component: Blog },
    { path: 'about', Component: About },
    { path: '*', Component: PageNotFound, status: 404 }
  ]
}]

Setting <title/> and <meta/> tags

To add <title/> and <meta/> tags to a page, define meta: (...) => object static function on a page component:

function Page() {
  return (
    <section>
      ...
    </section>
  )
}

Page.load = async ({ params }) => {
  return {
    props: {
      bodyBuilder: await getBodyBuilderInfo(params.id)
    }
  }
}

Page.meta = ({ props, useSelector }) => {
  const notificationsCount = useSelector(state => state.user.notificationsCount)

  const { bodyBuilder } = props

  return {
    // `<meta property="og:site_name" .../>`
    siteName: 'International Bodybuilders Club',

    // Webpage `<title/>` will be replaced with this one
    // and also `<meta property="og:title" .../>` will be added.
    title: `(${notificationsCount}) ${bodyBuilder.name}`,

    // `<meta property="og:description" .../>`
    description: 'Muscles',

    // `<meta property="og:image" .../>`
    // https://iamturns.com/open-graph-image-size/
    image: 'https://cdn.google.com/logo.png',

    // Objects are expanded.
    //
    // `<meta property="og:image" content="https://cdn.google.com/logo.png"/>`
    // `<meta property="og:image:width" content="100"/>`
    // `<meta property="og:image:height" content="100"/>`
    // `<meta property="og:image:type" content="image/png"/>`
    //
    image: {
      _: 'https://cdn.google.com/logo.png',
      width: 100,
      height: 100,
      type: 'image/png'
    },

    // Arrays are expanded (including arrays of objects).
    image: [{...}, {...}, ...],

    // `<meta property="og:audio" .../>`
    audio: '...',

    // `<meta property="og:video" .../>`
    video: '...',

    // `<meta property="og:locale" content="ru_RU"/>`
    locale: state.user.locale,

    // `<meta property="og:locale:alternate" content="en_US"/>`
    // `<meta property="og:locale:alternate" content="fr_FR"/>`
    locales: ['ru_RU', 'en_US', 'fr_FR'],

    // `<meta property="og:url" .../>`
    url: 'https://google.com/',

    // `<meta property="og:type" .../>`
    type: 'profile',

    // `<meta charset="utf-8"/>` tag is added automatically.
    // The default "utf-8" encoding can be changed
    // by passing custom `charset` parameter.
    charset: 'utf-16',

    // `<meta name="viewport" content="width=device-width, initial-scale=1.0"/>`
    // tag is added automatically
    // (prevents downscaling on mobile devices).
    // This default behaviour can be changed
    // by passing custom `viewport` parameter.
    viewport: '...',

    // All other properties will be transformed directly to
    // either `<meta property="{property_name}" content="{property_value}/>`
    // or `<meta name="{property_name}" content="{property_value}/>`
  }
})

The parameters of a meta function are:

  • props โ€” Any props returned from the load() function.
  • useSelector โ€” A hook that could be used to access Redux state.

If the root route component also has a meta function, the result of the page component's meta function will be merged on top of the result of the root route component's meta function.

The meta will be applied on the web page and will overwrite any existing <meta/> tags. For example, if there were any <meta/> tags written by hand in index.html template then all of them will be dicarded when this library applies its own meta, so any "base" <meta/> tags should be moved from the index.html file to the root route component's meta function:

function App({ children }) {
  return (
    <div>
      {children}
    </div>
  )
}

App.meta = ({ useSelector }) => {
  return {
    siteName: 'WebApp',
    description: 'A generic web application',
    locale: 'en_US'
  }
}

The meta function behaves like a React "hook": <meta/> tags will be updated if the values returned from useSelector() function calls do change.

In some advanced cases, the meta() function might need to access some state that is local to the page component and is not stored in global Redux state. That could be done by setting metaComponentProperty property of a page component to true and then rendering the <Meta/> component manually inside the page component, where any properties passed to the <Meta/> component will be available in the props of the meta() function.

function Page({ Meta }) {
  const [number, setNumber] = useState(0)
  return (
    <>
      <Meta number={number}/>
      <button onClick={() => setNumber(number + 1)}>
        Increment
      </button>
    </>
  )
}

Page.metaComponentProperty = true

Page.meta = ({ props }) => {
  return {
    title: String(props.number)
  }
}

Navigation Listener

If the application would like to listen to navigation changes โ€” for example, to report the current location to Google Analytics โ€” it might supply onNavigate() function option to the client-side render() function:

See code example

import { render } from 'react-pages/client'

await render(settings, {
  // Runs on the initial page load, and then on each navigation.
  onNavigate({
    // Stringified `location` object.
    url,
    // `location` object.
    location,
    // URL pathname parameters.
    params,
    // (optional) If `getLoadContext()` function is defined,
    // this will be the result of calling that function.
    context,
    // Redux `dispatch()` function.
    dispatch,
    // Mimicks Redux `useSelector()` hook.
    useSelector
  }) {
    if (process.env.NODE_ENV === 'production') {
      // Set up Google Analytics via `gtag`.
      gtag('config', configuration.googleAnalytics.id, {
        // Anonymize IP for all Google Analytics events.
        // https://developers.google.com/analytics/devguides/collection/gtagjs/ip-anonymization
        // This makes Google Analytics compliant with GDPR:
        // https://www.jeffalytics.com/gdpr-ip-addresses-google-analytics/
        'anonymize_ip': true,
        // Google Analytics can get users' "Demographics" (age, sex)
        // from "3rd party" data sources if "Advertising Reporting Features"
        // are enabled in Google Analytics admin panel.
        // Such data could be considered "Personal Identifiable Information"
        // which falls under the terms of GDPR.
        // There's also "Remarketing" feature that could also
        // fall under the terms of GDPR.
        'allow_display_features': false,
        // Specifies what percentage of users should be tracked.
        // This defaults to 100 (no users are sampled out) but
        // large sites may need to use a lower sample rate
        // to stay within Google Analytics processing limits.
        // 'sample_rate': 1,
        // Report "page view" event to Google Analytics.
        // https://stackoverflow.com/questions/37655898/tracking-google-analytics-page-views-in-angular2
        // https://developers.google.com/analytics/devguides/collection/gtagjs/single-page-applications
        'page_path': location.pathname
      })
    }
  }
})

onNavigate() function option only gets called after the navigation has finished.

If navigation start events are of interest, one may supply onBeforeNavigate() function option, which is basically the same as onNavigate() but runs before the navigation has started. Another minor difference is that it doesn't receive the url parameter due to it not being used.

Get current location

Inside a load function: use the location parameter.

Anywhere in a React component: use useLocation() hook.

import { useLocation } from 'react-pages'

const location = useLocation()

Get Redux state for current location

One edge case is when an application is architectured in such a way that:

  • A certain page Component handles a certain route.
    • For example, an Item page component handles /items/:id URLs.
  • For that route, it is possible to navigate to the same route but with different route parameters.
    • For example, a user could navigate from /items/1 to /items/2 via a "Related items" links section.
  • The page Component has a .load() function that puts data in Redux state.
    • For example, the Item page component first fetch()es item data and then puts it in Redux state via dispatch(setItem(itemData)).
  • The page Component uses the loaded data from the Redux state.
    • For example, the Item page component gets the item data via useSelector() and renders it on the page.

In the above example, when a user navigates from item A to item B, there's a short timeframe of inconsistency:

  • Item A page renders item A data from Redux state.
  • User clicks the link to item B.
  • Item B data is fetched and put into Redux state.
  • Item A page is still rendered. useSelector() on it gets refreshed with the new data from Redux state and now returns item B data while still being on item A page.
  • The navigation finishes and item B page is rendered. useSelector() on it returns item B data.

To work around that, one could use useSelectorForLocation hook instead of useSelector.

import { useSelectorForLocation } from 'react-pages'

const propertyValue = useSelectorForLocation(state => state.reducerName.propertyName)

Get last location in the navigation chain

useNavigationLocation hook returns "navigation location" โ€” the last location (so far) in the navigation chain:

  • When a user starts navigating to a page, the "navigation location" set to that new page's location.
  • If there's an error during said navigation, the "navigation location" is reset back to the current page's location.

useSelectorForLocation hook uses useNavigationLocation under the hood.

Get current route

Inside a load function: you already know what route it is.

Anywhere in a React component: use useRoute() hook.

import { useRoute } from 'react-pages'

const route = useRoute()

A route has:

  • path โ€” Example: "/users/:id"
  • params โ€” Example: { id: "12345" }
  • location โ€” Same as useLocation()

Changing current location

To navigate to a different URL, use useNavigation() hook.

import { useNavigate, useRedirect } from 'react-pages'

// Usage example.
// * `navigate` navigates to a URL while adding a new entry in browsing history.
// * `redirect` does the same replacing the current entry in browsing history.
function Page() {
  const navigate = useNavigate()
  // const redirect = useRedirect()
  const onClick = (event) => {
    navigate('/items/1?color=red')
    // redirect('/somewhere')
  }
}

One could also pass a load: false option to navigate(location, options) or redirect(location, options) to skip the .load() function of the target page.

If the current location URL needs to be updated while still staying at the same page (i.e. no navigation should take place), then instead of redirect(location, options) one should call locationHistory.replace(location).

import { useLocationHistory } from 'react-pages'

function Page() {
  const locationHistory = useLocationHistory()

  // * `locationHistory.push(location)`
  // * `locationHistory.replace(location)`
  // * `locationHistory.go(-1)`

  const onSearch = (searchQuery) => {
    dispatch(
      locationHistory.replace({
        pathname: '/'
        query: {
          searchQuery
        }
      })
    )
  }
  return (
    <input onChange={onSearch}/>
  )
}

To go "Back" or "Forward", one could use useGoBack() or useGoForward() hooks.

import { useLocationHistory } from 'react-pages'

function Page() {
  const goBack = useGoBack()
  const goForward = useGoForward()
  return (
    <button onClick={() => goBack()}>
      Back
    </button>
  )
}

Both goBack() and goForward() functions accept an optional delta numeric argument that tells how far should it "go" in terms of the number of entries in the history. The default delta is 1.

If someone prefers to interact with found router directly then it could be accessed at any page: either as a router property of a page component or via useRouter hook.

import React from 'react'
import { useRouter } from 'react-pages'

export default function Component() {
  const { match, router } = useRouter()
  ...
}

Monitoring

For each page being rendered stats are reported if stats() parameter is passed as part of the rendering service settings.

{
  ...

  stats({ url, route, time: { load } }) {
    if (load > 1000) { // in milliseconds
      db.query('insert into server_side_rendering_stats ...')
    }
  }
}

The arguments for the stats() function are:

  • url โ€” The requested URL (without the protocol://host:port part)
  • route โ€”ย The route path (e.g. /user/:userId/post/:postId)
  • time.load โ€” The time for executing all loads.

Rendering a complex React page (having more than 1000 components) takes about 30ms (as of 2017).

One could also set up overall Server Side Rendering performance monitoring using, for example, StatsD
{
  ...

  stats({ url, route, time: { initialize, load, total } }) {
    statsd.increment('count')

    statsd.timing('initialize', initialize)
    statsd.timing('load', load)
    statsd.timing('total', total)

    if (total > 1000) { // in milliseconds
      db.query('insert into server_side_rendering_stats ...')
    }
  }
}

Where the metrics collected are

  • count โ€” rendered pages count
  • initialize โ€” server side initialize() function execution time (if defined)
  • load โ€” page loading time
  • time - total time spent loading and rendering the page

Speaking of StatsD itself, one could either install the conventional StatsD + Graphite bundle or, for example, use something like Telegraf + InfluxDB + Grafana.

Telegraf starter example:

# Install Telegraf (macOS).
brew install telegraf
# Generate Telegraf config.
telegraf -input-filter statsd -output-filter file config > telegraf.conf
# Run Telegraf.
telegraf -config telegraf.conf
# Request a webpage and see rendering stats being output to the terminal.

Hot Reload

React Hot Reload via Webpack HMR

Webpack's Hot Module Replacement (aka Hot Reload) provides the ability to "hot reload" React components.

To enable hot reload for React components, one could use a combination of react-refresh/babel Babel plugn and react-refresh-webpack-plugin Webpack plugin.

npm install @pmmmwh/react-refresh-webpack-plugin react-refresh --save-dev

.babelrc

{
  "presets": [
    "react",
    ["env", { modules: false }]
  ],

  "plugins": [
    // React "Fast Refresh".
    "react-refresh/babel"
  ]
}

webpack.config.js

import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'

export default {
  mode: 'development',
  ...,
  plugins: [
    new ReactRefreshWebpackPlugin(),
    ...
  ]
}

Then start webpack-dev-server.

webpack serve --hot --module-strict-export-presence --stats-errors --stats-error-details true --config path-to-webpack.config.js"

P.S.: Hot reload won't work for page component's load/meta functions, so when a load/meta function code is updated, the page has to be refreshed in order to observe the changes.

Redux Hot Reload via Webpack HMR

Webpack's Hot Module Replacement (aka Hot Reload) provides the ability to "hot reload" Redux reducers and Redux action creators.

Enabling "hot reload" for Redux reducers and Redux action creators is slightly more complex and requires some additional "hacky" code. The following line:

import * as reducers from './redux/reducers.js'

Should be replaced with:

import * as reducers from './redux/reducers.with-hot-reload.js'

And a new file called reducers.with-hot-reload.js should be created:

import { updateReducers } from 'react-pages'

import * as reducers from './reducers.js'

export * from './reducers.js'

if (import.meta.webpackHot) {
  import.meta.webpackHot.accept(['./reducers.js'], () => {
    updateReducers(reducers)
  })
}

And then add some additional code in the file that calls the client-side render() function:

import { render } from 'react-pages/client'

import settings from './react-pages.js'

export default async function() {
  const { enableHotReload } = await render(settings)

  if (import.meta.webpackHot) {
    enableHotReload()
  }
}

WebSocket

websocket() helper sets up a WebSocket connection.

import { render } from 'react-pages/client'
import websocket from 'react-pages/websocket'

render(settings).then(({ store }) => {
  websocket({
    host: 'localhost',
    port: 80,
    // secure: true,
    store,
    token: localStorage.getItem('token')
  })
})

If token parameter is specified then it will be sent as part of every message (providing support for user authentication).

How to use WebSocket

WebSocket will autoreconnect (with "exponential backoff") emitting open event every time it does.

After the websocket() call a global websocket variable is created exposing the following methods:

  • listen(eventName, function(event, store))
  • onOpen(function(event, store)) โ€“ is called on open event
  • onClose(function(event, store)) โ€“ is called on close event
  • onError(function(event, store)) โ€“ is called on error event (close event always follows the corresponding error event)
  • onMessage(function(message, store))
  • send(message)
  • close()

The store argument can be used to dispatch() Redux "actions".

websocket.onMessage((message, store) => {
  if (message.command) {
    switch (message.command) {
      case 'initialized':
        store.dispatch(connected())
        return console.log('Realtime service connected', message)
      case 'notification':
        return alert(message.text)
      default:
        return console.log('Unknown message type', message)
    }
  }
})

websocket.onOpen((event, store) => {
  websocket.send({ command: 'initialize' })
})

websocket.onClose((event, store) => {
  store.dispatch(disconnected())
})

The global websocket object also exposes the socket property which is the underlying robust-websocket object (for advanced use cases).

As for the server-side counterpart I can recommend using uWebSockets

import WebSocket from 'uws'

const server = new WebSocket.Server({ port: 8888 })

const userConnections = {}

server.on('connection', (socket) => {
  console.log('Incoming WebSocket connection')

  socket.sendMessage = (message) => socket.send(JSON.stringify(message))

  socket.on('close', async () => {
    console.log('Client disconnected')

    if (socket.userId) {
      userConnections[socket.userId].remove(socket)
    }
  })

  socket.on('message', async (message) => {
    try {
      message = JSON.parse(message)
    } catch (error) {
      return console.error(error)
    }

    try {
      switch (message.command) {
        case 'initialize':
          // If a user connected (not a guest)
          // then store `userId` for push notifications.
          // Using an authentication token here
          // instead of simply taking `userId` out of the `message`
          // because the input can't be trusted (could be a hacker).
          if (message.userAuthenticationToken) {
            // (make sure `socket.userId` is a `String`)
            // The token could be a JWT token (jwt.io)
            // and `authenticateUserByToken` function could
            // check the token's authenticity (by verifying its signature)
            // and then extract `userId` out of the token payload.
            socket.userId = authenticateUserByToken(message.userAuthenticationToken)

            if (!userConnections[socket.userId]) {
              userConnections[socket.userId] = []
            }

            userConnections[socket.userId].push(socket)
          }

          return socket.sendMessage({
            command: 'initialized',
            data: ...
          })

        default:
          return socket.sendMessage({
            status: 404,
            error: `Unknown command: ${message.command}`
          })
      }
    } catch (error) {
      console.error(error)
    }
  })
})

server.on('error', (error) => {
  console.error(error)
})

// Also an HTTP server is started and a REST API endpoint is exposed
// which can be used for pushing notifications to clients via WebSocket.
// The HTTP server must only be accessible from the inside
// (i.e. not listening on an external IP address, not proxied to)
// otherwise an attacker could push any notifications to all users.
// Therefore, only WebSocket connections should be proxied (e.g. using NginX).
httpServer().handle('POST', '/notification', ({ to, text }) => {
  if (userConnections[to]) {
    for (const socket of userConnections[to]) {
      socket.sendMessage({
        command: 'notification',
        text
      })
    }
  }
})

Feature: upon receiving a message (on the client side) having a type property defined such a message is dispatch()ed as a Redux "action" (this can be disabled via autoDispatch option). For example, if { type: 'PRIVATE_MESSAGE', content: 'Testing', from: 123 } is received on a websocket connection then it is automatically dispatch()ed as a Redux "action". Therefore, the above example could be rewritten as

// Server side (REST API endpoint)
socket.sendMessage({
  type: 'DISPLAY_NOTIFICATION',
  text
})

// Client side (Redux reducer)
function reducer(state, action) {
  switch (action.type) {
    case 'DISPLAY_NOTIFICATION':
      return {
        ...state,
        notifications: state.notifications.concat([action.text])
      }
    default:
      return state
  }
}

Server-Side Rendering and bundlers

If the application is being built with a bundler (most likely Webpack) and Server-Side Rendering is enabled then make sure to build the server-side code with the bundler too so that require() calls for assets (images, styles, fonts, etc) inside React components don't break (see universal-webpack, for example).

Code splitting

Code splitting is supported. See README-CODE-SPLITTING

Get cookies and HTTP headers on server side

When server-side rendering is enabled, one can pass a getInitialState() function as an option to the server-side rendering function.

That function should return an object โ€” the initial Redux state โ€” based on its parameters:

  • cookies โ€” Cookies JSON object.
  • headers โ€” HTTP request headers JSON object.
  • locales โ€” A list of locales parsed from Accept-Language HTTP header and ordered by most-preferred ones first.

For example, the application could set defaultLocale initial state property based on the Accept-Language HTTP header value, or it could set device initial state property based on the User-Agent HTTP header value.

Known Issues

Same Route Navigation

Suppose there's a "forum" web application having <Thread/> pages with URLs like /thread/:id, and one thread could link to another thread. When a user navigates to a thread and clicks a link to another thread there, a navigation transition will start: the "current" thread page will still be rendered while the "new" thread page is loading. The issue is that both these URLs use the same Redux state subtree, so, after the "new" thread data has been loaded, but before the "new" thread page is rendered, the "current" thread page is gonna re-render with the updated Redux state subtree.

If a thread page doesn't use useState(), then it wouldn't be an issue. But if it does, it could result in weird bugs. For example, if a <Thread/> page had a fromIndex state variable that would control the first shown comment index, then, when the "current" page is re-rendered with the updated Redux state subtree for the "new" thread, the fromIndex might exceed the "new" thread's comments count resulting in an "out of bounds" exception and the page breaking.

To prevent such bugs, for all routes that could link to the same route, their page components should be rendered in a wrapper with a key corresponding to all URL parameters:

function Thread() {
  const [fromIndex, setFromIndex] = useState(0)
  return ...
}

Thread.meta = ...
Thread.load = async ({ dispatch, params }) => {
  await dispatch(loadThreadData(params.id))
}

// This is a workaround for cases when navigating from one thread
// to another thread in order to prevent bugs when the "new" thread data
// has already been loaded and updated in Redux state but the "old" thread
// page is still being rendered.
// https://github.com/4Catalyzer/found/issues/639#issuecomment-567084189
export default function Thread_() {
  const thread = useSelector(state => state.thread.thread)
  return <Thread key={thread.id}/>
}
Thread_.meta = Thread.meta
Thread_.load = Thread.load

Advanced

At some point in time this README became huge so I extracted some less relevant parts of it into README-ADVANCED (including the list of all possible settings and options). If you're a first timer then just skip that one - you don't need it for sure.

License

MIT

react-pages's People

Contributors

boccob avatar catamphetamine avatar greatmercury avatar purecatamphetamine avatar rcambrj avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-pages's Issues

Where to Autohydrate Store for Redux?

I am enjoying this library, but am having difficulty figuring out the syntax for adding a store enhancer. It looks like it's simple to add new middleware, but I am thinking of adding redux-persist.

Where would I do this?

I need to add the autoRehydrate() function to the enhancer, and I assume this is done in the application.js file or the react-isomorphic-render.js file.

        .then(({ store, rerender }) => {
// begin periodically persisting the store;
// add autoRehydrate as enhancer
// call persistState(store);
  if (module.hot)
  {
    module.hot.accept('./react-isomorphic-render', () =>
    {
      store.hotReload(settings.reducer)
      rerender()
    })
  }
})```

Allow more control on the http client

I think it could be great to have one of the following option:

  • Be able to manually set another "http client" instance (to include some logic)
  • Have more control on the generated request and url formatter with callbacks or...

For example, in my project, i do have two api servers and they are not running on the same server as the webpage renderer or any other service. They have their own HTTP stack and i don't want to locally proxy to them.

Deployment Issues (AWS)

The documentation by and large is great. I am struggling with finding information about deployment.Depending on my error, it might be good to add some of this info the package docs. I am trying to deploy the isomorphic package on elastic beanstalk as is. In deploying my app, I receive a "502 Bad Gateway" error.

It also has dozens of rows of error messages more or less like this:
2017/07/05 18:02:42 [error] 5215#0: *29 connect() failed (111: Connection refused) while connecting to upstream, client: 172.31.36.70, server: , request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8081/", host: "XXX.elasticbeanstalk.com"

		application: {
			host: 'http://XXXXX.elasticbeanstalk.com/',
			port: configuration.web.port
			// secure: true
		},

Is there something I am missing that I have to do before deployment?

Make it easier to Upload Files through async middleware, or add this method to documentation?

Just spent some time figuring out how to upload files through async middleware. It can be done like this:

export function myActionCreator(nonFileFields, fileList) {
  const fileObject = fileList[0];
  const formData = new FormData();
  _.each(nonFileFields, (key, value) => {
    formData.append(key, value);
  });

  formData.append('file', fileObject);

  return {
    events: [SUBMIT, SUBMIT_SUCCESS, SUBMIT_FAIL],
    promise: (client) => client.post('/submitUrl', formData)
  };
}

Superagent lets you do something like request.post('/myurl').attach('file', fileObject). Maybe there is a way to expose an attach option through your http_client to make it easier to upload files? So that the resulting code would be something like this instead:

export function myActionCreator(nonFileFields, fileList) {
  const fileObject = fileList[0];

  return {
    events: [SUBMIT, SUBMIT_SUCCESS, SUBMIT_FAIL],
    promise: (client) => client.post('/submitUrl', nonFileFields, { attach: { file: fileObject } })
  };
}

Or maybe there is even a way to just include File/FileList objects in the data passed to client and have it automatically attach them? So resulting code like this:

export function myActionCreator(allFieldsIncludingFiles) {
  return {
    events: [SUBMIT, SUBMIT_SUCCESS, SUBMIT_FAIL],
    promise: (client) => client.post('/submitUrl', allFieldsIncludingFiles)
  };
}

IndexRedirect and Redirect not working, 'undefined' appended to all redirect locations

Seems to be caused by redirect_location.search being always undefined. This:

<Redirect from='/foo' to='/bar' />

causes redirect_location in redux/render/render_on_server to be:

{ pathname: '/bar', query: { bar: null }, state: null }

When requested with /foo?bar. Leaving out the query string entirely does not change anything except query is an empty {}.

Add a way to pass a base path aka 'basename' to createHistory?

I need to add a base path to react-isomorphic-render, so I can mount it at a subdirectory of a large app and it won't get confused.

history.js has a basename option for this reason: https://github.com/mjackson/history#using-a-base-url

The relevant place in your code to pass it would be something like source/redux/store.js (around line 101):

    store_enhancers.push
    (
        // `redux-router` middleware
        // (redux-router keeps react-router state in Redux)
        reduxReactRouter
        ({
            getRoutes : create_routes,
            createHistory: (...historyArgs) => createHistory({ ...historyArgs, basename: options.baseName || '' })
        }),

        // Ajax and @preload middleware (+ optional others)
        applyMiddleware(...middlewares)
    )

Would it be possible to add something like this?

Plug to be able to use alternate render functions with react-isomorphic render

We noticed that our renderToString function calls are taking too long (our page payload is large).

Have you come across or tried react-dom-stream?

Is it possible to provide a hook to be able to use alternate render functions like react-dom-stream or react-ssr-optimization?

PS: Thanks for all the great work - we are using a lot of it.

Redirect from within middleware

I want to redirect from within my own middleware by dispatching 'goto', but it doesn't seem to work. I see within your code you do some hacky stuff to get around this in your preload middleware, but I don't have access to that in my middleware. Maybe you could apply the user supplied middleware 'before' applying reduxRouter store enhancer, but keep applying your preload middleware after it?

Usage with Radium

I've been looking into using Radium in my codebase which is based of https://github.com/halt-hammerzeit/webpack-react-redux-isomorphic-render-example. It's pretty easy to get set up until you get to the SSR part where Radium uses the User-Agent to generate vendor prefixed CSS.

The way the suggest to configure Radium is to pass a radumConfig from the outer-most react component which here is the Wrapper. I don't see a way that is possible here. Maybe I am missing something.

Or maybe it is ill-advised to use Radium in this kind of setup?

Caching

I have an application where it makes sense to cache the entire rendered html, because there is no user authentication and data changes infrequently. I'd like to implement a simple caching strategy, where rendered output is saved to memcached/redis, and every request both serves any cached page, then renders and caches a new version for the next visitor.

Serving up a blank page is unacceptable for my application, and waiting 100ms+ for render-on-demand is similarly bad.

Is there a way you could expose hooks at both ends of the process? The pre-hook would need to be passed res and location, and the post-hook would need to be passed the location and htmlstring.

Alternatively (or additionally), if you integrate something like Rapscallion, that would also solve my issues, and seems like a robust solution that would address all the concerns you raised in your caching write-up.

Devtools "render_on_client" running twice causes some problems

In my setup I am doing some things loading 3rd party scripts. When I run in development mode, the code gets mounted twice, and this causes some of the 3rd party scripts to fail. Is there any way to prevent this? Maybe devtools could be mounted separately from the rest of the client application (like in a separate div included in the html)?

(Posted this a second ago under a different github account by accident)

Access store outside React scope

Hello, I need access Redux store to dispatch action from async callback in utils, which runs separately from react components. How can I obtain app store object.

Saga Support

Your platform has been working very nicely! I appreciate all the thought put into it.

One thing I'd love to see is Saga support. Sagas tend to be a lot easier to manage then the more 'thunk' like pattern (https://github.com/redux-saga/redux-saga), especially when you want to chain events.

I got very close to getting them working with the platform (thanks to all the middleware hooks etc). But there is a missing piece that revolves around saga's working server side: redux-saga/redux-saga#13.
Saga's Resolution: redux-saga/redux-saga#255

Essentially server side rendering and Sagas have some challenges, but they have a special 'END' event that needs to be triggered at specific point in the code.

More specifically

  1. Run Saga Middleware (Already a hook for this)
  2. renderToString happens
  3. Call dispatch(end)

Example code from the saga issue 255 linked above:

       const rootTask = sagaMiddleware.run(rootSaga)

      // this will cause the universal Saga tasks to trigger
      renderToString(
         <Root store={store} renderProps={renderProps} type="server"/>
      )
      // notify Saga that there will be no more dispatches
      // this will break the while loop of watchers
      store.dispatch(END)

Another challenge that I had mostly worked around was the fact that Saga's don't return promises by default which are required by the Preload decorators. But I created a helper file that can take care of that because when you call this.sagaMiddleware.run(common) it returns the associated promise, which I then save and can be returned in the 'preload' methods. (Tho there might be some trickery I hadn't gotten to around preload running client side?)

In summary: Saga support would be awesome :)

Pass Store to the assets function?

I want to use some value in the redux store to decide which favicon to use.
Would it be possible to pass the redux store to the assets function in webpageServer, like below? Or any other ideas how to achieve this?

import partnerIcon1 from '../../assets/images/favicons/p1favicon.ico';
import partnerIcon2 from '../../assets/images/favicons/p2favicon.ico';

export default function (parameters) {
  const server = webpageServer({
    assets: (store) => {
      const result = global.clone(parameters.chunks());

      // Webpack entry point (code splitting)
      result.entry = 'main';

      // clear Webpack require() cache for hot reload in development mode
      if (__DEVELOPMENT__) {
        delete require.cache[require.resolve('../../assets/images/favicons/favicon.ico')];
      }

      // add "favicon"
      result.icon = store.partner === 'partner1' ? partner1Icon : partner2Icon;
    }
  })
}

Trying to get the store before route creation

Hi!

I'm trying to get the proper store in the create_routes function. I need to instanciate services before creating the routes in order to be able to use them in the Route callbacks (onEnter).
Basically, my routes callbacks need the store and additionnals services.
I tried to add some logics in the react-isomorphic-render file to be able to inject my services to the create_routes function and to the wrapper. Something like that:

import { IntlProvider, addLocaleData } from 'react-intl';
import frLocaleData                    from 'react-intl/locale-data/fr';
addLocaleData(frLocaleData);

import translations from './helpers/Translations';
import { ListUrl, Seo, Api, References, Security } from 'services';

import routes  from './routes'
import Wrapper from './wrapper'

const locale     = 'en';
const host       = 'en';

const client     = new Api();
const messages   = translations(locale);
const rel        = new IntlProvider({locale, messages, children: []}, {});
const intl       = rel.getChildContext().intl;
const references = new References();

let services;

export default
{
    reducer: ()      => require('store/modules/reducer'),
    routes:  (store) => {
        const listUrl = new ListUrl(store, intl, references);
        const seo     = new Seo(intl, locale, host, listUrl);

        services = {listUrl, seo, intl, api: client, references};

        return routes(store, services.listUrl, api, intl);
    },
    wrapper: (props) => {

        const newProps = {
            ...props,
            services,
            locale,
            messages
        };

        return (<Wrapper {...newProps} />);
    },

    on_store_created({ reload_reducer }) {
        if (_development_ && module.hot) {
            module.hot.accept('./store/modules/reducer', reload_reducer)
        }
    }
}

and my routes file looks like that:

export default (store, listUrl, api, intl) => {
  const requireLogin     = hookAuth(store);
  const requireLogout    = hookNoAuth(store);
  const checkList        = hookList(store, listUrl, api);
  const checkItem        = hookItem(store, listUrl);
  const getComponentList = componentList(store, listUrl, api);

  const requireLoginAction = hookAuth(store, "actions");

  /**
   * Please keep routes in alphabetical order
   */
  return (
    <Route path="/" component={App}>
      <IndexRoute component={Home} />
      <Route component={Layout}>
        <Route path="login"    onEnter={requireLogout} component={Login} />

I don't know how to achieve this. I'd really like to move from react-isomorphic-tools to universal-webpack and your complete universal stack. Help would be really appreciate. Thanks !

Using react-router-redux

Hi, is there any way I can wrap history with syncHistoryWithStore ?

import {syncHistoryWithStore} from 'react-router-redux';

const history = syncHistoryWithStore(
    browserHistory,
    store,
  );

Thanks.

Tiniest of documentation issues

This line in the readme:

localize: (store, preferredLocales) => ({ locale: preferredLocales[0], messages: { 'page.heading': 'Test' } })

My observation is that the first argument to localize is a hash containing store, so the line should be like this:

localize: ({ store }, preferredLocales) => ({ locale: preferredLocales[0], messages: { 'page.heading': 'Test' } })

Question about third party scripts for bodyEnd + node_modules

I'm using React-Intl, which requires script references in the HTML file. Looking #25, I was able to get this working properly with the CDN references:

In code/page-server/web server.js

bodyEnd(url)
      {
        const addScripts=[];
        addScripts.push(<script key="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react.min.js" />);
        addScripts.push(<script key="reactIntl" src="https://cdnjs.cloudflare.com/ajax/libs/react-intl/1.2.2/react-intl.min.js" />);

        return addScripts;
      }

And I see the following for the resulting HTML:

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react.min.js" data-reactroot="" data-reactid="1" data-react-checksum="-2039534981"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-intl/1.2.2/react-intl.min.js" data-reactroot="" data-reactid="1" data-react-checksum="978398733"></script>

In my app code prior to webpack-react-redux-isomorphic-render-example, I used the node_module references directly in my index.html file (no server-side rendering) which worked:

<script src="../node_modules/react/dist/react.min.js"></script>
<script src="../node_modules/react-intl/dist/react-intl.min.js"></script>

I tried to use the node_module references with the webpack-react-redux-isomorphic-render-example boilerplate as follows:

      bodyEnd(url)
      {
        const addScripts=[];
        addScripts.push(<script key="react" src="../../node_modules/react/dist/react.min.js" />);
        addScripts.push(<script key="reactIntl" src="../../node_modules/react-intl/dist/react-intl.min.js" />);

        return addScripts;
      }

And I get the following for the resulting HTML:

<script src="../../node_modules/react/dist/react.min.js" data-reactroot="" data-reactid="1" data-react-checksum="-27843881"></script> <script src="../../node_modules/react-intl/dist/react-intl.min.js" data-reactroot="" data-reactid="1" data-react-checksum="-2060048737"></script>

And the following console errors:

react.min.js:1 Uncaught SyntaxError: Unexpected token <
react-intl.min.js:1 Uncaught SyntaxError: Unexpected token <

Any ideas? All help is appreciated as I'm LOVING react-isomorphic-render!

How to get html <head> to be valid html with multiple child nodes?

I have some code in my web server.js that looks like this:

import webpageServer from 'react-isomorphic-render/page-server';
import common from '../client/react-isomorphic-render';

export default function (parameters) {
  const server = webpageServer({
    html:
    {
      // Will be inserted into server rendered webpage <head/>
      // (this `head()` function is optional and is not required)
      // (its gonna work with or without this `head()` parameter)
      head: (url) => {
        const script = devtools({ ...parameters, entry: 'main' });
        return (
          <div>
            {__DEVELOPMENT__ && (
              <script dangerouslySetInnerHTML={{ __html: script }} />
            )}
            <script src="https://use.fontawesome.com/0d4417303a.js" />
          </div>
        );
      }
    },
  },
  common);

I get a complaint that <div> is not a valid child of <head>. Any idea how to work around that? Maybe the head function could return an array of nodes to be added to <head>?

Adding json-ld script tags (dynamic) in SSR pages

Hi, I need to add script tags (to indicate JSON-LD structured data) to the Head tag, dynamically for each page, just like title/description/meta tags. What is the recommended way to do this?

Example -

<script type="application/ld+json">
{
  "@context": "http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [{
    "@type": "ListItem",
    "position": 1,
    "item": {
      "@id": "https://example.com/books",
      "name": "Books",
      "image": "http://example.com/images/icon-book.png"
    }
  },{
    "@type": "ListItem",
    "position": 2,
    "item": {
      "@id": "https://example.com/books/authors",
      "name": "Authors",
      "image": "http://example.com/images/icon-author.png"
    }
  }
  }]
}
</script>

Cannot read property 'getState' of undefined in routes.js

I am trying to use onEnter, passing store as parameter in code/client/routes.js. However I see TypeError: Cannot read property 'getState' of undefined. Seems store is function getState() instead on initial loading.

Here is the code is code/client/routes.js:

export default function({store})
{
        console.log(store)
        const routes =
        (
            <Route
                component={RequireRole}
                onEnter={RequireRole.onEnter(store)}>
                <Route path="/" component={Layout} />
            </Route>
        )
    return routes
}

Thanks.

http client 'put' uses query string

Usually I send a 'put' with data using superagent's send method like this: request.send(data). You are using request.send(data) for 'post', but not for 'put' or 'patch', you are using request.query(data) instead. I'm not 100% sure but I think you should use send for 'put' and 'patch'.

In my case using the query parameter for 'put' serializes some objects that breaks communication with the api.

mark - undefined

Hi, i found this problem when I try to use the lib. Can u help me?
thanks,
gabriele

TypeError: Cannot read property 'mark' of undefined

  • web server.js:39 start_web_server
    [TIPortal]/[react-isomorphic-render]/babel-transpiled-modules/page-server/web server.js:39:61
  • render.js:9 start
    render.js:9:5
  • web.js:36
    web.js:36:29
  • es6.promise.js:89 run
    [TIPortal]/[core-js]/modules/es6.promise.js:89:22
  • es6.promise.js:102
    [TIPortal]/[core-js]/modules/es6.promise.js:102:28
  • _microtask.js:18 flush
    [TIPortal]/[core-js]/modules/_microtask.js:18:9
  • next_tick.js:67 _combinedTickCallback
    internal/process/next_tick.js:67:7
  • next_tick.js:98 process._tickCallback
    internal/process/next_tick.js:98:9

"devDependencies": {
"autoprefixer-loader": "^3.2.0",
"babel-cli": "^6.10.1",
"babel-core": "^6.10.4",
"babel-loader": "^6.2.4",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-react-transform": "^2.0.2",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-runtime": "^6.9.0",
"babel-plugin-typecheck": "^3.9.0",
"babel-polyfill": "^6.9.1",
"babel-preset-es2015": "^6.9.0",
"babel-preset-node6": "^11.0.0",
"babel-preset-react": "^6.11.1",
"babel-preset-stage-0": "^6.5.0",
"babel-runtime": "^6.9.2",

Feature request - way to set headers in the http client

This is a feature request.

Great work on this project and universal-webpack. I've been trying to migrate a project to use it today.

One thing I've run into that is stopping me is not having a way to hook into the http client when making api requests in order to add a header, while also having access to cookies server side.

In our project currently we add an Auth header with a JWT (JSON web token) when making most requests to the api, and this is based on a value stored in a cookie. I can't figure out a way to do that using the react-isomorphic-render page-server currently.

I tried using my own middleware to do the api requests instead of the asynchronous middleware provided by default, but then I am not able to access cookies server side (which you are doing in the asynchronous middleware by passing the clone_request to the http_client).

So I think the best way to address this is to expand on the http_client api to allow header setting. Maybe just allow any key/value passed into the options hash to set headers on the superagent request?

Let me know what you think!

Custom html template

Hello,

I need to use a custom html template for serving the pages, is this possible?

Http Error with error handler triggers event undefined

In the latest version (11.0.3) I am seeing this error:

TypeError: Cannot read property 'preloading' of undefined
    at asynchronous middleware.js:130

Looks to be triggered by this line, where event is undefined:
if (!server && on_error && !event.preloading) {

This is coming from dispatching an action creator that looks like this:

// I've simplied this - I am doing some logic after the dispatch. Wanted to show you that I am using thunk middleware here to dispatch the event - maybe that messes with the order of things?
export function login(email, password) {
  return (dispatch) => dispatch({
    events: [LOGIN, LOGIN_SUCCESS, LOGIN_FAIL],
    promise: client => client.post('/api/auth/authenticate_mlo/', {
      email,
      password
    })
  });
}

The server is responding with a 400 error with a response body like this:
{"errors":{"base":["Invalid username / password combination"]}}

And I have http.error defined in react-isomorphic-render like this:

  http: {
    error: (error, { redirect }) => {
      console.log('HTTP ERROR', error);
      if (error && (error.status === 401 || error.status === 403 || error.status === 419)) {
        return redirect('/logout');
      }
    }
  },

Not sure, but maybe event is undefined because I'm dispatching the action with events from thunk middleware?

ReduxMiddleware

Hi, I am trying apply redux-thunk however the reduxMiddleware seems not picked up. Would you please have a look? Thanks.

import thunkMiddleware from 'redux-thunk';
// (optional)
// User can add his own middleware to this middleware list
reduxMiddleware: () => [thunkMiddleware]

Parse Dates caused unexpected bug

Hi again! Sorry for all the issue reports.

I am passing a lot of data around in API calls, a lot of which includes some postgres database IDs. I'd never run into a problem before, but I guess today my IDs got into a certain numerical range where they started looking like dates? So they started getting parsed as dates and crashed many things.

I haven't looked at how the date parsing works, but it seems pretty dangerous if it can mistake database ids for dates. Anyway I turned off the parse dates option and now everything is working fine. Just thought I would let you know. Feel free to close this.

redux_middleware setting not working for page-server

The redux_middleware option is not being respected on the server. I tracked it down, it is a simple naming bug:

page-server/render.js is expecting the option to be passed as 'redux_middleware' (line 17), but page-server/web server.js is passing it in as 'middleware' (line 155).

Exposing superagent request.abort?

Thanks for all hard work you put into webpack comminunity and this platform!

One question regarding HTTP utility - I would like to sometimes abort/cancel requests it produces, but in current form superagent's request (or aborting function) is not exposed anywhere.

I thought about doing a simple change around here and add a static property eg:

const sendRequest = request.send().then
(
  // ....
)

sendRequest.abort = () => {
  if (request.request && typeof request.request.abort === 'function') {
    request.request.abort()
  }
}

return sendRequest;

While this would most likely work (together with replacing http client in webpack) for simple cases (chained requests will hide the abort reference), I thought it might be useful enough for you to consider adding some way of aborting/cancelling requests to the main project (especially that superagent allows it).

Besides simple/obvious use case I can see this evolving even into a custom middleware that tracks and cancels previous requests of the same type (eg. based on flag in actions).

If you have some other ideas on how to accomplish similar outcome without messing around http client then I'm all ears :)

Thanks!

Question about third party scripts

Quick question for you. Where is the best place to put third party scripts and css? I will show you some of the places I am using in my current project for some javascript, maybe you can tell me which one is preferred? Or tell me if there is a better way.

I am passing the assets option to webpageServer like below. So this is just for the webpacked javascript (and maybe css in production?). Would it make sense to include third party javascript or css here?

    assets: (ignore, { store }) => {
      // retrieves asset chunk file names
      // (which is output by client side Webpack build)
      const result = global.clone(parameters.chunks());

      // Webpack entry point (code splitting)
      result.entry = 'main';

      // clear Webpack require() cache for hot reload in development mode
      if (__DEVELOPMENT__) {
        delete require.cache[require.resolve('../../assets/images/favicons/favicon.ico')];
      }

      const state = store.getState();
      const partner = getActivePartner(state) || {};
      const faviconUrl = (partner.attributes || {}).favicon_url;

      if (faviconUrl) {
        result.icon = faviconUrl;
      } else {
        result.icon = '/favicon-check.png';
      }
      return result;
    },

Then in my root react component I am including some scripts like this (so these end up in the body):

          <span>
            <script type="text/javascript" dangerouslySetInnerHTML={{ __html: newRelicScript }}></script>
            <script src="https://cdn.optimizely.com/js/281523179.js"></script>
            [...other scripts as well]
          </span>

I am also using the "html head" parameter like this to insert a script (so this ends up in the head):

    html:
    {
      // Will be inserted into server rendered webpage <head/>
      // (this `head()` function is optional and is not required)
      // (its gonna work with or without this `head()` parameter)
      head: (url) => {
        const elements = [];

        if (__DEVELOPMENT__) {
          const script = devtools({ ...parameters, entry: 'main' });
          elements.push(
            <script key="mainjs" dangerouslySetInnerHTML={{ __html: script }} />
          );
        }
        elements.push(
          <script key="fontawesome" src="https://use.fontawesome.com/0d4417303a.js" async />
        );
        return elements;
      }
    },

Error response is not passed in asynchronous middleware

Currently only message and status code from promise error is handled and passed to redux store. It should also pass response from server, which could be parsed later.

In my application API sends some meaningful information when request is rejected. I checked and it is stored in error.response key, but asynchronous middleware.js is only using error.data, but it is undefined.

Unnecessary modules in production builds

Seems like source/redux/store.js gets included for both client and server builds, in both production and development mode. Since it requires things like redux-devtools, redux-router/server, history/lib/createMemoryHistory, those get included in production builds as well, unnecessarily increasing bundle size. The application I'm working on is quite sensitive to file size, it would be great if we could find a way to not include these in production bundles.

You can verify the issue on a fresh clone of https://github.com/halt-hammerzeit/webpack-react-redux-isomorphic-render-example, and then running webpack --colors --display-error-details --config ./webpack/webpack.config.client.production.entry.js --display-modules. If you look through the output, you'll see that it includes redux-devtools and other unnecessary dependencies.

It may be possible to work around the issue by using IgnorePlugin or an alias, I'll look into that next.

Rewrite routing

Currently it works and seems to have no issues.
Still the routing handler feels like it has too many moving parts and the code is complex.
Maybe move from the complex (and unsupported) redux-router to bare react-router some day.
But I'm not sure about react-router.
To be honest, I don't like the founders of react-router, they don't care about the users of their library, they just care for themselves and their business.
So, maybe even migrate to another router, like Found.
But I don't like the author of Found either, lol.
But still, given these two alternatives, I think Found would still be a better candidate (lesser evil).

Devtools "render_on_client" running twice causes some problems

In my setup I am doing some things loading 3rd party scripts. When I run in development mode, the code gets mounted twice, and this causes some of the 3rd party scripts to fail. Is there any way to prevent this? Maybe devtools could be mounted separately from the rest of the client application (like in a separate div included in the html)?

Expose react root component and store for testing purposes

I am trying to setup some end-to-end testing based on my project using react-isormorphic-render. I need a way to access the root react component that is getting rendered on the client, as well as the redux store that is created.

Maybe there could be a hook added, similar to on_store_created, that provides access to some of the internal objects created by react-isomorphic-render. Something like:

on_client_render_complete({store, reactRoot, router, etc...}) {
// Here I can grab the store and reactRoot and pass them to my testing framework for example
}

What do you think? Thanks for your help.

Question about redirecting in preload

For an authentication use case, I want to check if a user is authenticated when navigating to a certain page (like the "onEnter" hook in react-router). I've seen you make some references in your documentation that you can use preload in place of onEnter, which seems ideal. But I'm trying to figure out if I'm doing the redirection correctly, given how the preload function returns a Promise and I'm not clear if redirect is synchronous or asynchronous.

So I am doing something like this:

import { preload, redirect } from 'react-isomorphic-render';

@preload(({ dispatch, getState }) => {
  const { authentication } = getState();
  if (authentication.isAuthenticated) {
    // redirect the user to their dashboard if already logged in
    dispatch(redirect('/dashboard/'));
  }
  return Promise.resolve();
})
export default class Login extends Component {
  [... my Login component]
}

My question is - is that the right idea for server-side rendering? Should I be returning Promise.resolve() like that, or instead returning something else to ensure the redirect is happening server side? Everything seems to work this way but just wanted to check. Thank you!

Cookie issues moving from version 11.0.4 to 11.0.19

(Sorry I just posted this under the wrong user name)

Moving from version 11.0.4 to 11.0.19, I am now running into this error in preloading when the preload function makes an api call and no cookies are present:

"value" required in setHeader("cookie", value)
    at ClientRequest.setHeader (_http_outgoing.js:365:11)
    at Request.request (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/node_modules/superagent/lib/node/index.js:584:9)
    at Request.end (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/node_modules/superagent/lib/node/index.js:673:18)
    at /Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/build/http client.js:626:20
    at F (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/core-js/library/modules/_export.js:35:28)
    at Http_request.send (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/build/http client.js:625:11)
    at _promise2.default.then.catch_to_retry.getCookie (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/build/http client.js:253:16)
    at F (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/core-js/library/modules/_export.js:35:28)
    at perform_http_request (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/build/http client.js:251:14)
    at http_client._this.(anonymous function) [as get] (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/build/http client.js:309:13)

Any idea what is causing that? I can try to dig in more if you point me in a direction.

Hook to inject render-dependent data for SSR?

Helmetand redux stores are two examples of render-dependent data, in that they require a full render pass through renderToString to populate the store | enable rewind. These are currently being used to that effect in this project, but there aren't any hooks exposed to interact with this step of the render pipeline.

I'm using a css-in-js library which offers the same functionality: a render pass will populate its stylesheets, and you can call getCss to render a string version into the <head> for SSR.

I'm having trouble finding any hooks available that would enable me to do this. My best guess, the html.head() method, is unfortunately called before renderToString.

If there's somewhere I could inject this during SSR, please let me know. Otherwise, could you expose a post-render method to allow this sort of pattern?

Setting props on the created router

Currently the router is created from source/redux/render / source/react-router/render. I am transitioning some old code from react-redux-universal-hot-example which applies as ReduxAsyncConnect element as the render prop to Router.

It seems to me that there isn't any way to me to set props on the Router from its creation. The only object that gets to set the props is store from transitionManager.

Need to pass additional props to the router

I'm using onUpdate method on the react-router (to hit analytics).
Currently, there is no way to pass additional properties to the router.
Maybe a new option would be great ?
Something like this in the options:

router: {
    props: {
        onUpdate: () => { analytics.hit(); }    
    }
}

Optional/configurable reviver

Definitely no offense meant in any way, but I'm one of those people who really care about the raw HTML output, and I find the JSON.date_parser snippet quite annoying to look at, especially since I don't even need it right now. I was wondering if it would be possible to do one of the following:

  1. Make the reviver removable for apps that don't need it
  2. Make the reviver snippet fully configurable so that it could either be completely removed or replaced with custom content

I'm kind of feeling that the reducer should take care of converting the initial state if necessary.

Problem with Cookie moving from version 11.0.4 to 11.0.19

Moving from version 11.0.4 to 11.0.19, I am now running into this error in preloading when the preload function makes an api call and no cookies are present:

"value" required in setHeader("cookie", value)
    at ClientRequest.setHeader (_http_outgoing.js:365:11)
    at Request.request (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/node_modules/superagent/lib/node/index.js:584:9)
    at Request.end (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/node_modules/superagent/lib/node/index.js:673:18)
    at /Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/build/http client.js:626:20
    at F (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/core-js/library/modules/_export.js:35:28)
    at Http_request.send (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/build/http client.js:625:11)
    at _promise2.default.then.catch_to_retry.getCookie (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/build/http client.js:253:16)
    at F (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/core-js/library/modules/_export.js:35:28)
    at perform_http_request (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/build/http client.js:251:14)
    at http_client._this.(anonymous function) [as get] (/Users/andrewdailey/Code/mortgagehipporeact/node_modules/react-isomorphic-render/build/http client.js:309:13)

Any idea what is causing that? I can try to dig in more if you point me in a direction.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.