Skip to content

Commit

Permalink
feat($scrollRestoration): add updateScroll calls in relevant places +…
Browse files Browse the repository at this point in the history
… edit docs
  • Loading branch information
faceyspacey committed May 2, 2017
1 parent 88120df commit 5cb3bad
Show file tree
Hide file tree
Showing 6 changed files with 35 additions and 57 deletions.
30 changes: 17 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Pure Redux Router [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square)](https://gitter.im/redux-first-router/Lobby)
# Redux First Router [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square)](https://gitter.im/redux-first-router/Lobby)

<p align="center">
<a href="https://www.npmjs.com/package/redux-first-router">
Expand Down Expand Up @@ -37,14 +37,14 @@

![redux-first-router flow chart](https://raw.githubusercontent.com/faceyspacey/redux-first-router/master/docs/redux-first-router-flow-chart.png)

At face value, the goal of **Pure Redux Router** is to think of your app in *states*--which, thanks to tools like Redux and React itself,
At face value, the goal of **Redux First Router** is to think of your app in *states*--which, thanks to tools like Redux and React itself,
so many of us have found effective--NOT *routes*; and of course while keeping the address bar in sync.

The thinking behind this package has been: "if we were to dream up a 'Redux-first' approach to routing from the
ground up, what would it look like?" The result has been what we hope you feel to be one of those
"inversion of control" scenarios that makes a challenging problem *simple* when coming at it from a different angle.

Before we get started, there is some *prior art*, and you should [check them out](./docs/prior-art.md). **Pure Redux Router**
Before we get started, there is some *prior art*, and you should [check them out](./docs/prior-art.md). **Redux First Router**
isn't the first stab at something like this, but--aside from this path being pre-validated--we feel it is the most complete, tested and *spot on* solution.
We have reviewed what came before, stripped what was unnecessary, added what was needed, and generally focused on getting the ***developer experience*** right. The best
part is that once you set it up there's virtually nothing left to do. It's truly "set it and forget it." Let's get started.
Expand All @@ -70,7 +70,7 @@ back/forward buttons. The "bi-directional" aspect is embodied in the diagram abo
points both ways--dispatching actions changes the address bar, *and* changes to
the address bar dispatches actions.

In addition, here are some key obstacles **Pure Redux Router** seeks to *avoid*:
In addition, here are some key obstacles **Redux First Router** seeks to *avoid*:

* having to render from any state that doesn't come from redux
* cluttering component code with route-oriented components
Expand Down Expand Up @@ -179,9 +179,9 @@ as keys in the payload object:

*note: if you have more keys in your payload that is fine--so long as you have the minimum required keys to populate the path*

Lastly, we haven't mentioned `redux-first-router-link`yet--**Pure Redux Router** is purposely built in
Lastly, we haven't mentioned `redux-first-router-link`yet--**Redux First Router** is purposely built in
a very modular way, which is why the `<Link />` component is in a separate package. It's extremely simple
and you're free to make your own. Basically it passes the `href` on to **Pure Redux Router** and calls
and you're free to make your own. Basically it passes the `href` on to **Redux First Router** and calls
`event.preventDefault()` to stop page reloads. It also can take an action object as a prop, which it will transform
into a URL for you! The package is obvious enough once you get the hang of what's going on here--check it
out when you're ready: [redux-first-router-link](http://github.com/faceyspacey/redux-first-router-link). And if
Expand Down Expand Up @@ -230,7 +230,9 @@ const routesMap = {
USER: { path: '/user/:slug', thunk: userThunk },
}
```
*note: visit the [location reducer docs](./docs/locationReducer) to see its shape*
> your `thunk` should return a promise for SSR to be able to `await` for its resolution and for `updateScroll()` to be called if using our [scroll restoration package](https://github.com/faceyspacey/redux-first-router-restore-scroll).
*note: visit the [location reducer docs](./docs/locationReducer) to see the `location` state's shape*

| URL | <-> | ACTION |
| ----------------------- |:---:| ----------:|
Expand All @@ -241,10 +243,12 @@ That's all folks! :+1:


## More Docs (they are short easy reads)
* [connectRoutes (there is a third `options` parameter you should check out)](./docs/connectRoutes.md)
* [action.meta (the `meta` key is how our system communicates & how our action maintains its status as an "FSA")](./docs/action.md)
* [location reducer shape](./docs/reducer.md)
* [server side rendering](./docs/server-rendering.md)
* [connectRoutes (there is a third `options` parameter you should check out)](./docs/connectRoutes.md)
* [scroll restoration](./docs/scroll-restoration.md)
* [redirects](./docs/server-rendering.md#redirects-example)

## FAQ

Expand Down Expand Up @@ -281,10 +285,10 @@ It essentially generates a fake `history` object based on the `request.path` *ex

Does this work with React Native?
> Yes, just like server side rendering, you can use the `history` package's `createMemoryHistory()` function. It's perfect for React Native's `Linking` API and push notifications in general. In fact,
if you built your React Native app already and are just starting to deal with deep-linking and push notifications, **Pure Redux Router**
if you built your React Native app already and are just starting to deal with deep-linking and push notifications, **Redux First Router**
is perfectly suited to be tacked on in final stages with very few changes.

Ok, but there's gotta be a catch--what changes should I expect to make if I start using **Pure Redux Router**?
Ok, but there's gotta be a catch--what changes should I expect to make if I start using **Redux First Router**?
> Primarily it will force you to consolidate the actions you use in your reducers. Whereas before you might have had
several actions to trigger the same state, you will now centralize on a smaller number of actions that each correspond
to a specific URL path. Your actions will become more "page-like", i.e. geared towards triggering page/URL transitions.
Expand All @@ -294,7 +298,7 @@ to the biggest visual changes in the page that we want search engines to pick up
And what about actually getting links on the page for search engines to see?
> Use [redux-first-router-link](http://github.com/faceyspacey/redux-first-router-link). This package has been built in a modular way,
which is why that's not in here. *redux-first-router-link's* `<Link />` component is simple. Review its code. Perhaps you want to make your own.
All it does is take an `href`, pass that along to **Pure Redux Router** and call `event.preventDefault()` to prevent the browser
All it does is take an `href`, pass that along to **Redux First Router** and call `event.preventDefault()` to prevent the browser
from reloading the page as it visits the new URL. The net result is you have `<a>` tags on your page for *Google* to pick up.

Why no route matching components like *React Router*?
Expand All @@ -309,11 +313,11 @@ where it seems to have a different `store` per page. That's greatly complicates
very page-like, great--but we think the whole purpose of tools like React and Redux is to build *"apps"* not *pages*.
The hallmark of an app is seamless animated transitions where you forget you're on a specific page. You need full
control of rendering to do that at the highest level. `shouldComponentUpdate`, pure functions and [reselect](https://github.com/reactjs/reselect)
become be your best friends. Everything else gets in the way. And of course **Pure Redux Router** stays out of the way.
become be your best friends. Everything else gets in the way. And of course **Redux First Router** stays out of the way.
Straightup, let us know if you think we nailed it or what we're missing. Feel free to use github issues.

Gee, I've never seen a Redux middleware/enhancer tool return so many things to use for configuring the store???
>Part of what **Pure Redux Router** does so well (and one of its considerations from the start) is server side rendering. All these
>Part of what **Redux First Router** does so well (and one of its considerations from the start) is server side rendering. All these
aspects depend on state unique to each visit/request. The returned `middleware`, `enhancer`, `reducer` and `thunk` functions share
enclosed state (i.e. within a "closure") in a *per instance* fashion. Most of the code is written as pure utility functions
and we are very proud about that. But what's not is returned to you in a way that will insure state is not shared between
Expand Down
4 changes: 2 additions & 2 deletions docs/connectRoutes.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ type Options = {
}
```

* **location** - the `location` lets you specify what key **Redux First Router** should expect its reducer to be attached to in your Redux state stree.
* **location** - the `location` lets you specify what key **Redux First Router** should expect its reducer to be attached to in your Redux state tree.

* **title** - the `title` is similarly the name of the state key for your page title. If it's provided, **Redux First Router** will change your page
* **title** - the `title` is similarly the name of the state key for your page title. **Redux First Router** will change your page
title for you when the route changes, e.g. `document.title = 'foo'`.

* **scrollTop** - the `scrollTop` option calls `window.scrollTo(0, 0)` on route changes so the user starts each page at the top. This is a *"poor man's"* scroll
Expand Down
42 changes: 3 additions & 39 deletions docs/scroll-restoration.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,21 @@
# Scroll Restoration

We have a companion package, [redux-first-router-restore-scroll](https://github.com/faceyspacey/redux-first-router-restore-scroll), which provides full-on scroll restoration through the call of a single function. It also insures hash changes work as you would expect (e.g. like when you click `#links` to different section of a Github readme it automatically scrolls, and allows you to use the browser back/next buttons to move between sections you've visited).
Complete Scroll restoration and hash `#links` handling is addressed primarily by one of our companion packages: [redux-first-router-restore-scroll](https://github.com/faceyspacey/redux-first-router-restore-scroll) *(we like to save you the bytes sent to clients if you don't need it)*. In most cases all you need to do is:

Example:

```js
import restoreScroll from 'redux-first-router-restore-scroll'

connectRoutes(history, routesMap, {
restoreScroll: restoreScroll((prev, locationState) => {
// disable scroll restoration on history state changes
// note: this is useful if you want to maintain scroll position from previous route
if (prev.type === 'HOME' && locationState.type === 'CATEGORY') {
return false
}

// scroll into view HTML element with this ID or name attribute value
else if (locationState.load && locationState.type === 'USER') {
return 'profile-box'
}


// return an array of xy coordinates to scroll there
else if (locationState.payload.coords) {
return [coords.x, coords.y]
}

// Accurately emulate the default behavior of scrolling to the top on new history
// entries, and to previous positions on pop state + hash changes.
// This is the default behavior, and this callback is not needed if this is all you want.
return true
})
})
connectRoutes(history, routesMap, { restoreScroll: restoreScroll() })
```

## Notes
Modern browsers like Chrome attempt to provide the default behavior, but we have found
it to be flakey in fact. It's pretty good in Chrome, but doesn't always happen. If all you want is the default behavior and nothing more,
simply `return true` as above or even better, just call `restoreScroll()`:

```js
import restoreScroll from 'redux-first-router-restore-scroll'
connectRoutes(history, routesMap, { restoreScroll: restoreScroll() })
```
Visit [redux-first-router-restore-scroll](https://github.com/faceyspacey/redux-first-router-restore-scroll) for more information and advanced usage.

We try to make everything as modular as possible and not making assumptions on how many bytes/packages you ship to your clients,
hence we try to break out as many such features into independent companion packages as possible.

## Scroll Restoration for Elements other than `window`
We got you covered. Please checkout [redux-first-router-scroll-container](https://github.com/faceyspacey/redux-first-router-scroll-container).

## Scroll Restoration for React Native
We got you covered! Please checkout [redux-first-router-scroll-container-native](https://github.com/faceyspacey/redux-first-router-scroll-container-native).

## Thanks
Our Scroll Restoration package comes thanks to: https://github.com/taion/scroll-behavior, which powered [react-router-scroll](https://github.com/taion/react-router-scroll) in older versions of React Router. See either for more information on how this works.
5 changes: 4 additions & 1 deletion src/connectRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,10 @@ export default (
}

const nextAction = next(action)
nextState = store.getState()

// perform various actions if a route was matched and its corresponding action dispatched:
if (route) {
nextState = store.getState()
_afterRouteChange(store, next, route)
}

Expand Down Expand Up @@ -281,6 +281,9 @@ export default (
}
}

// update the scroll position after initial rendering of page
setTimeout(_updateScroll, 0)

// dispatch the first location-aware action so initial app state is based on the url on load
if (!location.hasSSR || isServer()) {
// only dispatch on client before SSR is setup, which passes state on to the client
Expand Down
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ export {
push,
replace,
back,
next
next,
scrollBehavior,
updateScroll
} from './connectRoutes'

export const NOT_FOUND = '@@redux-first-router/NOT_FOUND'
Expand Down
7 changes: 6 additions & 1 deletion src/pure-utils/attemptCallRouteThunk.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow
import { updateScroll } from '../connectRoutes'
import type {
Dispatch,
GetState,
Expand All @@ -17,7 +18,11 @@ export default (dispatch: Dispatch, getState: GetState, route: RouteObject) => {
// without SSR setup yet, so app state is setup on client when prototyping,
// such as with with webpack-dev-server before server infrastructure is built
if (!load || (load && !hasSSR)) {
thunk(dispatch, getState)
const prom = thunk(dispatch, getState)

if (prom && typeof prom.next === 'function') {
prom.next(updateScroll)
}
}
}
}
Expand Down

0 comments on commit 5cb3bad

Please sign in to comment.