Skip to content

Instantly share code, notes, and snippets.

@Jessidhia
Last active November 10, 2018 12:55
Show Gist options
  • Save Jessidhia/3be0f3c9f348f58fc9938652a7f04273 to your computer and use it in GitHub Desktop.
Save Jessidhia/3be0f3c9f348f58fc9938652a7f04273 to your computer and use it in GitHub Desktop.
// 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