npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@reatom/url

v3.8.0

Published

Reatom for url

Downloads

1,250

Readme

This package provides a set of helpers to be sync with the url, search parameters, change it and react to it changes.

Installation

npm i @reatom/url

Usage

Base primitive is urlAtom, it contains URL and by default initiates with a browser location.href (see Behavior for details).

You can call the urlAtom.go action with a root path to navigate programmatically.

Search parameters

There is also searchParamsAtom which derives from urlAtom and allow you to handle search params reactively. You could pick needed parameter by searchParamsAtom.lens method just by passed key or specify the type of the parameter by providing optional parse and serialize functions.

import { searchParamsAtom } from '@reatom/url'

export const filtersAtom = searchParamsAtom.lens('filters')

In the code below filtersAtom is a mutable atom which changes will be synced with the passed search parameter. If you want to setup the sync for some other atom, you could use withSearchParamsPersist decorator.

import { reatomNumber } from '@reatom/primitives'
import { withSearchParamsPersist } from '@reatom/url'

export const pageAtom = reatomNumber(0, 'pageAtom').pipe(withSearchParamsPersist('page', (page = '0') => Number(page)))
//...
pageAtom.increment(ctx)
ctx.get(urlAtom).href // http://example.com/?page=1

Now you have handy increment and decrement actions in pageAtom and synchronization with "page" search parameter.

Also, both searchParamsAtom.lens and withSearchParamsPersist accepts options object by the second argument, which you can use to specify replace strategy (false by default).

import qs from 'qs'
import { atom } from '@reatom/core'
import { withSearchParamsPersist } from '@reatom/url'

export const filtersAtom = atom<Filters>({}, 'filtersAtom').pipe(
  withSearchParamsPersist('filter', {
    replace: true,
    parse: (v = '') => qs.parse(v, { arrayFormat: 'bracket' }),
    serialize: (v) => qs.stringify(v, { arrayFormat: 'bracket' }),
  }),
)

Another useful option is path which put a border to the search parameter application. By default search parameters reads from every url at any path and it changes applied to every url patch. You can change it by providing path option.

import { searchParamsAtom } from '@reatom/url'

export const filtersAtom = searchParamsAtom.lens('filters', { path: '/list' })

In the code below and filters changes will sync to the url only to the /list path.

import { searchParamsAtom } from '@reatom/url'

export const filtersAtom = searchParamsAtom.lens('filters', { path: '/list/*' })
//...
urlAtom.go('/list')
filtersAtom(ctx, 'some')
ctx.get(urlAtom).href // http://example.com/list?filters=some


urlAtom.go('/list/123')
ctx.get(urlAtom).href // http://example.com/list/123?filters=some
urlAtom.go('/list')
ctx.get(urlAtom).href // http://example.com/list?filters=some

Now, the filters will persists across the /list and /list/id paths, which is useful if you have programmable "back" button in your list element interface.

Types

Here are the types of the key features.

// used by `urlAtom`
export interface UrlAtom extends Atom<URL> {
  (ctx: Ctx, url: URL, replace?: boolean): URL
  (ctx: Ctx, update: (url: URL, ctx: Ctx) => URL, replace?: boolean): URL

  go: Action<[path: string, replace?: boolean], URL>
  settingsAtom: AtomMut<AtomUrlSettings>
}

// used by `searchParamsAtom`
export interface SearchParamsAtom extends Atom<Rec<string>> {
  set: Action<[key: string, value: string, replace?: boolean], void>
  del: Action<[key: string, replace?: boolean], void>
  /** create AtomMut which will synced with the specified query parameter */
  lens<T = string>(key: string, parse?: (value?: string) => T, serialize?: (value: T) => undefined | string): AtomMut<T>
  /** create AtomMut which will synced with the specified query parameter */
  lens<T = string>(
    key: string,
    options: {
      parse?: (value?: string) => T
      serialize?: (value: T) => undefined | string
      replace?: boolean
      path?: string
    },
  ): AtomMut<T>
}

Behavior

When urlAtom is first read (directly or indirectly through searchParamsAtom), a subscription to the popstate event and interception of the document.body click appears. If you don't want to handle and prevent link clicks, you could call setupUrlAtomBrowserSettings(ctx, /* shouldCatchLinkClick: */false) action.

If you need to run your code in a different environment (unit tests or SSR) you could replace the url handling behavior by calling setupUrlAtomSettings action with a function for getting URL (init parameter) and optional sync parameter for subscription to URL change.

// a server handler
export const handler = async (req) => {
  const ctx = createCtx()
  const url = new URL(req.url!, `http://${req.headers.host}`)
  setupUrlAtomSettings(ctx, () => url)
  // do your stuff...
}

Integrations

By default urlAtom uses window.location as the source of truth (SoT). But if you using any other router manager, you should setup reactive integrations by yourself, as the native location API is not reactive and can't be used as the SoT.

To put the new URL from the the source of truth to urlAtom you should always use updateFromSource action, because only this updates will not be synced back and it will help you to prevent cyclic stack.

Here is the example of integration with https://reactrouter.com. Put this component in the top of your application tree (as a child of RR provider and Reatom provider)!

You could play with it in Tanstack VS Reatom example

import React from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useCtx } from '@reatom/npm-react'
import { updateFromSource, urlAtom } from '@reatom/url'

export const RouterSync = () => {
  const ctx = useCtx()
  const setupRef = React.useRef(false)

  // subscribe to location changes
  useLocation()
  if (ctx.get(urlAtom).href !== location.href && setupRef.current) {
    // do not use `useEffect` to prevent race conditions (`urlAtom` reading during the render)
    updateFromSource(ctx, new URL(location.href))
  }

  const navigate = useNavigate()
  if (!setupRef.current) {
    setupRef.current = true
    urlAtom.settingsAtom(ctx, {
      init: () => new URL(location.href),
      sync: (_ctx, url, replace) => navigate(url.pathname + url.search, { replace }),
    })
    // trigger `onChange` hooks.
    urlAtom(ctx, new URL(location.href))
  }

  return null
}

The warning Cannot update a component while rendering a different component ("RouterSync") is ok, there are no way to write it in another way and fix it, as the Router.subscribe method is deprecated.