Skip to content

Commit

Permalink
feat($reactNativeScrollRestoration): add support for react native scr…
Browse files Browse the repository at this point in the history
…oll restoration + backNext infe

the `backNext` boolean flag is now added to actions and the location state using the `history`
package's memory history when the user navigates back or forward to a route he was just at, so that
scroll restoration can be implemented without browser buttons are using the exported `back` or
`next` functions.
  • Loading branch information
faceyspacey committed May 7, 2017
1 parent 01954ac commit d0f305a
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 29 deletions.
9 changes: 9 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ docs
flow-typed
src
*.log

.babelrc
.codeclimate.yml
.editorconfig
.eslintrc.js
.snyk
.travis.yml
wallaby.js
webpack.config.js
12 changes: 2 additions & 10 deletions __tests__/pure-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ describe('nestAction(pathname, receivedAction, prevLocation, history, isMiddlewa
pathname,
receivedAction,
location,
history,
true
history
) /*? $.meta */

expect(action.type).toEqual('FOO')
Expand All @@ -79,14 +78,7 @@ describe('nestAction(pathname, receivedAction, prevLocation, history, isMiddlewa
expect(action).toMatchSnapshot()

expect(action.meta.location.load).not.toBeDefined()
action = nestAction(
pathname,
receivedAction,
location,
history,
false,
'load'
)
action = nestAction(pathname, receivedAction, location, history, 'load')
expect(action.meta.location.load).toEqual(true)

// check that new paths are not pushed if pathname is the same
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "redux-first-router",
"version": "0.0.0-development",
"description": "think of your app in states not routes (and, yes, while keeping the address bar in sync)",
"main": "dist/index.js",
"main": "src/index.js",
"scripts": {
"build": "babel src -d dist",
"build:umd": "BABEL_ENV=commonjs NODE_ENV=development webpack src/index.js dist/redux-first-router.js",
Expand Down
2 changes: 1 addition & 1 deletion src/action-creators/historyCreateAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export default (
kind: string
): Action => {
const action = pathToAction(pathname, routesMap)
return nestAction(pathname, action, prevLocation, history, false, kind)
return nestAction(pathname, action, prevLocation, history, kind)
}
49 changes: 46 additions & 3 deletions src/action-creators/middlewareCreateAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,62 @@ export default (
): Action => {
try {
const pathname = actionToPath(action, routesMap)
return nestAction(pathname, action, prevLocation, history, true)
const kind = getKind(pathname, history)
return nestAction(pathname, action, prevLocation, history, kind)
}
catch (e) {
// developer dispatched an invalid type + payload
// preserve previous pathname to keep app stable for future correct actions that depend on it
const pathname = prevLocation.pathname
const payload = { ...action.payload }

return nestAction(
pathname,
{ type: NOT_FOUND, payload },
prevLocation,
history,
true
history
)
}
}

const getKind = (pathname: string, history: History): ?string => {
const isMemoryHistory = !!history.entries

if (!isMemoryHistory) {
return undefined
}

const isLast = history.index === history.length - 1
const prev = history.entries[history.index - 1]
// REACT NATIVE FEATURE:
// emulate npm `history` package and `historyCreateAction` so that actions
// and state indicate the user went back or forward. The idea is if you are
// going back or forward to a route you were just at, apps can determine
// from `state.location.backNext` and `action.backNext` that things like
// scroll position should be restored.
if (isLast && prev) {
const prevPath = prev.pathname
const isGoingBack = prevPath === pathname

if (isGoingBack) {
history.index--
return 'backNext'
}

return undefined
}

const next = history.entries[history.index + 1]

if (next) {
const nextPath = next.pathname
const isGoingForward = nextPath === pathname

if (isGoingForward) {
history.index++
return 'backNext'
}
}

return undefined
}
29 changes: 22 additions & 7 deletions src/connectRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,7 @@ export default (
pathname,
{ type: NOT_FOUND, payload },
prevLocation,
history,
true
history
)
prevLocation = action.meta.location.current
}
Expand Down Expand Up @@ -224,12 +223,21 @@ export default (
title: ?string,
history: History
) => {
// IMPORTANT: insure history hasn't already handled location change
if (locationState.pathname !== currentPathname) {
// IMPORTANT: insure history hasn't already handled location change
currentPathname = locationState.pathname // IMPORTANT: must happen before history.push() (to prevent double handling)

const method = locationState.redirect ? 'replace' : 'push'
history[method](currentPathname) // change address bar corresponding to matched actions from middleware
// for React Native `middlewareCreateAction` may emulate
// `history` backNext actions to support features such
// as scroll restoration, in case `back` or `next` is
// not called directly. In those cases, we need to prevent
// pushing new routes on to the entries array.
const manuallyInvoked = locationState.backNext

if (!manuallyInvoked) {
const method = locationState.redirect ? 'replace' : 'push'
history[method](currentPathname) // change address bar corresponding to matched actions from middleware
}

changePageTitle(windowDocument, title)
}
Expand Down Expand Up @@ -282,7 +290,9 @@ export default (
}

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

// dispatch the first location-aware action so initial app state is based on the url on load
if (!location.hasSSR || isServer()) {
Expand All @@ -303,7 +313,8 @@ export default (

const _historyAttemptDispatchAction = (
store: Store<*, *>,
location: HistoryLocation
location: HistoryLocation,
historyAction: string
) => {
if (location.pathname !== currentPathname) {
// IMPORTANT: insure middleware hasn't already handled location change
Expand Down Expand Up @@ -378,6 +389,10 @@ export const back = () => _history.goBack()

export const next = () => _history.goForward()

export const go = (n: number) => _history.go(n)

export const canGo = (n: number) => _history.canGo(n)

export const scrollBehavior = () => _scrollBehavior

export const updateScroll = () => _updateScroll && _updateScroll()
6 changes: 5 additions & 1 deletion src/flow-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,14 @@ export type ReceivedAction = {
meta?: Meta
}

export type Listener = HistoryLocation => void
export type Listener = (HistoryLocation, HistoryAction) => void
export type Listen = Listener => void
export type Push = (pathname: string) => void
export type Replace = (pathname: string) => void
export type GoBack = () => void
export type GoForward = () => void
export type Go = number => void
export type CanGo = number => boolean

export type History = {
listen: Listen,
Expand All @@ -100,6 +101,7 @@ export type History = {
goBack: GoBack,
goForward: GoForward,
go: Go,
canGo: CanGo,
entries: Array<{ pathname: string }>,
index: number,
length: number,
Expand All @@ -112,4 +114,6 @@ export type HistoryLocation = {
pathname: string
}

export type HistoryAction = string

export type Document = Object
22 changes: 16 additions & 6 deletions src/pure-utils/nestAction.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
// @flow
import type { Action, Location, ReceivedAction, History } from '../flow-types'
import type {
Action,
Location,
ReceivedAction,
History,
HistoryData
} from '../flow-types'

export default (
pathname: string,
receivedAction: ReceivedAction,
prev: Location,
history: History,
isMiddleware: boolean,
kind?: string
kind?: ?string
): Action => {
const { type, payload = {}, meta } = receivedAction

Expand All @@ -28,14 +33,19 @@ export default (
redirect: meta && meta.location && meta.location.redirect
? pathname
: undefined,
history: getHistory(pathname, history, isMiddleware)
history: getHistory(!!history.entries, pathname, history, !kind)
}
}
}
}

const getHistory = (pathname, history, isMiddleware) => {
if (!history.entries) {
const getHistory = (
isMemoryHistory: boolean,
pathname: string,
history: History,
isMiddleware: boolean
): ?HistoryData => {
if (!isMemoryHistory) {
return undefined
}

Expand Down

0 comments on commit d0f305a

Please sign in to comment.