Repository: MoOx/phenomic (js)

Issue:
UX improvement: Even FASTER Page Loading
Right now we have the [generic loading component](https://github.com/MoOx/phenomic/blob/36b03832dde5cf3a11d1b3480f15935172697060/src/components/PageContainer/component.js#L228) which is good but I was thinking about ways to make loading appear even faster for visitors.

Facebook loads the layout with phony loading state text. Example:

![image](https://cloud.githubusercontent.com/assets/532272/19334369/46e7a1ca-90b0-11e6-9d93-e34ee50ea856.png)

The effect makes the pages feel like they are loading faster to the user

**So the idea here is:**

If you build your layout components with default loading states, we could basically show the layout instantly when a link is clicked.
1. Visitor clicks link
2. Layout XYZ with prop `phenomicLoading={true}` (or something) renders instead of default generic `<PageLoading>` component
3. After `xyz.json` data is finished fetching & loads, the real layout component just refreshes with the content
4. Visitor is flipping amazed at how fast this site is

While this doesn't technically speed up anything, the perceived site performance would be much faster.

**What do you think?**

Possible implementation: 

We could check [here](https://github.com/MoOx/phenomic/blob/36b03832dde5cf3a11d1b3480f15935172697060/src/components/PageContainer/component.js#L228) is the requested Layout has the reserved `phenomicLoading` propType defined and if so, use it as loading component. If `phenomicLoading` propType is not defined use the default generic `<PageLoading>` component

B/c this would require some work/logic in layouts, this would of course be optional and not required if people just want a generic loading state component as it works now.

Related: https://github.com/MoOx/phenomic/issues/587 


Pull request:
Optimistic loading (new way to display loading state)
Now, if you remove your ``PageLoading`` layouts, you will automatically receive partial data in the correct (future) layout (only head, body won't be defined until ``isLoading`` is != true).

![phenomic-optimistic-loading](https://cloud.githubusercontent.com/assets/157534/19846901/463a09d8-9f43-11e6-951e-283195b2a63d.gif)

Should not be a breaking change if you do still use ``PageLoading``.

If you still use ``PageLoading``, you can define a static property in some layouts to tell Phenomic to send an ``isLoading`` props to your specific layout instead of using ``PageLoading`` for this specific transition.

You can refer to [the minimal change you can do in the docs code](https://github.com/MoOx/phenomic/commit/66884e13a69ce6d30505454b763faca543c62b2c) if you want to replace ``PageLoading`` usage by optimistic loading via ``isLoading`` props.

This probably allows you to handle page transition (ref #587) since current layout won't be unmounted.

Closes #836
Closes #824

Patch:
diff --git a/docs/content/docs/getting-started.md b/docs/content/docs/getting-started.md
index 1ee14a53..9ec81193 100644
--- a/docs/content/docs/getting-started.md
+++ b/docs/content/docs/getting-started.md
@@ -145,7 +145,6 @@ So you need to register your layout in ``src/routes.js`` in the ``PhenomicPageCo
 ```js
 import Page from "./layouts/Page"
 import PageError from "./layouts/PageError"
-import PageLoading from "./layouts/PageLoading"
 import Homepage from "./layouts/Homepage"
 import Post from "./layouts/Post"
 // ↓ Add your layout here
@@ -158,7 +157,6 @@ import MySuperLayout from "./layouts/MySuperLayout"
         layouts={ {
           Page,
           PageError,
-          PageLoading,
           Homepage,
           Post,
           MySuperLayout, // ← Add your layout here
diff --git a/docs/content/docs/usage/collections.md b/docs/content/docs/usage/collections.md
index 155ab99a..4bf681f3 100644
--- a/docs/content/docs/usage/collections.md
+++ b/docs/content/docs/usage/collections.md
@@ -81,7 +81,7 @@ export default class Collection extends Component {
 
   static propTypes = {
     head: PropTypes.object.isRequired,
-    body: PropTypes.string.isRequired,
+    body: PropTypes.string,
   };
 
   static contextTypes = {
diff --git a/docs/content/docs/usage/layouts.md b/docs/content/docs/usage/layouts.md
index 008cda6e..c754e444 100644
--- a/docs/content/docs/usage/layouts.md
+++ b/docs/content/docs/usage/layouts.md
@@ -9,7 +9,6 @@ By default, Phenomic offers you the following layouts:
 
 - [`Page`](#page)
 - [`PageError`](#pageerror)
-- [`PageLoading`](#pageloading)
 
 If you use the ``PageContainer``, you will need those components in
 your `layouts` definition (by default, declared in `app/routes.js`).
@@ -22,6 +21,37 @@ title: Hello World
 layout: MyCustomLayoutComponent
 ```
 
+## `props` sent to layouts
+
+Layouts receive data handled by Phenomic. Here are the props that you can use:
+
+### ``isLoading``
+
+This boolean value tells you when a layout only have partial data
+(only the head).
+This allows you to display a content placeholder or a spinner while data are
+being downloaded.
+
+### ``head``
+
+This object contains data from the front-matter and is always available.
+You might find data such as page ``title``, ``date``, ``layout`` or whatever
+your files contains.
+
+### ``body``
+
+This data is the body of your files, and is available when content are loaded.
+You might only try to display this when ``isLoading`` is false.
+
+### ``__filename``
+
+This string contains the file path of your current resource
+(relative to project root)
+
+### ``__url``
+
+This string contains the url of your current resource.
+
 ---
 
 ## Default page layouts that Phenomic can use
@@ -43,9 +73,15 @@ it is not available.
 
 [Check out the default `PageError` and the `props` it has.](https://github.com/MoOx/phenomic/blob/master/themes/phenomic-theme-base/src/layouts/PageError/index.js)
 
-### `PageLoading`
+### ``PageLoading``
+
+_This layout is optional and deprecated, prefer handling loading state directly
+in your layouts_.
 
-_This layout is optional_.
+⚠️
+If you are using ``PageLoading`` and want to have some custom loading state for
+some layouts, you can define ``hasLoadingState`` static value on your layouts
+to tell Phenomic to use your layout loading state instead of ``PageLoading``
 
 If you are not satisfied with the default loading layout, feel free
 adjust it by grabbing some CSS or SVG spinners/loaders from:
diff --git a/docs/content/loading.md b/docs/content/loading.md
deleted file mode 100644
index b8af60b5..00000000
--- a/docs/content/loading.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-layout: PageLoading
----
-Content here not used, see ``src/layouts/PageLoading``.
-This page is a demo for loading state.
-You can safely delete it if you want.
diff --git a/docs/src/components/Footer/index.js b/docs/src/components/Footer/index.js
index 128353d3..74a4a12b 100644
--- a/docs/src/components/Footer/index.js
+++ b/docs/src/components/Footer/index.js
@@ -54,13 +54,6 @@ const Footer = () => (
       >
         { "404" }
       </Link>
-      { ", " }
-      <Link
-        className={ styles.link }
-        to="/loading/"
-      >
-        { "Loading" }
-      </Link>
     </Banner>
   </footer>
 )
diff --git a/docs/src/layouts/PageLoading/index.css b/docs/src/components/Loading/index.css
similarity index 100%
rename from docs/src/layouts/PageLoading/index.css
rename to docs/src/components/Loading/index.css
diff --git a/docs/src/layouts/PageLoading/index.js b/docs/src/components/Loading/index.js
similarity index 74%
rename from docs/src/layouts/PageLoading/index.js
rename to docs/src/components/Loading/index.js
index abc0a4a9..235ddf48 100644
--- a/docs/src/layouts/PageLoading/index.js
+++ b/docs/src/components/Loading/index.js
@@ -2,8 +2,6 @@ import React from "react"
 import Helmet from "react-helmet"
 import TopBarProgressIndicator from "react-topbar-progress-indicator"
 
-import Banner from "../../components/Banner"
-
 import styles from "./index.css"
 
 TopBarProgressIndicator.config({
@@ -14,19 +12,16 @@ TopBarProgressIndicator.config({
   shadowBlur: 5,
 })
 
-const PageLoading = () => (
+const Loading = () => (
   <div>
     <Helmet
       title={ "Loading..." }
     />
     <TopBarProgressIndicator />
-    <Banner small>
-      <h1>{ " " }</h1>
-    </Banner>
     <div className={ styles.loader }>
       <div className={ styles.spinner } />
     </div>
   </div>
 )
 
-export default PageLoading
+export default Loading
diff --git a/docs/src/layouts/Page/index.js b/docs/src/layouts/Page/index.js
index b20aa0bb..1f868477 100644
--- a/docs/src/layouts/Page/index.js
+++ b/docs/src/layouts/Page/index.js
@@ -3,6 +3,7 @@ import Helmet from "react-helmet"
 import invariant from "invariant"
 import { joinUri, BodyContainer } from "phenomic"
 
+import Loading from "../../components/Loading"
 import EditThisPage from "../../components/EditThisPage"
 import Banner from "../../components/Banner"
 import ContentWrapper from "../../components/ContentWrapper"
@@ -11,6 +12,7 @@ import styles from "./index.css"
 
 const Page = (
   {
+    isLoading,
     __filename,
     __url,
     head,
@@ -86,7 +88,9 @@ const Page = (
           </div>
         }
         {
-          body &&
+          isLoading
+          ? <Loading />
+          : body &&
           <div className={ styles.content }>
             <BodyContainer>{ body }</BodyContainer>
           </div>
@@ -100,6 +104,7 @@ const Page = (
 
 Page.propTypes = {
   children: PropTypes.node,
+  isLoading: PropTypes.boolean,
   __filename: PropTypes.string.isRequired,
   __url: PropTypes.string.isRequired,
   head: PropTypes.object.isRequired,
diff --git a/docs/src/routes.js b/docs/src/routes.js
index 5c65d895..544bdc56 100644
--- a/docs/src/routes.js
+++ b/docs/src/routes.js
@@ -5,7 +5,6 @@ import { PageContainer as PhenomicPageContainer } from "phenomic"
 import AppContainer from "./AppContainer.js"
 import Page from "./layouts/Page"
 import PageError from "./layouts/PageError"
-import PageLoading from "./layouts/PageLoading"
 import Homepage from "./layouts/Homepage"
 import Showcase from "./components/Showcase"
 
@@ -15,7 +14,6 @@ const DocsPageContainer = (props) => (
     layouts={{
       Page,
       PageError,
-      PageLoading,
       Homepage,
     }}
   />
diff --git a/src/components/PageContainer/component.js b/src/components/PageContainer/component.js
index fef89529..d9259e25 100644
--- a/src/components/PageContainer/component.js
+++ b/src/components/PageContainer/component.js
@@ -1,9 +1,14 @@
 // @flow
+
 /* eslint-disable react/sort-comp */
 import React, { Component, PropTypes } from "react"
 import { findDOMNode } from "react-dom"
 
 import urlify from "../../_utils/urlify"
+import catchLinks from "../../_utils/catch-links"
+import { browserHistory } from "../../client"
+
+const logPrefix = "phenomic: PageContainer:"
 
 type DefaultProps = {
   defaultLayout: string,
@@ -32,18 +37,6 @@ const splatToUrl = (string) => {
   return (url === "//") ? "/" : url
 }
 
-const isDevelopment = (): boolean => process.env.NODE_ENV !== "production"
-const isClient = (): boolean => typeof window !== "undefined"
-const isDevelopmentClient = (): boolean => isDevelopment() && isClient()
-
-let catchLinks
-let browserHistory
-
-if (isClient()) {
-  catchLinks = require("../../_utils/catch-links").default
-  browserHistory = require("../../client").browserHistory
-}
-
 function find(
   collection: PhenomicCollection,
   pageUrl: string
@@ -69,8 +62,7 @@ function adjustCurrentUrl(location: Object, item: Object, props: Props): void {
 
   if (currentExactPageUrl !== itemURL) {
     props.logger.info(
-      "phenomic: PageContainer: " +
-      `replacing by '${ currentExactPageUrl }' to '${ itemURL }'`
+      `${ logPrefix } replacing by '${ currentExactPageUrl }' to '${ itemURL }'`
     )
     if (browserHistory) {
       browserHistory.replace(itemURL)
@@ -102,18 +94,13 @@ class PageContainer extends Component<DefaultProps, Props, void> {
     logger: console,
   };
 
-  constructor(props: Props) {
-    super(props)
-
+  componentWillMount() {
+    const { props } = this
     if (!getLayout(props.defaultLayout, props)) {
       props.logger.error(
-        "phenomic: PageContainer: " +
-        `default layout "${ props.defaultLayout }" not provided. `
+        `${ logPrefix } default layout "${ props.defaultLayout }" not provided.`
       )
     }
-  }
-
-  componentWillMount() {
     this.preparePage(this.props, this.context)
   }
 
@@ -130,10 +117,6 @@ class PageContainer extends Component<DefaultProps, Props, void> {
   }
 
   catchInternalLink() {
-    if (!isClient()) {
-      return
-    }
-
     const layoutDOMElement = findDOMNode(this)
 
     if (layoutDOMElement) {
@@ -159,14 +142,14 @@ class PageContainer extends Component<DefaultProps, Props, void> {
 
   preparePage(props: Props, context: Context): void {
     const pageUrl = splatToUrl(props.params.splat)
-    if (isDevelopmentClient()) {
+    if (process.env.NODE_ENV !== "production") {
       props.logger.info(
-        `phenomic: PageContainer: '${ pageUrl }' rendering...`
+        `${ logPrefix } '${ pageUrl }' rendering...`
       )
     }
 
     const item = find(context.collection, pageUrl)
-    if (isClient() && item) {
+    if (typeof window !== "undefined" && item) {
       adjustCurrentUrl(window.location, item, props)
     }
 
@@ -177,7 +160,7 @@ class PageContainer extends Component<DefaultProps, Props, void> {
       }
       else {
         props.logger.error(
-          `phenomic: PageContainer: ${ pageUrl } is a page not found.`
+          `${ logPrefix } ${ pageUrl } is a page not found.`
         )
         props.setPageNotFound(pageUrl)
       }
@@ -190,9 +173,8 @@ class PageContainer extends Component<DefaultProps, Props, void> {
       const Layout = getLayout(page.type, props)
       if (page.type !== undefined && !Layout) {
         props.logger.error(
-          "phenomic: PageContainer: " +
-          `Unkown page type: "${ page.type }" component not available in ` +
-          "\"layouts\" property. " +
+          `${ logPrefix } Unkown page type: "${ page.type }"` +
+          "component not available in \"layouts\" property." +
           `Please check the "layout" or "type" of page "${ page }" header.`
         )
       }
@@ -201,18 +183,23 @@ class PageContainer extends Component<DefaultProps, Props, void> {
 
   render() {
     const { props } = this
+    const { collection } = this.context
 
     const pageUrl = splatToUrl(props.params.splat)
+    // page url from redux store
     const page = props.pages[pageUrl]
+    const partialPageHead = collection.find((pageData) => {
+      return pageUrl === pageData.__url
+    }) || {}
 
     if (!page) {
-      if (isDevelopmentClient()) {
-        props.logger.info(`phenomic: PageContainer: '${ pageUrl }' no data`)
+      if (process.env.NODE_ENV !== "production") {
+        props.logger.info(`${ logPrefix } '${ pageUrl }' no data`)
       }
-      return null
+      // return null
     }
-    if (isDevelopmentClient()) {
-      props.logger.info(`phenomic: PageContainer: '${ pageUrl }'`, page)
+    if (process.env.NODE_ENV !== "production") {
+      props.logger.info(`${ logPrefix } '${ pageUrl }'`, page)
     }
 
     if (
@@ -220,15 +207,21 @@ class PageContainer extends Component<DefaultProps, Props, void> {
       page.toString() !== "[object Object]"
     ) {
       props.logger.info(
-        `phenomic: PageContainer: page ${ pageUrl } should be an object`
+        `${ logPrefix } page ${ pageUrl } should be an object`
       )
       return null
     }
-
     const PageLoading = getLayout("PageLoading", props)
     const PageError = getLayout("PageError", props)
     const LayoutFallback = getLayout(props.defaultLayout, props)
-    const Layout = getLayout(page.type, props) || LayoutFallback
+    const Layout = getLayout(
+      (
+        partialPageHead.type || partialPageHead.layout ||
+        // page.type is head type||layout too
+        page.type
+      ),
+      props
+    ) || LayoutFallback
 
     if (page.error) {
       if (!PageError) {
@@ -241,13 +234,30 @@ class PageContainer extends Component<DefaultProps, Props, void> {
       }
       return <PageError { ...page } />
     }
-    else {
-      if (page.loading && PageLoading) {
-        return <PageLoading />
-      }
-      else if (Layout) {
-        return <Layout { ...page } />
-      }
+    else if (
+      page.isLoading &&
+      PageLoading &&
+      (Layout && !Layout.hasLoadingState)
+    ) {
+      return <PageLoading />
+    }
+    else if (Layout) {
+      return (
+        <Layout
+          // head will be overwritten by "page"
+          // (since page contains a head when loaded)
+          head={ partialPageHead }
+          { ...page }
+        />
+      )
+    }
+
+    if (process.env.NODE_ENV !== "production") {
+      return (
+        <div>
+          { "No layout can be rendered. See console for more information." }
+        </div>
+      )
     }
 
     return null
diff --git a/src/redux/modules/pages.js b/src/redux/modules/pages.js
index 9f2c2eed..33322543 100644
--- a/src/redux/modules/pages.js
+++ b/src/redux/modules/pages.js
@@ -25,7 +25,7 @@ export default function reducer(
     return {
       ...state,
       [action.page]: {
-        loading: true,
+        isLoading: true,
       },
     }
 
diff --git a/themes/phenomic-theme-base/content/index.md b/themes/phenomic-theme-base/content/index.md
index f209a7b6..0f007377 100644
--- a/themes/phenomic-theme-base/content/index.md
+++ b/themes/phenomic-theme-base/content/index.md
@@ -25,8 +25,7 @@ Built with
 
 ---
 
-You may want to take a look to the <a href="/404.html">404.html</a> page and
-the <a href="/loading/">loading</a> page.
+You may want to take a look to the <a href="/404.html">404.html</a>.
 
 If you want to adjust those, directly adjust them in ``src/layouts/``.
 
diff --git a/themes/phenomic-theme-base/content/loading.md b/themes/phenomic-theme-base/content/loading.md
deleted file mode 100644
index 34e6f160..00000000
--- a/themes/phenomic-theme-base/content/loading.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-layout: PageLoading
----
-Content here not used, see ``src/layouts/PageLoading``.
-This page is a demo for loading state.
-You can safely delete if you want.
diff --git a/themes/phenomic-theme-base/src/layouts/PageLoading/index.css b/themes/phenomic-theme-base/src/components/Loading/index.css
similarity index 100%
rename from themes/phenomic-theme-base/src/layouts/PageLoading/index.css
rename to themes/phenomic-theme-base/src/components/Loading/index.css
diff --git a/themes/phenomic-theme-base/src/layouts/PageLoading/index.js b/themes/phenomic-theme-base/src/components/Loading/index.js
similarity index 89%
rename from themes/phenomic-theme-base/src/layouts/PageLoading/index.js
rename to themes/phenomic-theme-base/src/components/Loading/index.js
index 3e44573c..ea44da07 100644
--- a/themes/phenomic-theme-base/src/layouts/PageLoading/index.js
+++ b/themes/phenomic-theme-base/src/components/Loading/index.js
@@ -12,7 +12,7 @@ TopBarProgressIndicator.config({
   shadowBlur: 5,
 })
 
-const PageLoading = () => (
+const Loading = () => (
   <div>
     <Helmet
       title={ "Loading..." }
@@ -24,4 +24,4 @@ const PageLoading = () => (
   </div>
 )
 
-export default PageLoading
+export default Loading
diff --git a/themes/phenomic-theme-base/src/layouts/Page/index.js b/themes/phenomic-theme-base/src/layouts/Page/index.js
index bc8b285f..f4a7708e 100644
--- a/themes/phenomic-theme-base/src/layouts/Page/index.js
+++ b/themes/phenomic-theme-base/src/layouts/Page/index.js
@@ -3,10 +3,13 @@ import Helmet from "react-helmet"
 import invariant from "invariant"
 import { BodyContainer, joinUri } from "phenomic"
 
+import Loading from "../../components/Loading"
+
 import styles from "./index.css"
 
 const Page = (
   {
+    isLoading,
     __filename,
     __url,
     head,
@@ -52,7 +55,11 @@ const Page = (
         <h1 className={ styles.heading }>{ head.title }</h1>
       }
       { header }
-      <BodyContainer>{ body }</BodyContainer>
+      {
+        isLoading
+        ? <Loading />
+        : <BodyContainer>{ body }</BodyContainer>
+      }
       { children }
       { footer }
     </div>
@@ -60,11 +67,12 @@ const Page = (
 }
 
 Page.propTypes = {
+  isLoading: PropTypes.boolean,
   children: PropTypes.node,
   __filename: PropTypes.string.isRequired,
   __url: PropTypes.string.isRequired,
   head: PropTypes.object.isRequired,
-  body: PropTypes.string.isRequired,
+  body: PropTypes.string,
   header: PropTypes.element,
   footer: PropTypes.element,
 }
diff --git a/themes/phenomic-theme-base/src/routes.js b/themes/phenomic-theme-base/src/routes.js
index 6e4a5bac..de64027b 100644
--- a/themes/phenomic-theme-base/src/routes.js
+++ b/themes/phenomic-theme-base/src/routes.js
@@ -5,7 +5,6 @@ import { PageContainer as PhenomicPageContainer } from "phenomic"
 import AppContainer from "./AppContainer"
 import Page from "./layouts/Page"
 import PageError from "./layouts/PageError"
-import PageLoading from "./layouts/PageLoading"
 import Homepage from "./layouts/Homepage"
 import Post from "./layouts/Post"
 
@@ -15,7 +14,6 @@ const PageContainer = (props) => (
     layouts={{
       Page,
       PageError,
-      PageLoading,
       Homepage,
       Post,
     }}


Test patch:
diff --git a/e2e-tests/__tests__/cli-build.js b/e2e-tests/__tests__/cli-build.js
index a8d974a6..559a0b1f 100644
--- a/e2e-tests/__tests__/cli-build.js
+++ b/e2e-tests/__tests__/cli-build.js
@@ -49,7 +49,6 @@ it("should build the base theme correctly",
         [
           "404.html",
           "index.html",
-          "loading/index.html",
           "posts/first-post/index.html",
           "posts/hello-world/index.html",
         ]
@@ -80,14 +79,6 @@ it("should build the base theme correctly",
         "window.__INITIAL_STATE__ = {\"pages\":{\"/\"",
       )
 
-      const htmlLoading = read(testFolder, "loading/index.html")
-      expect(htmlLoading).toContain(
-        ">Loading..."
-      )
-      expect(htmlLoading).toContain(
-        "window.__INITIAL_STATE__ = {\"pages\":{\"/loading/\"",
-      )
-
       const htmlFirstPost = read(testFolder, "posts/first-post/index.html")
       expect(htmlFirstPost).toContain(
         ">First Post"
diff --git a/src/components/PageContainer/__tests__/__snapshots__/component.js.snap b/src/components/PageContainer/__tests__/__snapshots__/component.js.snap
index 1284ddd9..04ef1be4 100644
--- a/src/components/PageContainer/__tests__/__snapshots__/component.js.snap
+++ b/src/components/PageContainer/__tests__/__snapshots__/component.js.snap
@@ -1,11 +1,29 @@
+exports[`test should be able to render a loading state 1`] = `
+<function Page(props) /* istanbul ignore next */{return (
+    props.isLoading ?
+    /* istanbul ignore next */_react2.default.createElement( /* istanbul ignore next */"div", /* istanbul ignore next */{ className: "isLoading" }) :
+    /* istanbul ignore next */_react2.default.createElement( /* istanbul ignore next */"div", /* istanbul ignore next */{ className: "Page" }));}
+  head={Object {}}
+  isLoading={true} />
+`;
+
 exports[`test should render PageError if page not found and PageError is available 1`] = `
 <function PageError() /* istanbul ignore next */{return (/* istanbul ignore next */_react2.default.createElement( /* istanbul ignore next */"div", /* istanbul ignore next */{ className: "PageError" }));}
   error="Test" />
 `;
 
-exports[`test should render a Page if page is ok 1`] = `<function Page() /* istanbul ignore next */{return (/* istanbul ignore next */_react2.default.createElement( /* istanbul ignore next */"div", /* istanbul ignore next */{ className: "Page" }));} />`;
+exports[`test should render a Page if page is ok 1`] = `
+<function Page(props) /* istanbul ignore next */{return (
+    props.isLoading ?
+    /* istanbul ignore next */_react2.default.createElement( /* istanbul ignore next */"div", /* istanbul ignore next */{ className: "isLoading" }) :
+    /* istanbul ignore next */_react2.default.createElement( /* istanbul ignore next */"div", /* istanbul ignore next */{ className: "Page" }));}
+  head={Object {}} />
+`;
 
-exports[`test should render a another page layout if defaultLayout is used 1`] = `<function AnotherPage() /* istanbul ignore next */{return (/* istanbul ignore next */_react2.default.createElement( /* istanbul ignore next */"div", /* istanbul ignore next */{ className: "AnotherPage" }));} />`;
+exports[`test should render a another page layout if defaultLayout is used 1`] = `
+<function AnotherPage() /* istanbul ignore next */{return (/* istanbul ignore next */_react2.default.createElement( /* istanbul ignore next */"div", /* istanbul ignore next */{ className: "AnotherPage" }));}
+  head={Object {}} />
+`;
 
 exports[`test should render a visible error if page is not ok and no PageError
 available 1`] = `
diff --git a/src/components/PageContainer/__tests__/component.js b/src/components/PageContainer/__tests__/component.js
index ca2113ba..d25834d0 100644
--- a/src/components/PageContainer/__tests__/component.js
+++ b/src/components/PageContainer/__tests__/component.js
@@ -2,51 +2,81 @@ import React, { createElement as jsx } from "react"
 import { createRenderer } from "react-addons-test-utils"
 
 import PageContainer from "../component"
+import dom from "../../../_utils/jsdom"
+
+// just saw PageContainer think its on the client
+dom("http://localhost/")
 
 // fixtures
 /* eslint-disable react/no-multi-comp */
 const noop = () => {}
-const Page = () => <div className="Page" />
+const Page = (props: Object) => (
+  props.isLoading
+  ? <div className="isLoading" />
+  : <div className="Page" />
+)
+Page.hasLoadingState = true
 const PageError = () => <div className="PageError" />
 const AnotherPage = () => <div className="AnotherPage" />
 
+const defaultTestProps = {
+  params: { splat: "" },
+  pages: { "/": {} },
+  getPage: noop,
+  setPageNotFound: noop,
+  layouts: { Page },
+  logger: {
+    log: () => {},
+    info: () => {},
+    error: () => {},
+  },
+}
+
+const emptyContext = {
+  collection: [],
+}
+
 it("should render a Page if page is ok", () => {
+  const renderer = createRenderer()
+  renderer.render(
+    jsx(
+      PageContainer,
+      defaultTestProps,
+    ),
+    emptyContext,
+  )
+  expect(renderer.getRenderOutput()).toMatchSnapshot()
+})
+
+it("should be able to render a loading state", () => {
   const renderer = createRenderer()
   renderer.render(
     jsx(
       PageContainer,
       {
-        params: { splat: "" },
-        pages: { "/": {} },
-        getPage: noop,
-        setPageNotFound: noop,
-        layouts: { Page },
+        ...defaultTestProps,
+        // isLoading handled by redux actions
+        pages: { "/": { isLoading: true } },
       }
     ),
-    {
-      collection: [],
-    },
+    emptyContext,
   )
   expect(renderer.getRenderOutput()).toMatchSnapshot()
 })
 
 it("should try to get a page if no page in cache", () => {
-  const renderer = createRenderer()
   const getPage = jest.fn()
   const setPageNotFound = jest.fn()
 
+  const renderer = createRenderer()
   renderer.render(
     jsx(
       PageContainer,
       {
-        params: { splat: "" },
+        ...defaultTestProps,
         pages: { },
         getPage,
         setPageNotFound,
-        layouts: { Page },
-        logger: {
-          info: () => {},
-        },
       }
     ),
     {
@@ -58,7 +88,7 @@ it("should try to get a page if no page in cache", () => {
       ],
     },
   )
-  renderer.getRenderOutput()
+
   expect(getPage).toBeCalledWith("/", "/j.son")
   expect(setPageNotFound).not.toBeCalled()
 })
@@ -70,16 +100,11 @@ available`, () => {
     jsx(
       PageContainer,
       {
-        params: { splat: "" },
+        ...defaultTestProps,
         pages: { "/": { error: "Test", errorText: "" } },
-        getPage: noop,
-        setPageNotFound: noop,
-        layouts: { Page },
       }
     ),
-    {
-      collection: [],
-    },
+    emptyContext,
   )
 
   expect(renderer.getRenderOutput()).toMatchSnapshot()
@@ -92,16 +117,12 @@ it("should render PageError if page not found and PageError is available",
     jsx(
       PageContainer,
       {
-        params: { splat: "" },
-        pages: { "/": { error: "Test" } },
-        getPage: noop,
-        setPageNotFound: noop,
+        ...defaultTestProps,
         layouts: { Page, PageError },
+        pages: { "/": { error: "Test" } },
       }
     ),
-    {
-      collection: [],
-    },
+    emptyContext,
   )
 
   expect(renderer.getRenderOutput()).toMatchSnapshot()
@@ -113,18 +134,237 @@ it("should render a another page layout if defaultLayout is used", () => {
     jsx(
       PageContainer,
       {
-        params: { splat: "" },
-        pages: { "/": {} },
-        getPage: noop,
-        setPageNotFound: noop,
+        ...defaultTestProps,
         defaultLayout: "AnotherPage",
         layouts: { AnotherPage },
       }
     ),
+    emptyContext,
+  )
+
+  expect(renderer.getRenderOutput()).toMatchSnapshot()
+})
+
+it("should notify for Page not found", () => {
+  jest.resetModules()
+  const getPage = jest.fn()
+  const setPageNotFound = jest.fn()
+  const logger = {
+    error: jest.fn(),
+    info: jest.fn(),
+  }
+
+  const renderer = createRenderer()
+  renderer.render(
+    jsx(
+      PageContainer,
+      {
+        ...defaultTestProps,
+        pages: { },
+        getPage,
+        setPageNotFound,
+        layouts: { Page },
+        logger,
+      }
+    ),
+    emptyContext,
+  )
+  expect(getPage).not.toBeCalled()
+  expect(setPageNotFound).toBeCalledWith("/")
+  expect(logger.error).toBeCalledWith(
+    "phenomic: PageContainer: / is a page not found."
+  )
+})
+
+it("should log error if default layout doesn't exits", () => {
+  jest.resetModules()
+  const logger = {
+    error: jest.fn(),
+    info: jest.fn(),
+  }
+
+  const renderer = createRenderer()
+  renderer.render(
+    jsx(
+      PageContainer,
+      {
+        ...defaultTestProps,
+        defaultLayout: "AnotherPage",
+        logger,
+      }
+    ),
+    emptyContext,
+  )
+  expect(logger.error.mock.calls.length).toBe(1)
+  expect(logger.error.mock.calls[0][0])
+    .toMatch(
+      /default layout \"AnotherPage\" not provided./
+    )
+})
+
+it("should log error if required layout doesn't exits", () => {
+  jest.resetModules()
+  const logger = {
+    error: jest.fn(),
+    info: jest.fn(),
+  }
+
+  const renderer = createRenderer()
+  renderer.render(
+    jsx(
+      PageContainer,
+      {
+        ...defaultTestProps,
+        pages: { "/": {
+          type: "SomePage",
+        } },
+        logger,
+      }
+    ),
+    emptyContext,
+  )
+  expect(logger.error.mock.calls.length).toBe(1)
+  expect(logger.error.mock.calls[0][0])
+    .toMatch(
+      /Unkown page type: \"SomePage\"/
+    )
+})
+
+it("should notify if page is not an object", () => {
+  const logger = {
+    info: jest.fn(),
+  }
+  const renderer = createRenderer()
+  renderer.render(
+    jsx(
+      PageContainer,
+      {
+        ...defaultTestProps,
+        pages: { "/": [] },
+        logger,
+      }
+    ),
+    emptyContext,
+  )
+  expect(logger.info.mock.calls[2][0])
+    .toMatch(
+      /page \/ should be an object/
+    )
+})
+
+it("should redirect if url doesn't match needed", () => {
+  dom("http://localhost/foo")
+
+  // reset browserHistory to get a correct redirect
+  jest.resetModules()
+  process.env.PHENOMIC_USER_PATHNAME = "/"
+  const PageContainer = require("../component").default
+
+  const logger = {
+    info: jest.fn(),
+  }
+
+  const renderer = createRenderer()
+  renderer.render(
+    jsx(
+      PageContainer,
+      {
+        ...defaultTestProps,
+        params: { splat: "foo/" },
+        pages: { "/foo/": {} },
+        logger,
+      }
+    ),
     {
-      collection: [],
+      collection: [ {
+        __url: "/foo/",
+      } ],
     },
   )
+  expect(logger.info.mock.calls[1][0])
+    .toMatch(
+      // replacing by '/foo' to '/foo/'
+      /replacing by \'\/foo\' to \'\/foo\/\'/
+    )
+  expect(
+    window.location.href
+  ).toEqual(
+    "http://localhost/foo/"
+  )
+})
 
-  expect(renderer.getRenderOutput()).toMatchSnapshot()
+it("should redirect and keep url parameters", () => {
+  dom("http://localhost/foo?test=yes")
+
+  // reset browserHistory to get a correct redirect
+  jest.resetModules()
+  process.env.PHENOMIC_USER_PATHNAME = "/"
+  const PageContainer = require("../component").default
+
+  const logger = {
+    info: jest.fn(),
+  }
+
+  const renderer = createRenderer()
+  renderer.render(
+    jsx(
+      PageContainer,
+      {
+        ...defaultTestProps,
+        params: { splat: "foo/" },
+        pages: { "/foo/": {} },
+        logger,
+      }
+    ),
+    {
+      collection: [ {
+        __url: "/foo/",
+      } ],
+    },
+  )
+
+  expect(logger.info.mock.calls[1][0])
+    .toMatch(
+      // replacing by '/foo?test=yes' to '/foo/?test=yes'
+      /replacing by \'\/foo\?test=yes\' to \'\/foo\/\?test=yes\'/
+    )
+  expect(
+    window.location.href
+  ).toEqual(
+    "http://localhost/foo/?test=yes"
+  )
+})
+
+it("should NOT redirect if url contains hash", () => {
+  jest.resetModules()
+  const logger = {
+    error: jest.fn(),
+    info: jest.fn(),
+  }
+
+  process.env.PHENOMIC_USER_PATHNAME = "/"
+  dom("http://localhost/foo/#some-hash")
+
+  const renderer = createRenderer()
+  renderer.render(
+    jsx(
+      PageContainer,
+      {
+        ...defaultTestProps,
+        params: { splat: "foo/" },
+        pages: { "/foo/": {} },
+        logger,
+      }
+    ),
+    {
+      collection: [ {
+        __url: "/foo/",
+      } ],
+    },
+  )
+  expect(
+    window.location.href
+  ).toEqual(
+    "http://localhost/foo/#some-hash"
+  )
 })
diff --git a/src/components/PageContainer/__tests__/component.with-spy.js b/src/components/PageContainer/__tests__/component.with-spy.js
deleted file mode 100644
index b27f01f1..00000000
--- a/src/components/PageContainer/__tests__/component.with-spy.js
+++ /dev/null
@@ -1,260 +0,0 @@
-import React, { createElement as jsx } from "react"
-import { createRenderer } from "react-addons-test-utils"
-
-import dom from "../../../_utils/jsdom"
-
-// fixtures
-/* eslint-disable react/no-multi-comp */
-const noop = () => {}
-const Page = () => <div className="Page" />
-
-it("should notify for Page not found", () => {
-  jest.resetModules()
-  const getPage = jest.fn()
-  const setPageNotFound = jest.fn()
-  const logger = {
-    error: jest.fn(),
-    info: jest.fn(),
-  }
-
-  const PageContainer = require("../component").default
-  const renderer = createRenderer()
-  renderer.render(
-    jsx(
-      PageContainer,
-      {
-        params: { splat: "" },
-        pages: { },
-        getPage,
-        setPageNotFound,
-        layouts: { Page },
-        logger,
-      }
-    ),
-    {
-      collection: [],
-    },
-  )
-  expect(getPage).not.toBeCalled()
-  expect(setPageNotFound).toBeCalledWith("/")
-  expect(logger.error).toBeCalledWith(
-    "phenomic: PageContainer: / is a page not found."
-  )
-})
-
-it("should log error if default layout doesn't exits", () => {
-  jest.resetModules()
-  const logger = {
-    error: jest.fn(),
-    info: jest.fn(),
-  }
-
-  const PageContainer = require("../component").default
-  const renderer = createRenderer()
-  renderer.render(
-    jsx(
-      PageContainer,
-      {
-        params: { splat: "" },
-        pages: { "/": {} },
-        defaultLayout: "AnotherPage",
-        getPage: noop,
-        setPageNotFound: noop,
-        layouts: { Page },
-        logger,
-      }
-    ),
-    {
-      collection: [],
-    },
-  )
-  expect(logger.error.mock.calls.length).toBe(1)
-  expect(logger.error.mock.calls[0][0])
-    .toMatch(
-      /default layout \"AnotherPage\" not provided./
-    )
-})
-
-it("should log error if required layout doesn't exits", () => {
-  jest.resetModules()
-  const logger = {
-    error: jest.fn(),
-    info: jest.fn(),
-  }
-
-  const PageContainer = require("../component").default
-  const renderer = createRenderer()
-  renderer.render(
-    jsx(
-      PageContainer,
-      {
-        params: { splat: "" },
-        pages: { "/": {
-          type: "SomePage",
-        } },
-        getPage: noop,
-        setPageNotFound: noop,
-        layouts: { Page },
-        logger,
-      }
-    ),
-    {
-      collection: [],
-    },
-  )
-  expect(logger.error.mock.calls.length).toBe(1)
-  expect(logger.error.mock.calls[0][0])
-    .toMatch(
-      /Unkown page type: \"SomePage\"/
-    )
-})
-
-it("should notify if page is not an object", () => {
-  jest.resetModules()
-  const logger = {
-    error: jest.fn(),
-    info: jest.fn(),
-  }
-  const PageContainer = require("../component").default
-  const renderer = createRenderer()
-  renderer.render(
-    jsx(
-      PageContainer,
-      {
-        params: { splat: "" },
-        pages: { "/": [] },
-        getPage: noop,
-        setPageNotFound: noop,
-        layouts: { Page },
-        logger,
-      }
-    ),
-    {
-      collection: [],
-    },
-  )
-  expect(logger.info.mock.calls[0][0])
-    .toMatch(
-      /page \/ should be an object/
-    )
-})
-
-it("should redirect if url doesn't match needed", () => {
-  jest.resetModules()
-  const logger = {
-    error: jest.fn(),
-    info: jest.fn(),
-  }
-  process.env.PHENOMIC_USER_PATHNAME = "/"
-  dom("http://localhost/foo")
-  const PageContainer = require("../component").default
-  const renderer = createRenderer()
-  renderer.render(
-    jsx(
-      PageContainer,
-      {
-        params: { splat: "foo/" },
-        pages: { "/foo/": {} },
-        getPage: noop,
-        setPageNotFound: noop,
-        layouts: { Page },
-        logger,
-      }
-    ),
-    {
-      collection: [ {
-        __url: "/foo/",
-      } ],
-    },
-  )
-  expect(logger.info.mock.calls[1][0])
-    .toMatch(
-      // replacing by '/foo' to '/foo/'
-      /replacing by \'\/foo\' to \'\/foo\/\'/
-    )
-  expect(
-    window.location.href
-  ).toEqual(
-    "http://localhost/foo/"
-  )
-})
-
-it("should redirect and keep url parameters", () => {
-  jest.resetModules()
-  const logger = {
-    error: jest.fn(),
-    info: jest.fn(),
-  }
-
-  process.env.PHENOMIC_USER_PATHNAME = "/"
-  dom("http://localhost/foo?test=yes")
-
-  const PageContainer = require("../component").default
-  const renderer = createRenderer()
-  renderer.render(
-    jsx(
-      PageContainer,
-      {
-        params: { splat: "foo/" },
-        pages: { "/foo/": {} },
-        getPage: noop,
-        setPageNotFound: noop,
-        layouts: { Page },
-        logger,
-      }
-    ),
-    {
-      collection: [ {
-        __url: "/foo/",
-      } ],
-    },
-  )
-  expect(logger.info.mock.calls[1][0])
-    .toMatch(
-      // replacing by '/foo?test=yes' to '/foo/?test=yes'
-      /replacing by \'\/foo\?test=yes\' to \'\/foo\/\?test=yes\'/
-    )
-  expect(
-    window.location.href
-  ).toEqual(
-    "http://localhost/foo/?test=yes"
-  )
-})
-
-it("should NOT redirect if url contains hash", () => {
-  jest.resetModules()
-  const logger = {
-    error: jest.fn(),
-    info: jest.fn(),
-  }
-
-  process.env.PHENOMIC_USER_PATHNAME = "/"
-  dom("http://localhost/foo/#some-hash")
-
-  const PageContainer = require("../component").default
-  const renderer = createRenderer()
-
-  renderer.render(
-    jsx(
-      PageContainer,
-      {
-        params: { splat: "foo/" },
-        pages: { "/foo/": {} },
-        getPage: noop,
-        setPageNotFound: noop,
-        layouts: { Page },
-        logger,
-      }
-    ),
-    {
-      collection: [ {
-        __url: "/foo/",
-      } ],
-    },
-  )
-  expect(
-    window.location.href
-  ).toEqual(
-    "http://localhost/foo/#some-hash"
-  )
-})
diff --git a/src/redux/modules/__tests__/pages.js b/src/redux/modules/__tests__/pages.js
index 36dcf7f1..fa9d4463 100644
--- a/src/redux/modules/__tests__/pages.js
+++ b/src/redux/modules/__tests__/pages.js
@@ -73,7 +73,7 @@ test("reducer <-- GET, should set loading state", (t) => {
 
   t.deepEqual(nextState, {
     "/foo": {
-      loading: true,
+      isLoading: true,
     },
   })
 })