globalbrain/sefirot

[Url] Add `useUrlQuerySync` function

kiaking opened this issue · 0 comments

Often, we need to have a way to sync URL query params with ref or reactive. Would be nice to have good function does that.

Example usage

Let's say we have this kind of api call.

// Option to pass in to the API request.
// Each value is the default value.
const options = reactive({
  page: 1,
  conditions: {
    name: 'John'
  }
})

const { data } = useQuery(options)

function showNextPage() {
  options.page++
}

And then, we would like to "sync" this option to URL query params so that we can create a direct link to this conditions.

const options = reactive({
  page: 1,
  conditions: {
    name: 'John'
  }
})

// Sync with query.
useUrlQuerySync(options, {
  casts: {
    page: Number
  }
})

const { data } = useQuery(options)

function showNextPage() {
  options.page++
}

Detailed examples

Sync should be by-directional.

// when URL is
// `https://example.com`

const options = reactive({ page: 1 })

useUrlQuerySync(options)

// -> https://example.com
// If the query is default value, do not show params on URL.

options.page++

// -> https://example.com?page=2
// If query differs from initial value, add params.
// If user access page with
// https://example.com?page=5

const options = reactive({ page: 1 })

useUrlQuerySync(options)

options.page // <- 5. Should be synced with URL.

Options

It should be able to cast or mutation the query params, because they are all string.

useUrlQuerySync(options, {
  casts: {
    page: (value) => Number(value)
  }
})

It should be able to exclude certain params from showing up on URL.

Note

This option might be too much... we could always remove the values before passing them into the function 🤔

useUrlQuerySync(options, {
  // Do not display `perPage` option in URL.
  exclude: ['perPage']
})

Current implementation

We use something like this in our apps. Would be great to organize this one.

Warning

Note that argument is different from the above examples because I changed it.

export interface UseUrlQuerySyncOptions {
  state: Record<string, any>
  casts?: Record<string, (value: any) => any>
  exclude?: string[]
}

export function useUrlQuerySync({ state, casts = {}, exclude = [] }: UseQuerySyncOptions): void {
  const router = useRouter()
  const route = useRoute()

  const initialState = cloneDeep(state)

  syncQueryToState()

  watch(() => state, syncStateToQuery, { deep: true })

  function syncStateToQuery(): void {
    if (isEqual(state, initialState)) {
      router.replace({ query: {} })

      return
    }

    const newState = cloneDeep(state)

    exclude.forEach((path) => unset(newState, path))

    router.replace({ query: newState })
  }

  function syncQueryToState(): void {
    const newState = cloneDeep(state)

    mergeWith(newState, route.query, (stateValue, _queryValue, key) => {
      if (!isNaN(Number(key))) {
        return
      }

      if (stateValue === undefined) {
        return '__REMOVE__'
      }
    })

    mergeWith({}, newState, (_dummyValue, stateValue, key, _dummyParent, stateParent) => {
      if (stateValue === '__REMOVE__') {
        delete stateParent[key]
      }
    })

    for (const path in casts) {
      update(newState, path, casts[path])
    }

    exclude.forEach((path) => unset(newState, path))

    mergeWith(state, newState)
  }
}

Alternatives

The alternative API ideas are welcome as well!

Consideration

Maybe we could use useUrlSearchParams from vueuse? But not sure if this is OK when using Vue Router 🤔 (directly modifying URL no going through router API).