vue-condition-watcher

Vue Composition API for automatic fetch data when condition has been changed

Usage no npm install needed!

<script type="module">
  import vueConditionWatcher from 'https://cdn.skypack.dev/vue-condition-watcher';
</script>

README

English | 繁體中文

vue-condition-watcher πŸ•Ά

CircleCI vue3 vue3 npm npm bundle size npm

Introduction

Vue composition API for automatic data fetching and easily control conditions

requires Node.js 12.0.0 or higher.

Features

βœ” Automatic fetch data when conditions changed.
βœ” Automatic filter falsy value in conditions before fetch.
βœ” Automatic converts the corresponding type. (string, number, array, date)
βœ” Store the conditions within the URL query string every time a condition is changed
βœ” Sync the state with the query string and initialize off of that and that back/forward/execute work.
βœ” Keep requests first in β€” first out.
βœ” Dependent request before update data.
βœ” Easily manage paged data and customized your pagination hook.
βœ” Works for Vue 2 & 3 by the power of vue-demi

πŸ‘‰ 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 with vue-infinite-scroll

Edit vue-condition-watcher 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

This is a simple example for vue-next and vue-router-next

First you need to create a fetcher function, use the native fetch or libs like Axios. Then import useConditionWatcher and start using it.

createApp({
  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>
  `,
  setup() {
    const fetcher = params => axios.get('/user/', {params})
    const router = useRouter()

    const { conditions, data, loading, error } = useConditionWatcher(
      {
        fetcher,
        conditions: {
          name: ''
        },
        history: {
          sync: router
        }
      }
    )
    return { conditions, data, loading, error }
  },
})
.use(router)
.mount(document.createElement('div'))

You can use the value of data, error, and loading to determine the current state of the request.

When the conditions.name value changes, will fire the lifecycle to fetching data again.

Use config.history of sync to sync: router. Will store the conditions within the URL query string every time conditions change.

Basic Usage

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

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
Object.assign(conditions, {
  name: 'runkids',
  date: ['2022-01-01', '2022-01-02']
})

// Reset conditions
function reset () {
  Object.assign(conditions, {
    page: 0,
    name: '',
    date: []
  })

  // Or you can just use `resetConditions` function to initial value.
  resetConditions()
}

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
})

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, or you can call cancel function to stop fetch.

useConditionWatcher({
  fetcher,
  conditions: {
    date: ['2022/01/01', '2022/01/02']
  },
  initialData: [],
  async beforeFetch(conditions, cancel) {
    // await something
    await doSomething ()

    // conditions is an object clone copy from config.conditions
    const {date, ...baseConditions} = conditions
    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 requestss 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'

Configs

  • fetcher (⚠️Required) : A promise returning function to fetch your data
  • conditions (⚠️Required) : An object of conditions, also to be initial value
  • defaultParams: An object of fetcher's default parameters
  • initialData: data default value is null, and you can setting data default value by use this config
  • immediate: Setting the immediate to false will prevent the request until the execute function called. immediate default is true.
  • manual: You can use manual to disabled automatically fetch data
  • history: Sync conditions value to URL query string
  • beforeFetch: You can modify conditions before fetch, or you can call second of arguments to stop fetch this time.
  • afterFetch: You can modify data before update. also can use mutate modify too. But still recommend modify data at afterFetch.
  • onFetchError: Handle error, and you can modify data and error before update here.

Return Values

  • conditions( reactive ) : An object and returns a reactive proxy of conditions
  • data( πŸ‘β€πŸ—¨ readonly & ⚠️ ref ) : Data resolved by config.fetcher
  • error( πŸ‘β€πŸ—¨ readonly & ref ) : Error thrown by config.fetcher
  • loading( πŸ‘β€πŸ—¨ readonly & ref ) : Request is fetching
  • execute: The function to trigger the request
  • mutate: You can use mutate() to directly modify data ( By default, data is readonly )
  • resetConditions: Reset conditions to initial value
  • onConditionsChange: Will fire on conditions changed
  • onFetchSuccess: Will fire on fetch request success
  • onFetchError: Will fire on fetch request error
  • onFetchFinally: Will fire on fetch finished

Lifecycle

  • onConditionsChange

    Fire new conditions value and old conditions value.

    onConditionsChange((cond, preCond)=> {
      console.log(cond)
      console.log(preCond)
    })
    
  • beforeFetch

    You can modify conditions before fetch, or you can call second of arguments to stop fetch this time.

    const { conditions } = useConditionWatcher({
      fetcher,
      conditions,
      beforeFetch
    })
    
    async function beforeFetch(cond, cancel){
      if(!cond.token) {
        // stop fetch
        cancel()
        // will fire onConditionsChange again
        conditions.token = await fetchToken()
      }
      return cond
    })
    
  • afterFetch & onFetchSuccess

    afterFetch fire before onFetchSuccess
    afterFetch can modify data before update. ||Type|Modify data before update| Dependent request | |-----|--------|------|------| |afterFetch| config | ⭕️ | ⭕️ | |onFetchSuccess | event | ❌ | ❌ |

      <template> 
        {{ data?.detail }} <!-- 'xxx' -->
      </template>
    
    const { data, onFetchSuccess } = useConditionWatcher({
     fetcher,
     conditions,
     async afterFetch(response){
       //response = { id: 1 }
       const detail = await fetchDataById(response.id)
       return detail // { id: 1, detail: 'xxx' }
     })
    })
    
    onFetchSuccess((response)=> {
     console.log(response) // { id: 1, detail: 'xxx' }
    })
    
  • onFetchError(config) & onFetchError(event)

    config.onFetchError fire before event.onFetchError
    config.onFetchError can modify data and error before update. ||Type|Modify data before update|Modify error before update| |-----|--------|------|------| |onFetchError| config | ⭕️ | ⭕️ | |onFetchError | event | ❌ | ❌ |

    const { onFetchError } = useConditionWatcher({
     fetcher,
     conditions,
     onFetchError(ctx){
       return {
         data: [],
         error: 'Error message.'
       }
     })
    })
    
    onFetchError((error)=> {
     console.log(error) // origin error data
    })
    
  • onFetchFinally

    Will fire on fetch finished.

    onFetchFinally(async ()=> {
      //do something
    })
    

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 { 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

  • Cache
  • Prefetching
  • Automatic Revalidation
  • Pulling