Skip to content

Instantly share code, notes, and snippets.

@StreetStrider
Last active November 23, 2023 15:49
Show Gist options
  • Save StreetStrider/c1f4a1eaa26bebaab5b7c2a881b851d9 to your computer and use it in GitHub Desktop.
Save StreetStrider/c1f4a1eaa26bebaab5b7c2a881b851d9 to your computer and use it in GitHub Desktop.
// ISC © 2023, Strider.
/* eslint-disable complexity */
const values = Object.values
import { useEffect, useRef } from 'react'
import { useParams } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useNavigate } from 'react-router-dom'
import { generatePath } from 'react-router-dom'
import { State } from './State'
export type Config =
{
path: string,
mapping: Mappings,
states: States,
}
export type Mappings = Record<string, Mapping<any>>
export type Mapping <T> =
{
load (uri: string): T,
dump (value: T): string | null,
fail? (e: Error): string | void,
}
export type States = Record<string, State<any>>
export default function urimap (config: Config)
{
const path = config.path
const mapping = config.mapping
const states = config.states
const keys = path.match(/:\w+/g)!.map(key => key.slice(1))
const params = useParams()
const location = useLocation()
const navigate = useNavigate()
const first = useRef(true)
useEffect(() =>
{
for (const key of keys)
{
const mapper = mapping[key]
const state = states[key]
const repr = mapper.dump(state.value)
const repr_uri = (params[key] as string)
if (repr_uri === repr) continue
try
{
var value = mapper.load(repr_uri)
}
catch (e)
{
const fail = mapper.fail
if (! fail)
{
throw e
}
const redirect = fail(e as Error)
if (redirect)
{
navigate(redirect, { replace: true })
}
return
}
state.set(value)
}
}
, [ params ])
//, values(params))
useEffect(() =>
{
if (first.current)
{
first.current = false
return
}
const reprs: Record<string, string | null> = {}
for (const key of keys)
{
const mapper = mapping[key]
const state = states[key]
reprs[key] = mapper.dump(state.value)
if (reprs[key] === null)
{
return
}
}
// try
// {
var path_to = generatePath(path, reprs as any)
// }
/*catch (e)
{
console.error('urimap: cannot generatePath(?)', path, reprs)
return
}*/
if (location.pathname !== path_to)
{
const replace = trailing_slash_fixing(location.pathname, path_to)
navigate(path_to, { replace })
}
}
, values(states))
}
function trailing_slash_fixing (prev: string, next: string)
{
if (prev === next) return true
const L1 = prev.length
const L2 = next.length
if (Math.abs(L1 - L2) > 1) return false
const min = Math.min(L1, L2)
if (prev.slice(0, min) !== next.slice(0, min)) return false
if (L1 > L2)
{
return (prev.slice(min) === '/')
}
else
{
return (next.slice(min) === '/')
}
}
@StreetStrider
Copy link
Author

  • urimap allows to register multiple Mappings one for each variable in path (/:var/)
  • each Mapping knows how to load/dump value to/from the corresponding state
  • urimap consists of two useEffects, one maps state → uri, other maps uri → state
  • there is a shortcut to prevent setState cycles: if repr_uri equals to current state's repr there should be no setState
  • in the first cycle uri loading gets upper hand, since it must populate the state with initial values, so the opposite is ignored once
  • there is fail recovery if user puts malformed values. we can conditionally redirect to some default uri
  • it removes trailing slash difference (for convenience)
  • urimap actually follows rules of hooks, I guess, most devs would like to rename it to something with use in the name

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment