Home

Awesome

English | 中文

vue-condition-watcher <img src="https://slackmojis.com/emojis/43271-glasses/download" width="40" />

CircleCI vue3 vue3 npm npm bundle size npm

Introduction

Data fetching with Vue Composition API. Power of conditions to easily control and sync to the URL query string.

requires Node.js 12.0.0 or higher.

Features

✔ Automatically fetch data whenever conditions change.<br> ✔ Automatically filter out null, undefined, [], and '' before sending the request.<br> ✔ Refresh the page to automatically initialize conditions according to the query string of the URL, and correspond to the type (string, number, array, date).<br> ✔ Whenever conditions change, the URL query string will be automatically synchronized, and the previous page and next page will work normally.<br> ✔ Avoid race conditions, ensure requests are first in, first out, and can also avoid repeated requests.<br> ✔ Perform dependent requests before updating data.<br/> ✔ Easily handle paging needs by customizing your own paging logic.<br/> ✔ Automatically refetch data when the web page is refocused or network disconnection resumes.<br/> ✔ Support polling, with the polling period adjustable dynamically.<br/> ✔ The caching mechanism allows data to be rendered faster without waiting for loading animations.<br/> ✔ No need to wait for the return result; you can manually change data to improve the user experience.<br/> ✔ TypeScript support.<br/> ✔ Works for Vue 2 & 3 through the power of vue-demi.

<img src=".github/vue-conditions-watcher.gif"/>

Navigation

Demo

👉 Download Vue3 example here (Use Vite)

cd examples/vue3
yarn 
yarn serve

👉 Download Vue2 @vue/composition-api example here

cd examples/vue2
yarn 
yarn serve

👉 Online demo

Getting Started

Installation

In your project

yarn add vue-condition-watcher

Or with npm

npm install vue-condition-watcher

CDN

https://unpkg.com/vue-condition-watcher/dist/index.js

Quick Start

You can start by creating a fetcher function using the native fetch or libraries like Axios. Then, import the useConditionWatcher function and start using it. Here's an example:

<script setup>
import axios from 'axios'
import { useRouter } from 'vue-router'
import { useConditionWatcher } from 'vue-condition-watcher'

const fetcher = params => axios.get('/user/', {params})
const router = useRouter()

const { conditions, data, loading, execute, error } = useConditionWatcher(
  {
    fetcher,
    conditions: {
      name: ''
    },
    history: {
      sync: router
    }
  }
)
</script>

<template>
  <div class="filter">
    <input v-model="conditions.name">
    <button @click="execute">Refetch</button>
  </div>
  <div class="container">
    {{ !loading ? data : 'Loading...' }}
  </div>
  <div v-if="error">{{ error }}</div>
</template>

The useConditionWatcher function returns an object with three properties: data, error, and loading. You can use these values to determine the current state of the request.

When the value of conditions.name changes, useConditionWatcher will automatically refetch the data.

You can use the config.history option to sync the conditions object with the router. This will store the conditions object within the URL query string every time it changes.

Basic Usage

const { conditions, data, error, loading, execute, resetConditions, onConditionsChange } = useConditionWatcher(config)

Configs

Return Values

Execute Fetch

conditions is reactive proxy, easy execute fetch when conditions value changed

const { conditions } = useConditionWatcher({
  fetcher,
  conditions: {
    page: 0
  },
  defaultParams: {
    opt_expand: 'date'
  }
})

conditions.page = 1 // fetch data with payload { page: 1, opt_expand: 'date' }

conditions.page = 2 // fetch data with payload { page: 2, opt_expand: 'date' }

Just call execute function to send a request if you need.

const { conditions, execute: refetch } = useConditionWatcher({
  fetcher,
  conditions: {
    page: 0
  },
   defaultParams: {
    opt_expand: 'date'
  }
})

refetch() // fetch data with payload { page: 0, opt_expand: 'date' }

Force update conditions in time.

const { conditions, resetConditions } = useConditionWatcher({
  fetcher,
  immediate: false,
  conditions: {
    page: 0,
    name: '',
    date: []
  },
})

// initial conditions then fire onConditionsChange event
resetConditions({
  name: 'runkids',
  date: ['2022-01-01', '2022-01-02']
})

// Reset conditions
function reset () {
  // You can just use `resetConditions` function to initial value.
  resetConditions()
}

Prevent Request

Setting the immediate to false will prevent the request until the execute function called or conditions changed.

const { execute } = useConditionWatcher({
  fetcher,
  conditions,
  immediate: false,
})

execute()

Manually Trigger Request

By default, vue-condition-watcher will automatically trigger fetch data. You can pass manual to disable the default fetch and then use execute() to trigger fetch data.

const { execute } = useConditionWatcher({
  fetcher,
  conditions,
  manual: true,
})

execute()

Intercepting Request

The beforeFetch let you modify conditions before fetch. Receive two params:

useConditionWatcher({
  fetcher,
  conditions: {
    date: ['2022/01/01', '2022/01/02']
  },
  initialData: [],
  async beforeFetch(conds, cancel) {
    // await to check token before fetch
    await checkToken ()

    // conds is an object clone copy from config.conditions
    const {date, ...baseConditions} = conds
    const [after, before] = date
    baseConditions.created_at_after = after
    baseConditions.created_at_before = before

    return baseConditions
  }
})

The afterFetch can intercept the response before data updated, also your can requests depend on each other 🎭

const { data } = useConditionWatcher({
  fetcher,
  conditions,
  async afterFetch(response) {
    //response.data = {id: 1, name: 'runkids'}
    if(response.data === null) {
      return []
    }
    // requests depend on each other
    // the loading is still be true until fire `onFetchFinally`
    const finalResponse = await otherAPIById(response.data.id)

    return finalResponse // [{message: 'Hello', sender: 'runkids'}]
  }
})

console.log(data) //[{message: 'Hello', sender: 'runkids'}]

The onFetchError can intercept the response before data and error updated

const { data, error } = useConditionWatcher({
  fetcher,
  conditions,
  async onFetchError({data, error}) {
    if(error.code === 401) {
      await doSomething()
    }

    return {
      data: [],
      error: 'Error Message'
    }
  }
})

console.log(data) //[]
console.log(error) //'Error Message'

Mutations data

In some cases, mutations to data is a good way to make the user experience better, you don't need wait for the remote data.

Use mutate function, you can update data. While onFetchSuccess will replace data again.

Two way to use mutate function:

mutate(newData)
const finalData = mutate((draft) => {
  draft[0].name = 'runkids'
  return draft
})

console.log(finalData[0]name === data.value[0].name) //true

🏄‍♂️ Example for update a part of your data based on the current data

POST API will just return the updated data directly, so we don’t need to fetch list data again.

const { conditions, data, mutate } = useConditionWatcher({
  fetcher: api.userInfo,
  conditions,
  initialData: []
})

async function updateUserName (userId, newName, rowIndex = 0) {
  console.log(data.value) //before: [{ id: 1, name: 'runkids' }, { id: 2, name: 'vuejs' }]

  const response = await api.updateUer(userId, newName)

  // 🚫 `data.value[0] = response.data`
  // Not work! Because `data` is read only.

  // Easy to use function will receive deep clone data, and return updated data.
  mutate(draft => {
    draft[rowIndex] = response.data
    return draft
  })

  console.log(data.value) //after: [{ id: 1, name: 'mutate name' }, { id: 2, name: 'vuejs' }]
}

Conditions Change Event

onConditionsChange can help you handle conditions changed. Will return new value and old value.

const { conditions, onConditionsChange } = useConditionWatcher({
  fetcher,
  conditions: {
    page: 0
  },
})

conditions.page = 1

onConditionsChange((conditions, preConditions)=> {
  console.log(conditions) // { page: 1}
  console.log(preConditions) // { page: 0}
})

Fetch Event

The onFetchResponse, onFetchError and onFetchFinally will fire on fetch request.

const { onFetchResponse, onFetchError, onFetchFinally } = useConditionWatcher(config)

onFetchResponse((response) => {
  console.log(response)
})

onFetchError((error) => {
  console.error(error)
})

onFetchFinally(() => {
  //todo
})

Polling

You can use pollingInterval to automatically refetch data. Just enable it by setting pollingInterval value.

useConditionWatcher({
  fetcher,
  conditions,
  pollingInterval: 1000
})

And also you can use ref, it's will be reactivity.

const pollingInterval = ref(0)

useConditionWatcher({
  fetcher,
  conditions,
  pollingInterval: pollingInterval
})

function startPolling () {
  pollingInterval.value = 1000
}

onMounted(startPolling)

The vue-condition-watcher default will disable polling when you leave the screen in focus or when the network is disconnected.

You can turn off the default behavior by setting:

You can also retry the request after enabling the focus screen to make sure the data is up to date.

useConditionWatcher({
  fetcher,
  conditions,
  pollingInterval: 1000,
  pollingWhenHidden: true, // pollingWhenHidden default is false
  pollingWhenOffline: true, // pollingWhenOffline default is false
  revalidateOnFocus: true // revalidateOnFocus default is false
})

Cache

The vue-condition-watcher preset will cache your first data in the current component. Then the following requests will use the cached data first, silently request new data behind, wait for the latest return result and compare whether the cached data is the same to achieve a similar preloading effect.

You can also set cacheProvider by function to share globally or cache data in localStorage, and with polling, it can achieve the effect of paging and synchronizing data.

Global Based
// App.vue
<script lang="ts">
const cache = new Map()

export default {
  name: 'App',
  provide: {
    cacheProvider: () => cache
  }
}

//Other.vue
useConditionWatcher({
  fetcher,
  conditions,
  cacheProvider: inject('cacheProvider')
})
</script>
LocalStorage Based
function localStorageProvider() {
  const map = new Map(JSON.parse(localStorage.getItem('your-cache-key') || '[]'))
  window.addEventListener('beforeunload', () => {
    const appCache = JSON.stringify(Array.from(map.entries()))
    localStorage.setItem('your-cache-key', appCache)
  })
  return map
}

useConditionWatcher({
  fetcher,
  conditions,
  cacheProvider: localStorageProvider
})

History Mode

You can enable History mode by setting config.history, which is based on vue-router and supports v3 and v4 versions

const router = useRouter()

useConditionWatcher({
  fetcher,
  conditions,
  history: {
    sync: router
  }
})

You can also set history.ignore to exclude the key&value in the conditions section from being synced to the URL query string.

const router = useRouter()

useConditionWatcher({
  fetcher,
  conditions: {
    users: ['runkids', 'hello']
    limit: 20,
    offset: 0
  },
  history: {
    sync: router,
    ignore: ['limit']
  }
})

// the query string will be ?offset=0&users=runkids,hello

History mode will convert the corresponding types of conditions default values ​​to query strings and will filter out undefined, null, '', [] values.

conditions: {
  users: ['runkids', 'hello']
  company: ''
  limit: 20,
  offset: 0
}
// the query string will be ?offset=0&limit=20&users=runkids,hello

Also automatically syncs the query string to conditions whenever you refresh the page

URL query string: ?offset=0&limit=10&users=runkids,hello&company=vue

conditions will become

{
  users: ['runkids', 'hello']
  company: 'vue'
  limit: 10,
  offset: 0
}

Use navigation to replace or push current location. The default value is 'push'

useConditionWatcher({
  fetcher,
  conditions: {
    limit: 20,
    offset: 0
  },
  history: {
    sync: router,
    navigation: 'replace'
  }
})

Lifecycle

<img src=".github/vue-condition-watcher_lifecycle.jpeg"/>

Make It Reusable

You might need to reuse the data in many places. It is incredibly easy to create reusable hooks of vue-condition-watcher :

function useUserExpensesHistory (id) {
  const { conditions, data, error, loading } = useConditionWatcher({
    fetcher: params => api.user(id, { params }),
    defaultParams: {
      opt_expand: 'amount,place'
    },
    conditions: {
      daterange: []
    }
    immediate: false,
    initialData: [],
    beforeFetch(cond, cancel) {
      if(!id) {
        cancel()
      }
      const { daterange, ...baseCond } = cond
      if(daterange.length) {
        [baseCond.created_at_after, baseCond.created_at_before] = [
          daterange[0],
          daterange[1]
        ]
      }
      return baseCond
    }
  })

  return {
    histories: data,
    isFetching: loading,
    isError: error,
    daterange: conditions.daterange
  }
}

And use it in your components:

<script setup>
  const { 
    daterange, 
    histories, 
    isFetching, 
    isError 
  } = useUserExpensesHistory(route.params.id)

  onMounted(() => {
    //start first time data fetching after initial date range
    daterange = [new Date(), new Date()]
  })
</script>
<template>
  <el-date-picker
    v-model="daterange"
    :disabled="isFetching"
    type="daterange"
  />
  <div v-for="history in histories" :key="history.id">
    {{ `${history.created_at}: ${history.amount}` }}
  </div>
</template>

Congratulations! 🥳 You have learned how to use composition-api with vue-condition-watcher.

Now we can manage the paging information use vue-condition-watcher .

Pagination

Here is an example use Django the limit and offset functions and Element UI.

Create usePagination

function usePagination () {
  let cancelFlag = false // check this to cancel fetch

  const { startLoading, stopLoading } = useLoading()
  const router = useRouter()
  
  const { conditions, data, execute, resetConditions, onConditionsChange, onFetchFinally } = useConditionWatcher(
    {
      fetcher: api.list,
      conditions: {
        daterange: [],
        limit: 20,
        offset: 0
      }
      immediate: true,
      initialData: [],
      history: {
        sync: router,
        // You can ignore the key of URL query string, prevent users from entering unreasonable numbers by themselves.
        // The URL will look like ?offset=0 not show `limit`
        ignore: ['limit'] 
      },
      beforeFetch
    }, 
  )

  // use on pagination component
  const currentPage = computed({
    get: () => conditions.offset / conditions.limit + 1,
    set: (page) => {
      conditions.offset = (page - 1) * conditions.limit
    }
  })

  // onConditionsChange -> beforeFetch -> onFetchFinally
  onConditionsChange((newCond, oldCond) => {
    // When conditions changed, reset offset to 0 and then will fire beforeEach again.
    if (newCond.offset !== 0 && newCond.offset === oldCond.offset) {
      cancelFlag = true
      conditions.offset = 0
    }
  })

  async function beforeFetch(cond, cancel) {
    if (cancelFlag) {
      // cancel fetch when cancelFlag be true
      cancel()
      cancelFlag = false // reset cancelFlag 
      return cond
    }
    // start loading
    await nextTick()
    startLoading()
    const { daterange, ...baseCond } = cond
    if(daterange.length) {
      [baseCond.created_at_after, baseCond.created_at_before] = [
        daterange[0],
        daterange[1]
      ]
    }
    return baseCond
  }

  onFetchFinally(async () => {
    await nextTick()
    // stop loading
    stopLoading()
    window.scrollTo(0, 0)
  })

  return {
    data,
    conditions,
    currentPage,
    resetConditions,
    refetch: execute
  }
}

And use it in your components:

<script setup>
  const { data, conditions, currentPage, resetConditions, refetch } = usePagination()
</script>
<template>
  <el-button @click="refetch">Refetch Data</el-button>
  <el-button @click="resetConditions">Reset Offset</el-button>

  <el-date-picker
    v-model="conditions.daterange"
    type="daterange"
  />

  <div v-for="info in data" :key="info.id">
    {{ info }}
  </div>

  <el-pagination
    v-model:currentPage="currentPage"
    v-model:page-size="conditions.limit"
    :total="data.length"
  />
</template>

When daterange or limit changed, will reset offset to 0 and only fetch data again after reset offset.

TDOD List

Thanks

This project is heavily inspired by the following awesome projects.

📄 License

MIT License © 2020-PRESENT Runkids