Original source: https://michaelnthiessen.com/12-design-patterns-vue
Last active
June 5, 2024 17:28
-
-
Save Kcko/3ace57a67536fc7ef57c16eae1f07b73 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Data Store Pattern | |
/* | |
This pattern has a few parts: | |
A global state singleton | |
Exporting some or all of this state | |
Methods to access and modify the state | |
*/ | |
import { reactive, toRefs, readonly } from 'vue'; | |
import { themes } from './utils'; | |
// 1. Create global state in module scope, shared every | |
// time we use this composable | |
const state = reactive({ | |
darkMode: false, | |
sidebarCollapsed: false, | |
// 2. This theme value is kept private to this composable | |
theme: 'nord', | |
}); | |
export default () => { | |
// 2. Expose only some of the state | |
// Using toRefs allows us to share individual values | |
const { darkMode, sidebarCollapsed } = toRefs(state); | |
// 3. Modify our underlying state | |
const changeTheme = (newTheme) => { | |
if (themes.includes(newTheme)) { | |
// Only update if it's a valid theme | |
state.theme = newTheme; | |
} | |
} | |
return { | |
// 2. Only return some of the state | |
darkMode, | |
sidebarCollapsed, | |
// 2. Only expose a readonly version of state | |
theme: readonly(state.theme), | |
// 3. We return a method to modify underlying state | |
changeTheme, | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Thin Composables | |
/* | |
Thin composables introduce an additional layer of abstraction, separating the reactivity management from the core business logic. | |
Here we use plain JavaScript or TypeScript for business logic, represented as pure functions, with a thin layer of reactivity on top. | |
*/ | |
import { ref, watch } from 'vue'; | |
import { convertToFahrenheit } from './temperatureConversion'; | |
export function useTemperatureConverter(celsiusRef: Ref<number>) { | |
const fahrenheit = ref(0); | |
watch(celsiusRef, (newCelsius) => { | |
// Actual logic is contained within a pure function | |
fahrenheit.value = convertToFahrenheit(newCelsius); | |
}); | |
return { fahrenheit }; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Humble Components Pattern | |
/* Props down, events up" principle */ | |
<template> | |
<div class="max-w-sm rounded overflow-hidden shadow-lg"> | |
<img class="w-full" :src="userData.image" alt="User Image" /> | |
<div class="px-6 py-4"> | |
<div class="font-bold text-xl mb-2"> | |
{{ userData.name }} | |
</div> | |
<p class="text-gray-700 text-base"> | |
{{ userData.bio }} | |
</p> | |
</div> | |
<div class="px-6 pt-4 pb-2"> | |
<button | |
@click="emitEditProfile" | |
class="bg-blue-500 hover:bg-blue-700 text-white | |
font-bold py-2 px-4 rounded" | |
> | |
Edit Profile | |
</button> | |
</div> | |
</div> | |
</template> | |
<script setup> | |
defineProps({ | |
userData: Object, | |
}); | |
const emitEditProfile = () => { | |
emit('edit-profile'); | |
}; | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Extract Conditional | |
// Before | |
<template> | |
<div v-if="condition"> | |
<!-- Lots of code here for the true condition --> | |
</div> | |
<div v-else> | |
<!-- Lots of other code for the false condition --> | |
</div> | |
</template> | |
// After | |
<template> | |
<TrueConditionComponent v-if="condition" /> | |
<FalseConditionComponent v-else /> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Extract Composable | |
/* logic separate from UI */ | |
import { ref, watch } from 'vue'; | |
export function useExampleLogic(initialValue: number) { | |
const count = ref(initialValue); | |
const increment = () => { | |
count.value++; | |
}; | |
const decrement = () => { | |
count.value--; | |
}; | |
watch(count, (newValue, oldValue) => { | |
console.log(`Count changed from ${oldValue} to ${newValue}`); | |
}); | |
return { count, increment, decrement }; | |
} | |
<template> | |
<div class="flex flex-col items-center justify-center"> | |
<button | |
@click="decrement" | |
class="bg-blue-500 text-white p-2 rounded" | |
> | |
Decrement | |
</button> | |
<p class="text-lg my-4">Count: {{ count }}</p> | |
<button | |
@click="increment" | |
class="bg-green-500 text-white p-2 rounded" | |
> | |
Increment | |
</button> | |
</div> | |
</template> | |
<script setup lang="ts"> | |
import { useExampleLogic } from './useExampleLogic'; | |
const { count, increment, decrement } = useExampleLogic(0); | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// List Component Pattern | |
// <!-- Before: Direct v-for in the parent component --> | |
<template> | |
<div v-for="item in list" :key="item.id"> | |
<!-- Lots of code specific to each item --> | |
</div> | |
</template> | |
// <!-- After: Abstracting v-for into a child component --> | |
<template> | |
<NewComponentList :list="list" /> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Preserve Object Pattern | |
/* | |
Passing an entire object to a component instead of individual props simplifies components and future-proofs them. | |
*/ | |
// <!-- Using the whole object --> | |
<template> | |
<CustomerDisplay :customer="activeCustomer" /> | |
</template> | |
// <!-- CustomerDisplay.vue --> | |
<template> | |
<div> | |
<p>Name: {{ customer.name }}</p> | |
<p>Age: {{ customer.age }}</p> | |
<p>Address: {{ customer.address }}</p> | |
</div> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Controller Components | |
/* | |
Controller Components in Vue bridge the gap between UI (Humble Components) | |
and business logic (composables). | |
*/ | |
// <!-- TaskController.vue --> | |
<script setup> | |
import useTasks from './composables/useTasks'; | |
// Composables contain the business logic | |
const { tasks, addTask, removeTask } = useTasks(); | |
</script> | |
<template> | |
// <!-- Humble Components provide the UI --> | |
<TaskInput @add-task="addTask" /> | |
<TaskList :tasks="tasks" @remove-task="removeTask" /> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Strategy Pattern | |
/* | |
It allows dynamic switching between different components based on runtime conditions, which improves readability and flexibility. | |
*/ | |
<template> | |
<component :is="currentComponent" /> | |
</template> | |
<script setup> | |
import { computed } from 'vue'; | |
import ComponentOne from './ComponentOne.vue'; | |
import ComponentTwo from './ComponentTwo.vue'; | |
import ComponentThree from './ComponentThree.vue'; | |
const props = defineProps({ | |
conditionType: String, | |
}); | |
const currentComponent = computed(() => { | |
switch (props.conditionType) { | |
case 'one': | |
return ComponentOne; | |
case 'two': | |
return ComponentTwo; | |
case 'three': | |
return ComponentThree; | |
default: | |
return DefaultComponent; | |
} | |
}); | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Hidden Components | |
/* | |
The Hidden Components Pattern involves splitting a complex component into smaller, more focused ones based on how it's used. | |
If different sets of properties are used together exclusively, it indicates potential for component division. | |
*/ | |
<!-- Before Refactoring --> | |
<template> | |
<!-- Really a "Chart" component --> | |
<DataDisplay | |
:chart-data="data" | |
:chart-options="chartOptions" | |
/> | |
<!-- Actually a "Table" component --> | |
<DataDisplay | |
:table-data="data" | |
:table-settings="tableSettings" | |
/> | |
</template> | |
<!-- After Refactoring --> | |
<template> | |
<Chart :data="data" :options="chartOptions" /> | |
<table :data="data" :settings="tableSettings" /> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Insider Trading | |
/* | |
The Insider Trading pattern solves the issue of overly coupled parent-child components in Vue. We simplify by inlining child components into their parent when necessary. | |
*/ | |
<!-- ParentComponent.vue --> | |
<template> | |
<div> | |
<!-- This component uses everything from the parent. | |
So what purpose is it serving? --> | |
<ChildComponent | |
:user-name="userName" | |
:email-address="emailAddress" | |
:phone-number="phoneNumber" | |
@user-update="(val) => $emit('user-update', val)" | |
@email-update="(val) => $emit('email-update', val)" | |
@phone-update="(val) => $emit('phone-update', val)" | |
/> | |
</div> | |
</template> | |
<script setup> | |
defineProps({ | |
userName: String, | |
emailAddress: String, | |
phoneNumber: String, | |
}); | |
defineEmits(['user-update', 'email-update', 'phone-update']); | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Long Components | |
// <!-- Before: A lengthy and complex component --> | |
<template> | |
<div> | |
// <!-- Lots of HTML and logic --> | |
</div> | |
</template> | |
/* <!-- After: Breaking down into smaller components | |
where the name tells you what the code does. --> | |
*/ | |
<template> | |
<ComponentPartOne /> | |
<ComponentPartTwo /> | |
</template> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment