Last active
November 10, 2018 12:55
-
-
Save Jessidhia/3be0f3c9f348f58fc9938652a7f04273 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
// A simplified replacement for react-router-redux | |
// that works with react-redux@^6 and with react-router@^4.4 | |
// Uses hooks, so requires react@^16.7 | |
import { | |
Action as LocationAction, | |
History, | |
Location, | |
LocationDescriptor, | |
LocationDescriptorObject, | |
LocationState, | |
Path | |
} from 'history' | |
import React from 'react' | |
import { ReactReduxContext } from 'react-redux' | |
import { Router } from 'react-router' | |
import { AnyAction, Dispatch, Middleware, Store } from 'redux' | |
declare module 'react-redux' { | |
export const ReactReduxContext: React.Context<{ | |
store: Store<any> | |
storeState: any // unfortunately, consts can't be generic so this has to be any | |
} | null> | |
} | |
const enum ActionNames { | |
UpdateLocation = '##router/UPDATE_LOCATION', | |
RouterSideEffect = '##router/SIDE_EFFECT' | |
} | |
export const UpdateLocationAction = ActionNames.UpdateLocation | |
interface UpdateLocationPayload { | |
location: Location | |
action: LocationAction | |
} | |
interface RouterSideEffectPayload { | |
effect: LocationAction | |
path: LocationDescriptor | undefined | |
state: LocationState | undefined | |
} | |
export type UpdateLocationAction = TypedAction< | |
ActionNames.UpdateLocation, | |
UpdateLocationPayload | |
> | |
type RouterSideEffectAction = TypedAction< | |
ActionNames.RouterSideEffect, | |
RouterSideEffectPayload | |
> | |
type Actions = UpdateLocationAction | RouterSideEffectAction | |
function updateLocation( | |
location: Location, | |
action: LocationAction | |
): UpdateLocationAction { | |
return { | |
type: ActionNames.UpdateLocation, | |
payload: { location, action } | |
} | |
} | |
function routerSideEffect( | |
effect: LocationAction, | |
path?: LocationDescriptor, | |
state?: LocationState | |
): RouterSideEffectAction { | |
return { | |
type: ActionNames.RouterSideEffect, | |
payload: { | |
effect, | |
path, | |
state | |
} | |
} | |
} | |
export function push(path: Path, state?: LocationState): RouterSideEffectAction | |
export function push(path: LocationDescriptorObject): RouterSideEffectAction | |
export function push( | |
path: Path | LocationDescriptorObject, | |
state?: LocationState | |
) { | |
return routerSideEffect('PUSH', path, state) | |
} | |
export function replace( | |
path: Path, | |
state?: LocationState | |
): RouterSideEffectAction | |
export function replace(path: LocationDescriptorObject): RouterSideEffectAction | |
export function replace( | |
path: Path | LocationDescriptorObject, | |
state?: LocationState | |
) { | |
return routerSideEffect('REPLACE', path, state) | |
} | |
export function pop(): RouterSideEffectAction { | |
return routerSideEffect('POP') | |
} | |
export interface State { | |
location: Location | null | |
} | |
export function getLocation(root: { router: State }) { | |
if (root.router.location === null) { | |
throw new Error('The redux store is missing the correct middleware') | |
} | |
return root.router.location | |
} | |
export function reducer( | |
state: State = { location: null }, | |
action: Actions | UnknownAction | |
): State { | |
switch (action.type) { | |
case ActionNames.UpdateLocation: | |
return { location: action.payload.location } | |
case ActionNames.RouterSideEffect: | |
return state | |
default: | |
unknownAction(action) | |
} | |
return state | |
} | |
export function ConnectedRouter({ | |
history, | |
children | |
}: { | |
history: History | |
children?: React.ReactNode | |
}) { | |
const redux = React.useContext(ReactReduxContext) | |
if (redux === null) { | |
throw new Error('ConnectedRouter mounted outside a redux Provider') | |
} | |
const store: Store<typeof storeState> = redux.store | |
const storeState: { router: State } = redux.storeState | |
if ((storeState.router as unknown) === undefined) { | |
throw new Error('The redux store is missing the `router` reducer') | |
} | |
if (storeState.router.location === null) { | |
throw new Error('The redux store is missing the correct middleware') | |
} | |
const fabricated = React.useRef(false) | |
React.useEffect( | |
() => | |
history.listen((location, action) => { | |
if (!fabricated.current) { | |
store.dispatch(updateLocation(location, action)) | |
} | |
}), | |
[store, history] | |
) | |
React.useEffect( | |
() => | |
// this is its own subscription, instead of just an effect that reads storeState, | |
// so we can update the router synchronously when there is a change | |
store.subscribe(() => { | |
const location = getLocation(store.getState()) | |
// should only happen when devtools are being used to manipulate the action history | |
if (location.key !== history.location.key) { | |
// fabricate a push to get the address bar / the Router itself to see the current path | |
fabricated.current = true | |
history.push(location) | |
fabricated.current = false | |
} | |
}), | |
[store, history] | |
) | |
return React.useMemo(() => <Router history={history}>{children}</Router>, [ | |
history, | |
children | |
]) | |
} | |
ConnectedRouter.displayName = 'ConnectedRouter' | |
export function middleware( | |
history: History | |
): Middleware<{}, { router: State }, Dispatch> { | |
return api => { | |
let inited = false | |
return next => action => { | |
if (inited === false) { | |
inited = true | |
api.dispatch(updateLocation(history.location, 'POP')) | |
} | |
if (isRouterSideEffect(action)) { | |
const { effect, path, state } = action.payload | |
switch (effect) { | |
case 'POP': | |
history.goBack() | |
break | |
case 'PUSH': | |
if (typeof path === 'string') { | |
history.push(path, state) | |
} else { | |
history.push(path!) | |
} | |
break | |
case 'REPLACE': | |
if (typeof path === 'string') { | |
history.replace(path, state) | |
} else { | |
history.replace(path!) | |
} | |
break | |
default: | |
unreachable(effect) | |
} | |
return | |
} | |
return next(action) | |
} | |
} | |
} | |
function isRouterSideEffect(action: any): action is RouterSideEffectAction { | |
return ( | |
typeof action === 'object' && | |
action !== null && | |
(action as AnyAction).type === ActionNames.RouterSideEffect | |
) | |
} | |
function unreachable(thing: never): never { throw new Error('unreachable') } | |
interface TypedAction<T extends string, P> { | |
type: T | |
payload: P | |
} | |
type UnknownAction = { type: '' } | |
function unknownAction(_action: UnknownAction) { | |
// empty | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment