Created
October 2, 2016 19:45
-
-
Save ryanflorence/a9019d1a9be6b0e9217f41c8280bfa26 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*eslint no-console: 0*/ | |
import React, { PropTypes } from 'react' | |
import { historyContext as historyContextType } from 'react-history/PropTypes' | |
import StaticRouter from './StaticRouter' | |
// intitial key is `null` because JSON.stringify turns undefined into null, and | |
// we use this value as the "initial key" | |
const initialKeys = [ null ] | |
const IS_DOM = typeof window !== 'undefined' | |
const findKeyIndex = (keys, key) => ( | |
// switchup `undefined` to `null` because that's what JSON.stringify does | |
// when we persist to sessionStorage | |
keys.indexOf(key || null) | |
) | |
const restoreKeys = () => { | |
if (IS_DOM) { | |
try { | |
return JSON.parse(sessionStorage.ReactRouterKeys) | |
} catch(e) { | |
// couldn't find them, so send the default | |
return initialKeys | |
} | |
} else { | |
return initialKeys | |
} | |
} | |
const locationsAreEqual = (a, b) => { | |
// If we're on the initial entry and we get a new location descriptor then | |
// neither will have a key, so we compare the path. We have to guard on the | |
// keys so we allow pushing the same path w/ a different key. | |
if (a.key == null && b.key == null) | |
return a.path === b.path | |
// the initial entry has no key, and neither does a pushed location | |
// descriptor, so we use identity for that case | |
if (a === b) | |
return true | |
// locations lose their identity after leaving the domain, so we use the key | |
// instead, all locations but the first will have a key | |
if (a.key === b.key) | |
return true | |
return false | |
} | |
const saveKeys = (keys) => { | |
if (IS_DOM) { | |
try { | |
sessionStorage.ReactRouterKeys = JSON.stringify(keys) | |
} catch(e) {} // eslint-disable-line | |
} | |
} | |
class ControlledHistory extends React.Component { | |
static propTypes = { | |
children: PropTypes.func.isRequired, | |
history: PropTypes.object.isRequired, | |
location: PropTypes.object.isRequired, | |
action: PropTypes.string.isRequired, | |
onChange: PropTypes.func.isRequired, | |
restoreKeys: PropTypes.func.isRequired, | |
saveKeys: PropTypes.func.isRequired | |
} | |
static childContextTypes = { | |
history: historyContextType.isRequired | |
} | |
static defaultProps = { | |
restoreKeys, | |
saveKeys | |
} | |
getChildContext() { | |
return { | |
history: this.getHistoryContext() | |
} | |
} | |
getHistoryContext() { | |
const { action, location, history } = this.props | |
return { | |
action, | |
location, | |
...history | |
} | |
} | |
constructor(props) { | |
super(props) | |
const location = props.history.getCurrentLocation() | |
const cameBackFromOtherDomain = !!location.key | |
this.updatingFromHistoryChange = false | |
this.syncingHistory = false | |
this.syncingReplace = false | |
this.keys = cameBackFromOtherDomain ? props.restoreKeys() : initialKeys | |
this.setupHistory() | |
this.state = { | |
location, | |
action: 'POP' | |
} | |
} | |
componentWillReceiveProps(nextProps) { | |
if ( | |
!this.syncingReplace && | |
!this.updatingFromHistoryChange && | |
!locationsAreEqual( | |
nextProps.location, | |
this.props.location | |
) | |
) { | |
this.syncingHistory = true | |
const { history } = this.props | |
const { action, location } = nextProps | |
const nextIndex = findKeyIndex(this.keys, location.key) | |
if (location.key && nextIndex !== -1) { | |
// we've been here before | |
const currentIndex = findKeyIndex(this.keys, this.props.location.key) | |
const delta = nextIndex - currentIndex | |
history.go(delta) | |
} else if (action === 'PUSH') { | |
history.push(location.path, location.state) | |
} else if (action === 'REPLACE') { | |
history.replace(location.path, location.state) | |
} | |
} | |
} | |
setupHistory() { | |
this.props.history.listen((location, action) => { | |
this.storeKey(location.key, action) | |
this.updatingFromHistoryChange = true // must come before onChange! | |
if (!this.syncingHistory) { | |
this.props.onChange(location, action) | |
} | |
if (this.syncingReplace) { | |
this.props.onChange(location, 'SYNC') | |
} | |
this.setState({ location, action }, () => { | |
this.updatingFromHistoryChange = false | |
if (this.syncingHistory) { | |
this.syncingHistory = false | |
} else { | |
this.checkIfLocationAccepted() | |
} | |
}) | |
}) | |
} | |
checkIfLocationAccepted() { | |
const { location } = this.props | |
const { location:stateLocation, action:stateAction } = this.state | |
if (!locationsAreEqual(location, stateLocation)) { | |
this.syncingHistory = true | |
const index = findKeyIndex(this.keys, location.key) | |
const stateIndex = findKeyIndex(this.keys, stateLocation.key) | |
const delta = index - stateIndex | |
if (stateAction === 'REPLACE') { | |
// Gah! the app is now going to have the right path, the right number | |
// of entries in the history, the right index in the history, but not | |
// the right key :(. | |
// | |
// Once the browser replaces, that's it! we can't stop it, and we | |
// can't ever get that location back. So we'll call props.onChange in | |
// history.listen to let the app synchronize with us (which it MUST do, | |
// it must always accept a 'SYNC' action location into its state) | |
this.syncingReplace = true | |
this.props.history.replace(location.path, location.state) | |
} else { | |
if (stateIndex === -1) { | |
// playing whack-a-mole here D: after we pop off the last key a | |
// few lines down, if they click "forward" we won't find the key | |
// so let's just do a -1 for delta. | |
this.props.history.go(-1) | |
} else { | |
this.props.history.go(delta) | |
} | |
if (stateAction === 'PUSH') { | |
// get rid of the last entry so our delta isn't off if they try to | |
// push here again | |
this.keys.pop() | |
} | |
} | |
} | |
} | |
storeKey(key, action) { | |
if (action === 'PUSH') { | |
this.keys.push(key) | |
} else if (action === 'REPLACE') { | |
this.keys[this.keys.length - 1] = key | |
} | |
// browsers only keep 50 entries, so we'll do that too | |
if (this.keys.length > 50) | |
this.keys.unshift() | |
this.props.saveKeys(this.keys) | |
} | |
render() { | |
return this.props.children(this.getHistoryContext()) | |
} | |
} | |
const ControlledRouter = ({ history, location, onChange, action, restoreKeys, saveKeys, ...rest }) => ( | |
<ControlledHistory | |
history={history} | |
location={location} | |
onChange={onChange} | |
action={action} | |
restoreKeys={restoreKeys} | |
saveKeys={saveKeys} | |
> | |
{(history) => ( | |
<StaticRouter | |
action={history.action} | |
location={history.location} | |
onPush={history.push} | |
onReplace={history.replace} | |
blockTransitions={history.block} | |
{...rest} | |
/> | |
)} | |
</ControlledHistory> | |
) | |
ControlledRouter.propTypes = { | |
history: PropTypes.object.isRequired, | |
location: PropTypes.object.isRequired, | |
onChange: PropTypes.func.isRequired, | |
action: PropTypes.string.isRequired, | |
restoreKeys: PropTypes.func, | |
saveKeys: PropTypes.func, | |
children: PropTypes.oneOfType([ | |
PropTypes.func, | |
PropTypes.node | |
]) | |
} | |
export default ControlledRouter |
I have a similar problem with 4.0.0-alpha.4, where 4.0.0-alpha.3 just works. Browser stuck in an infinite redirection back and forth redirected URL.
Also, I don't get why restoreKeys with sessionStorage is needed, what about React Native?
don't have time to explain much
- replace
<BrowserRouter/>
with<ControlledRouter/>
, they don't work together they are are exclusive - storage is required for when you leave the domain, it's a pluggable prop, so just no-op it for react native
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks! Gonna try this :) Can you provide an example usage please ?
Edit: can't seem to make it work. Used it like this:
First error I had was that
getCurrentLocation
doesn't exist inhistory
, so changed it with just.location
(l98).But when I try to manually change the location via dispatching
setLocation
, I get an infinite render cycle (ending up in a Maximum call stack size exceeded) :(Any help is welcome :)