From 5cb3bad0fd5a9b27edc7c2263b0c591dea6c0b6e Mon Sep 17 00:00:00 2001 From: James Gillmore Date: Tue, 2 May 2017 01:46:40 -0700 Subject: [PATCH] feat($scrollRestoration): add updateScroll calls in relevant places + edit docs --- README.md | 30 ++++++++++-------- docs/connectRoutes.md | 4 +-- docs/scroll-restoration.md | 42 ++----------------------- src/connectRoutes.js | 5 ++- src/index.js | 4 ++- src/pure-utils/attemptCallRouteThunk.js | 7 ++++- 6 files changed, 35 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index b17fc7ed..1c866848 100644 --- a/README.md +++ b/README.md @@ -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)

@@ -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. @@ -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 @@ -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 `` 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 @@ -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 | | ----------------------- |:---:| ----------:| @@ -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 @@ -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. @@ -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* `` 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 `` tags on your page for *Google* to pick up. Why no route matching components like *React Router*? @@ -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 diff --git a/docs/connectRoutes.md b/docs/connectRoutes.md index 372d0750..7b4cdf7f 100644 --- a/docs/connectRoutes.md +++ b/docs/connectRoutes.md @@ -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 diff --git a/docs/scroll-restoration.md b/docs/scroll-restoration.md index 0561e605..03cbe8d8 100644 --- a/docs/scroll-restoration.md +++ b/docs/scroll-restoration.md @@ -1,51 +1,17 @@ # 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). @@ -53,5 +19,3 @@ We got you covered. Please checkout [redux-first-router-scroll-container](https: ## 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. diff --git a/src/connectRoutes.js b/src/connectRoutes.js index e7504315..3522ebb6 100644 --- a/src/connectRoutes.js +++ b/src/connectRoutes.js @@ -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) } @@ -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 diff --git a/src/index.js b/src/index.js index dfaabb6d..776f0581 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,9 @@ export { push, replace, back, - next + next, + scrollBehavior, + updateScroll } from './connectRoutes' export const NOT_FOUND = '@@redux-first-router/NOT_FOUND' diff --git a/src/pure-utils/attemptCallRouteThunk.js b/src/pure-utils/attemptCallRouteThunk.js index 9e19f83d..8bc83b91 100644 --- a/src/pure-utils/attemptCallRouteThunk.js +++ b/src/pure-utils/attemptCallRouteThunk.js @@ -1,4 +1,5 @@ // @flow +import { updateScroll } from '../connectRoutes' import type { Dispatch, GetState, @@ -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) + } } } }