preface
Since the introduction of React Hooks, criticism has been mounting that they are mentally taxing because of the use estate /useEffect’s dependence on execution order, compared to the traditional Class system. In contrast, in the Vue3 CompositionAPI RFC, we see that Vue3 officially describes CompositionAPI as a better solution based on the existing “responsive” mental model, This made us feel as if we could jump into the Compositoin API development without any mental model switching. But after I tried it for a while, I realized that this wasn’t the case, and we still needed some mental changes to get used to the new Compsition API.
Setup a trap
Simple trap
Let’s start with a simple example of Vue2:
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0}},methods: {
addCount() {
this.count += 1}}};</script>
Copy the code
In the mental model of Vue2, we always return an object in data. We don’t care whether the value of the object is a simple type or a reference type, because both are handled well by a responsive system, as in the example above. However, if we start using the Composition API without making any mental model changes, we can easily write code like this:
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script>
import { reactive } from '@vue/runtime-dom'
export default {
setup() {
const data = reactive({
count: 0
})
function addCount() {
data.count += 1
}
return {
count: data.count,
addCount
}
}
};
</script>
Copy the code
In fact, this code doesn’t work because the view doesn’t respond to data changes when you click on the button. The reason is that we took count out of data and merged it into this.$data, but once count is taken out, it is a simple data type and the response is lost.
Complex trap
The more complex the data structure, the easier it is to fall into the trap of extracting a piece of business logic from custom hooks like this:
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_name'.role: 'default_role'
},
projectList: []
})
onMounted(() = > {
// Get data asynchronouslyfetch(...) .then(result= > {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
return data
}
Copy the code
Then, as usual, we use it in the business component:
// App.vue
<template>
<div>
{{name}}
{{role}}
{{list}}
</div>
</template>
<script>
import useSomeData from './useSomeData'
export default {
setup() {
const { userInfo, projectList } = useSomeData()
return {
name: userInfo.name // Reactive break
role: userInfo.role, // Reactive break
list: projectList // The response is still broken}}}</script>
Copy the code
We see that whatever we pull out of reactive data (simple types or reference types) causes the reactive to break and cannot update the view.
The root of all these problems is that setup is executed only once.
Migrate to a new mental model
- Always remember that setup is executed only once
- Never use simple types directly
- Deconstruction can be risky, using the reference itself in preference to deconstructing it
- There are ways to make deconstruction safe
Use new mental models to solve problems
The Simple trap: Never use simple types directly
<template>
<div id="app">
{{count}}
<button @click="addCount"></button>
</div>
</template>
<script>
import { reactive, ref } from '@vue/runtime-dom'
export default {
setup() {
const count = ref(0) // Wrap a layer of reference containers with ref here
function addCount() {
count.value += 1
}
return {
count,
addCount
}
}
};
</script>
Copy the code
Complex pitfall – Scheme 1: Deconstruction can be risky, prioritizing the reference itself over deconstructing it
// useSomeData.js
...
// App.vue
<template>
<div>
{{someData.userInfo.name}}
{{someData.userInfo.role}}
{{someData.projectList}}
</div>
</template>
<script>
import useSomeData from './useSomeData'
export default {
setup() {
const someData = useSomeData()
return {
someData
}
}
}
</script>
Copy the code
Complex pitfall – Scheme 2: You can make deconstruction secure with computations
// useSomeData.js
import { reactive, onMounted, computed } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_user'.role: 'default_role'
},
projectList: []
})
onMounted(() = > {
// Get data asynchronouslyfetch(...) .then(result= > {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
const userName = computed(() = > data.userInfo.name)
const userRole = computed(() = > data.userinfo.role)
const projectList = computed(() = > data.projectList)
return {
userName,
userRole,
projectList
}
}
Copy the code
// App.vue
export default {
setup() {
const { userName, userRole, projectList } = useSomeData()
return {
name: userName // It is a calculated attribute, and the response does not break
role: userRole, // It is a calculated attribute, and the response does not break
list: projectList // It is a calculated attribute, and the response does not break}}}Copy the code
Complex pitfall – Scheme 3: Scheme 2 requires some extra computed attributes, which is a bit of a hassle, and we can also make deconstruction safe with toRefs
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_user'.role: 'default_role'
},
projectList: []
})
onMounted(() = > {
// Get data asynchronouslyfetch(...) .then(result= > {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
/ / use toRefs
return toRefs(data)
}
Copy the code
// App.vue
export default {
setup() {
// Now userInfo and projectList are wrapped by ref
// The wrap is automatically unwrapped in template
const { userInfo, projectList } = useSomeData()
return {
name: userInfo.value.name, / /??? Okay?
role: userInfo.value.role, / /??? Okay?
list: projectList / /??? Okay?}}}Copy the code
You think that’s all it takes? There’s actually a trap of traps: projectList ok, but the name and role are still in a responsive broken state, because toRefs will only “shallow” wrap, and useSomeData actually returns something like this:
constSomeData = useSomeData() ↓ {userInfo: {
value: {
name: '... '.// Still a simple type, not wrapped
role: '... ' // Still a simple type, not wrapped}},projectList: {
value: [...]
}
}
Copy the code
Therefore, if our useSomeData wanted to implement true deconstruction security through toRefs, it would write:
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {...// make each level have a layer of ref
return toRefs({
projectList: data.projectList,
userInfo: toRefs(data.userInfo)
})
}
Copy the code
Suggestion: When using custom hooks to return data, use toRefs directly if the data hierarchy is simple; If the data hierarchy is complex, use computed.
Bypass the trap
This is actually the official Vue standard way of using the Position API, because the Position API is designed to be executed exactly once by Setup. But there is no denying that this does impose a lot of mental burden, as we have to keep an eye on whether responsive data can be deconstructed or not, or we can easily be pulled into a pit.
All of these problems are caused by the fact that setup only executes once, so is there a solution? If yes, you can use JSX or h to get around the problem that setup will only execute once:
Or this custom hooks with security implications:
// useSomeData.js
import { reactive, onMounted } from '@vue/runtime-dom'
export default function useSomeData() {
const data = reactive({
userInfo: {
name: 'default_name'.role: 'default_role'
},
projectList: []
})
onMounted(() = > {
// Get data asynchronouslyfetch(...) .then(result= > {
const { userInfo, projectList } = result
data.userInfo = userInfo
data.projectList = projectList
})
})
return data
}
Copy the code
Use JSX or H
import useSomeData from './useSomeData'
export default {
setup() {
const someData = useSomeData()
return () = > {
const { userInfo: { name, role }, projectList } = someData
return (
<div>
{ name }
{ role }
{ projectList }
</div>)}}}Copy the code
When using JSX or H, setup needs to return a function, which is called the render function, which is reexecuted when the data changes, so we just need to put the deconstructed logic into the render function and setup will only execute once.
Afterword.
We may need conventions to constrain how custom hooks are used. But it’s not official, which would result in hooks writing all over the place and full of bugs. For now, “Don’t deconstruct” is the safest way.
I specifically asked the BIG man at YYX (#1739) about this, and he made a “pact” to use “deconstruction” as little as possible. I have no choice. What I was hoping for was a tool that would reduce the possibility of making mistakes in custom hooks. (toRefs is such a tool, but it doesn’t solve every problem.)