Skip to content

Instantly share code, notes, and snippets.

@Kcko
Last active June 5, 2024 17:28
Show Gist options
  • Save Kcko/3ace57a67536fc7ef57c16eae1f07b73 to your computer and use it in GitHub Desktop.
Save Kcko/3ace57a67536fc7ef57c16eae1f07b73 to your computer and use it in GitHub Desktop.
// 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,
}
}
// 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 };
}
// 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>
// 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>
// 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>
// 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>
// 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>
// 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>
// 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>
// 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>
// 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>
// 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