preface

In the process of learning About React Hooks, I read an article about how to request data from Hooks, which abstracts the logic into a new hook for other components to reuse. In my blog, I translated it: how to request data from Hooks in React Hooks? You can have a look if you are interested. Although this is last year’s article, I immediately understood the use of Hooks after reading it, and data requests are very common logic in business code.

Vue 3 has been out for a while now, and the composition API has a bit of a React Hooks Hooks in it, so I’m going to learn the composition API that way today.

Project initialization

To get a Vue 3 project started quickly, we used the hottest tool of the moment, Vite, to initialize the project directly. The whole process goes smoothly.

npm init vite-app vue3-app
Copy the code
# Open the generated project folder
cd vue3-app
# install dependencies
npm install
# Start a project
npm run dev
Copy the code

Let’s open app.vue and delete the generated code.

Entry to the composite API

The Hacker News API returns the following data structure:

{
  "hits": [{"objectID": "24518295"."title": "Vue.js 3"."url": "https://github.com/vuejs/vue-next/releases/tag/v3.0.0"}, {... }, {... ]}},Copy the code

We display the news list on the interface through UI > Li, and the news data is obtained from hits traversal.

<template>
  <ul>
    <li
      v-for="item of hits"
      :key="item.objectID"
    >
      <a :href="item.url">{{item.title}}</a>
    </li>
  </ul>
</template>

<script>
import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({
      hits: []})return state
  }
}
</script>
Copy the code

The setup() method is used to start the composite API. The data returned by setup() can be used in the template, which can be easily interpreted as the data() method in Vue 2. The returned data needs to be wrapped in the Reactive () method to make it reactive.

Request data in the composite API

In Vue 2, when we request data, we usually need to put the code that initiated the request into some lifecycle (created or Mounted). In the setup() method, we can use the lifecycle hooks provided by Vue 3 to put requests into a particular lifecycle. Here’s how the lifecycle hook method compares to the previous lifecycle:

As you can see, you basically add an on to the name of the previous method and don’t provide an onCreated hook because executing inside setup() is equivalent to executing in the Created phase. SQL > select * from mounted;

import { reactive, onMounted } from 'vue'

export default {
  setup() {
    const state = reactive({
      hits: []
    })
    onMounted(async() = > {const data = await fetch(
        'https://hn.algolia.com/api/v1/search?query=vue'
      ).then(rsp= > rsp.json())
      state.hits = data.hits
    })
    return state
  }
}
Copy the code

The final effect is as follows:

Monitor data changes

Hacker News’s query interface has a query parameter, which we fixed in the previous example and now define with reactive data.

<template>
  <input type="text" v-model="query" />
  <ul>
    <li
      v-for="item of hits"
      :key="item.objectID"
    >
      <a :href="item.url">{{item.title}}</a>
    </li>
  </ul>
</template>

<script>
import { reactive, onMounted } from 'vue'

export default {
  setup() {
    const state = reactive({
      query: 'vue'.hits: []
    })
    onMounted((async() = > {const data = await fetch(
        `https://hn.algolia.com/api/v1/search?query=${state.query}`
      ).then(rsp= > rsp.json())
      state.hits = data.hits
    })
    return state
  }
}
</script>
Copy the code

Now we modify the input field to trigger a state.query synchronous update, but not a fetch recall, so we need to listen for changes to the response data via watchEffect().

import { reactive, onMounted, watchEffect } from 'vue'

export default {
  setup() {
    const state = reactive({
      query: 'vue'.hits: []})const fetchData = async (query) => {
      const data = await fetch(
        `https://hn.algolia.com/api/v1/search?query=${query}`
      ).then(rsp= > rsp.json())
      state.hits = data.hits
    }
    onMounted(() = > {
      fetchData(state.query)
      watchEffect(() = > {
        fetchData(state.query)
      })
    })
    return state
  }
}
Copy the code

FetchData in onMounted needs to be removed because watchEffect() is invoked once for the first time, causing the interface to be initialized twice.

onMounted(() => {
- fetchData(state.query)
  watchEffect(() => {
    fetchData(state.query)
  })
})
Copy the code

WatchEffect () listens for all reactive data in the incoming function, and if any of the data changes, the function is reexecuted. If you want to cancel listening, you can call the return value of watchEffect(), which returns a function. Here’s an example:

const stop = watchEffect(() = > {
  if (state.query === 'vue3') {
    // Stop listening when query is vue3
    stop()
  }
  fetchData(state.query)
})
Copy the code

When we type “vue3” in the input box, the request will not be made again.

Return event method

The problem is that each change in the input will trigger a request, so we can add a button that will trigger a state.query update when clicked.

<template>
  <input type="text" v-model="input" />
  <button @click="setQuery">搜索</button>
  <ul>
    <li
      v-for="item of hits"
      :key="item.objectID"
    >
      <a :href="item.url">{{item.title}}</a>
    </li>
  </ul>
</template>

<script>
import { reactive, onMounted, watchEffect } from 'vue'

export default {
  setup() {
    const state = reactive({
      input: 'vue',
      query: 'vue',
      hits: []
    })
    const fetchData = async (query) => {
      const data = await fetch(
        `https://hn.algolia.com/api/v1/search?query=${query}`
      ).then(rsp => rsp.json())
      state.hits = data.hits
    }
    onMounted(() => {
      watchEffect(() => {
        fetchData(state.query)
      })
    })
    
    const setQuery = () => {
      state.query = state.input
    }
    return { setQuery, state }
  }
}
</script>
Copy the code

Note that the button binding’s click event method is also returned by the setup() method. We can think of the setup() method return value as a combination of the data() method and the methods object in Vue2.

The original return value state is now an attribute of the return value, so we need to make some changes when fetching data in the template layer by adding state.

<template>
  <input type="text" v-model="state.input" />
  <button @click="setQuery">search</button>
  <ul>
    <li
      v-for="item of state.hits"
      :key="item.objectID"
    >
      <a :href="item.url">{{item.title}}</a>
    </li>
  </ul>
</template>
Copy the code

Return data modification

As a patient with obsessive-compulsive disorder, it is really uncomfortable to obtain data in the template layer by means of state. XXX. Can we return state data by object deconstruction?

<template>
  <input type="text" v-model="input" />
  <button class="search-btn" @click="setQuery">search</button>
  <ul class="results">
    <li
      v-for="item of hits"
      :key="item.objectID"
    >
      <a :href="item.url">{{item.title}}</a>
    </li>
  </ul>
</template>

<script>
import { reactive, onMounted, watchEffect } from 'vue'

export default {
  setup(props, ctx) {
    const state = reactive({
      input: 'vue'.query: 'vue'.hits: []})// omit some code...
    return {
      ...state,
      setQuery,
    }
  }
}
</script>
Copy the code

The answer is “no”. After modifying the code, you can see that the page makes the request but does not display the data.

After state is deconstructed, the data becomes static and can no longer be traced. The return value is similar to:

export default {
  setup(props, ctx) {
    // omit some code...
    return {
      input: 'vue'.query: 'vue'.hits: [],
      setQuery,
    }
  }
}
Copy the code

Vue3 also proposes a solution to keep track of the underlying type of data (that is, non-object data) : ref().

import { ref } from 'vue'

const count = ref(0)
console.log(count.value) / / 0

count.value++
console.log(count.value) / / 1
Copy the code

This is the official example of Vue 3. The ref() method returns an object, and both modification and retrieval require the value attribute of the returned object.

We changed state from a response object to a normal object, and then wrapped all attributes with ref so that subsequent deconstruction would take effect. The downside is that each property of state must be modified with its value property. However, there is no need to append.value to the template; Vue 3 handles it internally.

import { ref, onMounted, watchEffect } from 'vue'
export default {
  setup() {
    const state = {
      input: ref('vue'),
      query: ref('vue'),
      hits: ref([])
    }
    const fetchData = async (query) => {
      const data = await fetch(
        `https://hn.algolia.com/api/v1/search?query=${query}`
      ).then(rsp= > rsp.json())
      state.hits.value = data.hits
    }
    onMounted(() = > {
      watchEffect(() = > {
        fetchData(state.query.value)
      })
    })
    const setQuery = () = > {
      state.query.value = state.input.value
    }
    return {
      ...state,
      setQuery,
    }
  }
}
Copy the code

Is there a way to keep state as a response object while still supporting its object deconstruction? Of course there is, and Vue 3 provides a solution: toRefs(). The toRefs() method turns a response object into a normal object and appends ref() to each attribute.

import { toRefs, reactive, onMounted, watchEffect } from 'vue'

export default {
  setup() {
    const state = reactive({
      input: 'vue'.query: 'vue'.hits: []})const fetchData = async (query) => {
      const data = await fetch(
        `https://hn.algolia.com/api/v1/search?query=${query}`
      ).then(rsp= > rsp.json())
      state.hits = data.hits
    }
    onMounted(() = > {
      watchEffect(() = > {
        fetchData(state.query)
      })
    })
    const setQuery = () = > {
      state.query = state.input
    }
    return {
      ...toRefs(state),
      setQuery,
    }
  }
}
Copy the code

Loading and Error state

Loading and Error states are usually added to the request. We only need to add two variables in state to control these two states.

export default {
  setup() {
    const state = reactive({
      input: 'vue'.query: 'vue'.hits: [].error: false.loading: false,})const fetchData = async (query) => {
      state.error = false
      state.loading = true
      try {
        const data = await fetch(
          `https://hn.algolia.com/api/v1/search?query=${query}`
        ).then(rsp= > rsp.json())
        state.hits = data.hits
      } catch {
        state.error = true
      }
      state.loading = false
    }
    onMounted(() = > {
      watchEffect(() = > {
        fetchData(state.query)
      })
    })
    const setQuery = () = > {
      state.query = state.input
    }
    return {
      ...toRefs(state),
      setQuery,
    }
  }
}
Copy the code

Use both variables in the template:

<template>
  <input type="text" v-model="input" />
  <button @click="setQuery">search</button>
  <div v-if="loading">Loading ...</div>
  <div v-else-if="error">Something went wrong ...</div>
  <ul v-else>
    <li
      v-for="item of hits"
      :key="item.objectID"
    >
      <a :href="item.url">{{item.title}}</a>
    </li>
  </ul>
</template>
Copy the code

Displaying Loading and Error states:

Abstract the data request logic

Those of you who have used UMI know that UMI provides an Hooks function called useRequest, which is very convenient for requesting data. Vue’s composite API abstracts out a public method similar to useRequest.

Next we create a new file userequest.js:

import {
  toRefs,
  reactive,
} from 'vue'

export default (options) => {
  const { url } = options
  const state = reactive({
    data: {},
    error: false.loading: false,})const run = async () => {
    state.error = false
    state.loading = true
    try {
      const result = await fetch(url).then(res= > res.json())
      state.data = result
    } catch(e) {
      state.error = true
    }
    state.loading = false
  }

  return{ run, ... toRefs(state) } }Copy the code

Then introduce in app.vue:

<template>
  <input type="text" v-model="query" />
  <button @click="search">search</button>
  <div v-if="loading">Loading ...</div>
  <div v-else-if="error">Something went wrong ...</div>
  <ul v-else>
    <li
      v-for="item of data.hits"
      :key="item.objectID"
    >
      <a :href="item.url">{{item.title}}</a>
    </li>
  </ul>
</template>

<script>
import { ref, onMounted } from 'vue'
import useRequest from './useRequest'

export default {
  setup() {
    const query = ref('vue')
    const { data, loading, error, run } = useRequest({
      url: 'https://hn.algolia.com/api/v1/search'
    })
    onMounted(() = > {
      run()
    })
    return {
      data,
      query,
      error,
      loading,
      search: run,
    }
  }
}
</script>
Copy the code

The current useRequest also has two defects:

  1. The incoming URL is fixed. After the query is modified, the url cannot be reflected in time.
  2. Can’t automatically request, you need to manually call the run method;
import {
  isRef,
  toRefs,
  reactive,
  onMounted,
} from 'vue'

export default (options) => {
  const { url, manual = false, params = {} } = options

  const state = reactive({
    data: {},
    error: false.loading: false,})const run = async() = > {// Concatenate query parameters
    let query = ' '
    Object.keys(params).forEach(key= > {
      const val = params[key]
      // If we go to the ref object, we need to get the.value attribute
      const value = isRef(val) ? val.value : val
      query += `${key}=${value}& `
    })
    state.error = false
    state.loading = true
    try {
      const result = await fetch(`${url}?${query}`)
      	.then(res= > res.json())
      state.data = result
    } catch(e) {
      state.error = true
    }
    state.loading = false
  }

  onMounted(() = > {
    // Whether to call manually the first time! manual && run() })return{ run, ... toRefs(state) } }Copy the code

After this modification, our logic becomes surprisingly simple.

import useRequest from './useRequest'

export default {
  setup() {
    const query = ref('vue')
    const { data, loading, error, run } = useRequest(
      {
        url: 'https://hn.algolia.com/api/v1/search'.params: {
          query
        }
      }
    )
    return {
      data,
      query,
      error,
      loading,
      search: run,
    }
  }
}
Copy the code

Of course, useRequest still has a lot to improve on, such as no SUPPORT for HTTP method modification, no support for throttling and stabilization, no support for timeouts, and so on. Finally, I hope you can learn something after reading this article.