Table of Contents
Open Table of Contents
Introduction
Writing code that’s both easy to test and easy to read can be a challenge, especially with Vue components. In this blog post, I’m going to share a design idea that will make your Vue components better. This method won’t speed up your code, but it will make it simpler to test and understand. Think of it as a big-picture way to improve your Vue coding style. It’s going to make your life easier when you need to fix or update your components.
Whether you’re new to Vue or have been using it for some time, this tip will help you make your Vue components cleaner and more straightforward.
Understanding Vue Components
A Vue component is like a reusable puzzle piece in your app. Usually, it has three main parts:
- View: This is the template section where you design the user interface.
- Reactivity: Here, Vue’s features like
ref
make the interface interactive. - Business Logic: This is where you process data or manage user actions.
Case Study: snakeGame.vue
Let’s look at a common Vue component, snakeGame.vue
. It mixes the view, reactivity, and business logic, which can make it complex and hard to work with.
Code Sample: Traditional Approach
<template>
<div: HTMLAttributes & ReservedProps
div HTMLAttributes.class?: any
class="game-container">
<canvas: CanvasHTMLAttributes & ReservedProps
canvas ref?: VNodeRef | undefined
ref="const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>
canvas" CanvasHTMLAttributes.width?: Numberish | undefined
width="400" CanvasHTMLAttributes.height?: Numberish | undefined
height="400"></canvas: CanvasHTMLAttributes & ReservedProps
canvas>
</div: HTMLAttributes & ReservedProps
div>
</template>
<script setup lang="ts">
import { const onMounted: CreateHook<any>
onMounted, const onUnmounted: CreateHook<any>
onUnmounted, function ref<T>(value: T): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref } from "vue";
const const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>
canvas = ref<HTMLCanvasElement | null>(value: HTMLCanvasElement | null): Ref<HTMLCanvasElement | null, HTMLCanvasElement | null> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref<HTMLCanvasElement | null>(null);
const const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx = ref<CanvasRenderingContext2D | null>(value: CanvasRenderingContext2D | null): Ref<{
readonly canvas: HTMLCanvasElement;
... 70 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref<CanvasRenderingContext2D | null>(null);
let let snake: {
x: number;
y: number;
}[]
snake = [{ x: number
x: 200, y: number
y: 200 }];
let let direction: {
x: number;
y: number;
}
direction = { x: number
x: 0, y: number
y: 0 };
let let lastDirection: {
x: number;
y: number;
}
lastDirection = { x: number
x: 0, y: number
y: 0 };
let let food: {
x: number;
y: number;
}
food = { x: number
x: 0, y: number
y: 0 };
const const gridSize: 20
gridSize = 20;
let let gameInterval: number | null
gameInterval: number | null = null;
function onMounted(hook: any, target?: ComponentInternalInstance | null): void
onMounted(() => {
if (const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>
canvas.Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>.value: HTMLCanvasElement | null
value) {
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: CanvasRenderingContext2D | {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value = const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>
canvas.Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>.value: HTMLCanvasElement
value.HTMLCanvasElement.getContext(contextId: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D | null (+4 overloads)
Returns an object that provides methods and properties for drawing and manipulating images and graphics on a canvas element in a document. A context object includes information about colors, line widths, fonts, and other graphic parameters that can be drawn on a canvas.getContext("2d");
function resetFoodPosition(): void
resetFoodPosition();
let gameInterval: number | null
gameInterval = var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/setInterval)setInterval(function gameLoop(): void
gameLoop, 100);
}
var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.addEventListener<"keydown">(type: "keydown", listener: (this: Window, ev: KeyboardEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)
Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.
The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture.
When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET.
When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners.
When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed.
If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted.
The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)addEventListener("keydown", function handleKeydown(e: KeyboardEvent): void
handleKeydown);
});
function onUnmounted(hook: any, target?: ComponentInternalInstance | null): void
onUnmounted(() => {
if (let gameInterval: number | null
gameInterval !== null) {
var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.function clearInterval(id: number | undefined): void (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/clearInterval)clearInterval(let gameInterval: number
gameInterval);
}
var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.removeEventListener<"keydown">(type: "keydown", listener: (this: Window, ev: KeyboardEvent) => any, options?: boolean | EventListenerOptions): void (+1 overload)
Removes the event listener in target's event listener list with the same type, callback, and options.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener)removeEventListener("keydown", function handleKeydown(e: KeyboardEvent): void
handleKeydown);
});
function function handleKeydown(e: KeyboardEvent): void
handleKeydown(e: KeyboardEvent
e: KeyboardEvent) {
e: KeyboardEvent
e.Event.preventDefault(): void
If invoked when the cancelable attribute value is true, and while executing a listener for the event with passive set to false, signals to the operation that caused event to be dispatched that it needs to be canceled.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault)preventDefault();
switch (e: KeyboardEvent
e.KeyboardEvent.key: string
[MDN Reference](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/key)key) {
case "ArrowUp":
if (let lastDirection: {
x: number;
y: number;
}
lastDirection.y: number
y !== 0) break;
let direction: {
x: number;
y: number;
}
direction = { x: number
x: 0, y: number
y: -const gridSize: 20
gridSize };
break;
case "ArrowDown":
if (let lastDirection: {
x: number;
y: number;
}
lastDirection.y: number
y !== 0) break;
let direction: {
x: number;
y: number;
}
direction = { x: number
x: 0, y: number
y: const gridSize: 20
gridSize };
break;
case "ArrowLeft":
if (let lastDirection: {
x: number;
y: number;
}
lastDirection.x: number
x !== 0) break;
let direction: {
x: number;
y: number;
}
direction = { x: number
x: -const gridSize: 20
gridSize, y: number
y: 0 };
break;
case "ArrowRight":
if (let lastDirection: {
x: number;
y: number;
}
lastDirection.x: number
x !== 0) break;
let direction: {
x: number;
y: number;
}
direction = { x: number
x: const gridSize: 20
gridSize, y: number
y: 0 };
break;
}
}
function function gameLoop(): void
gameLoop() {
function updateSnakePosition(): void
updateSnakePosition();
if (function checkCollision(): boolean
checkCollision()) {
function endGame(): void
endGame();
return;
}
function checkFoodCollision(): void
checkFoodCollision();
function draw(): void
draw();
let lastDirection: {
x: number;
y: number;
}
lastDirection = { ...let direction: {
x: number;
y: number;
}
direction };
}
function function updateSnakePosition(): void
updateSnakePosition() {
for (let let i: number
i = let snake: {
x: number;
y: number;
}[]
snake.Array<{ x: number; y: number; }>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length - 2; let i: number
i >= 0; let i: number
i--) {
let snake: {
x: number;
y: number;
}[]
snake[let i: number
i + 1] = { ...let snake: {
x: number;
y: number;
}[]
snake[let i: number
i] };
}
let snake: {
x: number;
y: number;
}[]
snake[0].x: number
x += let direction: {
x: number;
y: number;
}
direction.x: number
x;
let snake: {
x: number;
y: number;
}[]
snake[0].y: number
y += let direction: {
x: number;
y: number;
}
direction.y: number
y;
}
function function checkCollision(): boolean
checkCollision() {
return (
let snake: {
x: number;
y: number;
}[]
snake[0].x: number
x < 0 ||
let snake: {
x: number;
y: number;
}[]
snake[0].x: number
x >= 400 ||
let snake: {
x: number;
y: number;
}[]
snake[0].y: number
y < 0 ||
let snake: {
x: number;
y: number;
}[]
snake[0].y: number
y >= 400 ||
let snake: {
x: number;
y: number;
}[]
snake
.Array<{ x: number; y: number; }>.slice(start?: number, end?: number): {
x: number;
y: number;
}[]
Returns a copy of a section of an array.
For both start and end, a negative index can be used to indicate an offset from the end of the array.
For example, -2 refers to the second to last element of the array.slice(1)
.Array<{ x: number; y: number; }>.some(predicate: (value: {
x: number;
y: number;
}, index: number, array: {
x: number;
y: number;
}[]) => unknown, thisArg?: any): boolean
Determines whether the specified callback function returns true for any element of an array.some(segment: {
x: number;
y: number;
}
segment => segment: {
x: number;
y: number;
}
segment.x: number
x === let snake: {
x: number;
y: number;
}[]
snake[0].x: number
x && segment: {
x: number;
y: number;
}
segment.y: number
y === let snake: {
x: number;
y: number;
}[]
snake[0].y: number
y)
);
}
function function checkFoodCollision(): void
checkFoodCollision() {
if (let snake: {
x: number;
y: number;
}[]
snake[0].x: number
x === let food: {
x: number;
y: number;
}
food.x: number
x && let snake: {
x: number;
y: number;
}[]
snake[0].y: number
y === let food: {
x: number;
y: number;
}
food.y: number
y) {
let snake: {
x: number;
y: number;
}[]
snake.Array<{ x: number; y: number; }>.push(...items: {
x: number;
y: number;
}[]): number
Appends new elements to the end of an array, and returns the new length of the array.push({ ...let snake: {
x: number;
y: number;
}[]
snake[let snake: {
x: number;
y: number;
}[]
snake.Array<{ x: number; y: number; }>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length - 1] });
function resetFoodPosition(): void
resetFoodPosition();
}
}
function function resetFoodPosition(): void
resetFoodPosition() {
let food: {
x: number;
y: number;
}
food = {
x: number
x: var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.floor(x: number): number
Returns the greatest integer less than or equal to its numeric argument.floor(var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.random(): number
Returns a pseudorandom number between 0 and 1.random() * 20) * const gridSize: 20
gridSize,
y: number
y: var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.floor(x: number): number
Returns the greatest integer less than or equal to its numeric argument.floor(var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.random(): number
Returns a pseudorandom number between 0 and 1.random() * 20) * const gridSize: 20
gridSize,
};
}
function function draw(): void
draw() {
if (!const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value) return;
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function clearRect(x: number, y: number, w: number, h: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/clearRect)clearRect(0, 0, 400, 400);
function drawGrid(): void
drawGrid();
function drawSnake(): void
drawSnake();
function drawFood(): void
drawFood();
}
function function drawGrid(): void
drawGrid() {
if (!const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value) return;
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.strokeStyle: string | {
addColorStop: (offset: number, color: string) => void;
} | {
setTransform: (transform?: DOMMatrix2DInit) => void;
}
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/strokeStyle)strokeStyle = "#ddd";
for (let let i: number
i = 0; let i: number
i <= 400; let i: number
i += const gridSize: 20
gridSize) {
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function beginPath(): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/beginPath)beginPath();
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function moveTo(x: number, y: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/moveTo)moveTo(let i: number
i, 0);
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function lineTo(x: number, y: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/lineTo)lineTo(let i: number
i, 400);
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function stroke(): void (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/stroke)stroke();
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function moveTo(x: number, y: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/moveTo)moveTo(0, let i: number
i);
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function lineTo(x: number, y: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/lineTo)lineTo(400, let i: number
i);
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function stroke(): void (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/stroke)stroke();
}
}
function function drawSnake(): void
drawSnake() {
if (!const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value) return;
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.fillStyle: string | {
addColorStop: (offset: number, color: string) => void;
} | {
setTransform: (transform?: DOMMatrix2DInit) => void;
}
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/fillStyle)fillStyle = "green";
let snake: {
x: number;
y: number;
}[]
snake.Array<{ x: number; y: number; }>.forEach(callbackfn: (value: {
x: number;
y: number;
}, index: number, array: {
x: number;
y: number;
}[]) => void, thisArg?: any): void
Performs the specified action for each element in an array.forEach(segment: {
x: number;
y: number;
}
segment => {
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value?.function fillRect(x: number, y: number, w: number, h: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/fillRect)fillRect(segment: {
x: number;
y: number;
}
segment.x: number
x, segment: {
x: number;
y: number;
}
segment.y: number
y, const gridSize: 20
gridSize, const gridSize: 20
gridSize);
});
}
function function drawFood(): void
drawFood() {
if (!const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value) return;
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.fillStyle: string | {
addColorStop: (offset: number, color: string) => void;
} | {
setTransform: (transform?: DOMMatrix2DInit) => void;
}
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/fillStyle)fillStyle = "red";
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function fillRect(x: number, y: number, w: number, h: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/fillRect)fillRect(let food: {
x: number;
y: number;
}
food.x: number
x, let food: {
x: number;
y: number;
}
food.y: number
y, const gridSize: 20
gridSize, const gridSize: 20
gridSize);
}
function function endGame(): void
endGame() {
if (let gameInterval: number | null
gameInterval !== null) {
var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.function clearInterval(id: number | undefined): void (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/clearInterval)clearInterval(let gameInterval: number
gameInterval);
}
function alert(message?: any): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/alert)alert("Game Over");
}
</script>
<style>
.game-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style>
Screenshot from the game
Challenges with the Traditional Approach
When you mix the view, reactivity, and business logic all in one file, the component often becomes bulky and hard to maintain. It also becomes difficult to test with unit tests, as you mostly end up doing more complex integration tests.
Introducing the Functional Core, Imperative Shell Pattern
To solve these problems in Vue, we use the “Functional Core, Imperative Shell” pattern. This pattern is key in software architecture and helps you structure your code better:
Functional Core, Imperative Shell Pattern: In this design, the main logic of your app (the ‘Functional Core’) stays pure and without side effects, making it easy to test. The ‘Imperative Shell’ then deals with the outside world, like the UI or databases, and talks to the pure core.
What Are Pure Functions?
In this pattern, pure functions are at the heart of the ‘Functional Core’. A pure function is a concept from functional programming, and it’s special for two reasons:
- Predictability: If you give a pure function the same inputs, it always gives back the same output.
- No Side Effects: Pure functions don’t change anything outside them. They don’t alter external variables, call APIs, or do any input/output.
Pure functions are simpler to test, debug, and understand. They are the foundation of the Functional Core, keeping your app’s business logic clean and manageable.
Applying the Pattern in Vue
In Vue, this pattern has two parts:
- Imperative Shell (
useGameSnake.ts
): This part handles the Vue-specific reactive bits. It’s where your components interact with Vue, managing things like state changes and events. - Functional Core (
pureGameSnake.ts
): This is where your pure business logic lives. It’s separate from Vue, which makes it easier to test and think about your app’s main functions, independent of the UI.
Implementing pureGameSnake.ts
The pureGameSnake.ts
file encapsulates the game’s business logic without any Vue-specific reactivity. This separation means easier testing and clearer logic.
export const const gridSize: 20
gridSize = 20;
interface Position {
Position.x: number
x: number;
Position.y: number
y: number;
}
type type Snake = Position[]
Snake = Position[];
export function function initializeSnake(): Snake
initializeSnake(): type Snake = Position[]
Snake {
return [{ Position.x: number
x: 200, Position.y: number
y: 200 }];
}
export function function moveSnake(snake: Snake, direction: Position): Snake
moveSnake(snake: Snake
snake: type Snake = Position[]
Snake, direction: Position
direction: Position): type Snake = Position[]
Snake {
return snake: Snake
snake.Array<Position>.map<{
x: number;
y: number;
}>(callbackfn: (value: Position, index: number, array: Position[]) => {
x: number;
y: number;
}, thisArg?: any): {
x: number;
y: number;
}[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map((segment: Position
segment, index: number
index) => {
if (index: number
index === 0) {
return { x: number
x: segment: Position
segment.Position.x: number
x + direction: Position
direction.Position.x: number
x, y: number
y: segment: Position
segment.Position.y: number
y + direction: Position
direction.Position.y: number
y };
}
return { ...snake: Snake
snake[index: number
index - 1] };
});
}
export function function isCollision(snake: Snake): boolean
isCollision(snake: Snake
snake: type Snake = Position[]
Snake): boolean {
const const head: Position
head = snake: Snake
snake[0];
return (
const head: Position
head.Position.x: number
x < 0 ||
const head: Position
head.Position.x: number
x >= 400 ||
const head: Position
head.Position.y: number
y < 0 ||
const head: Position
head.Position.y: number
y >= 400 ||
snake: Snake
snake.Array<Position>.slice(start?: number, end?: number): Position[]
Returns a copy of a section of an array.
For both start and end, a negative index can be used to indicate an offset from the end of the array.
For example, -2 refers to the second to last element of the array.slice(1).Array<Position>.some(predicate: (value: Position, index: number, array: Position[]) => unknown, thisArg?: any): boolean
Determines whether the specified callback function returns true for any element of an array.some(segment: Position
segment => segment: Position
segment.Position.x: number
x === const head: Position
head.Position.x: number
x && segment: Position
segment.Position.y: number
y === const head: Position
head.Position.y: number
y)
);
}
export function function randomFoodPosition(): Position
randomFoodPosition(): Position {
return {
Position.x: number
x: var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.floor(x: number): number
Returns the greatest integer less than or equal to its numeric argument.floor(var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.random(): number
Returns a pseudorandom number between 0 and 1.random() * 20) * const gridSize: 20
gridSize,
Position.y: number
y: var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.floor(x: number): number
Returns the greatest integer less than or equal to its numeric argument.floor(var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.random(): number
Returns a pseudorandom number between 0 and 1.random() * 20) * const gridSize: 20
gridSize,
};
}
export function function isFoodEaten(snake: Snake, food: Position): boolean
isFoodEaten(snake: Snake
snake: type Snake = Position[]
Snake, food: Position
food: Position): boolean {
const const head: Position
head = snake: Snake
snake[0];
return const head: Position
head.Position.x: number
x === food: Position
food.Position.x: number
x && const head: Position
head.Position.y: number
y === food: Position
food.Position.y: number
y;
}
Implementing useGameSnake.ts
In useGameSnake.ts
, we manage the Vue-specific state and reactivity, leveraging the pure functions from pureGameSnake.ts
.
import { const onMounted: CreateHook<any>
onMounted, const onUnmounted: CreateHook<any>
onUnmounted, function ref<T>(value: T): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref, interface Ref<T = any, S = T>
Ref } from "vue";
import * as import GameLogic
GameLogic from "./pureGameSnake";
interface Position {
Position.x: number
x: number;
Position.y: number
y: number;
}
type type Snake = Position[]
Snake = Position[];
interface GameState {
GameState.snake: Ref<Snake, Snake>
snake: interface Ref<T = any, S = T>
Ref<type Snake = Position[]
Snake>;
GameState.direction: Ref<Position, Position>
direction: interface Ref<T = any, S = T>
Ref<Position>;
GameState.food: Ref<Position, Position>
food: interface Ref<T = any, S = T>
Ref<Position>;
GameState.gameState: Ref<"over" | "playing", "over" | "playing">
gameState: interface Ref<T = any, S = T>
Ref<"over" | "playing">;
}
export function function useGameSnake(): GameState
useGameSnake(): GameState {
const const snake: Ref<Snake, Snake>
snake: interface Ref<T = any, S = T>
Ref<type Snake = Position[]
Snake> = ref<any>(value: any): any (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref(import GameLogic
GameLogic.initializeSnake());
const const direction: Ref<Position, Position>
direction: interface Ref<T = any, S = T>
Ref<Position> = ref<{
x: number;
y: number;
}>(value: {
x: number;
y: number;
}): Ref<{
x: number;
y: number;
}, {
x: number;
y: number;
} | {
x: number;
y: number;
}> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref({ x: number
x: 0, y: number
y: 0 });
const const food: Ref<Position, Position>
food: interface Ref<T = any, S = T>
Ref<Position> = ref<any>(value: any): any (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref(import GameLogic
GameLogic.randomFoodPosition());
const const gameState: Ref<"over" | "playing", "over" | "playing">
gameState: interface Ref<T = any, S = T>
Ref<"over" | "playing"> = ref<"playing">(value: "playing"): Ref<"playing", "playing"> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref("playing");
let let gameInterval: number | null
gameInterval: number | null = null;
const const startGame: () => void
startGame = (): void => {
let gameInterval: number | null
gameInterval = var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/setInterval)setInterval(() => {
const snake: Ref<Snake, Snake>
snake.Ref<Snake, Snake>.value: Snake
value = import GameLogic
GameLogic.moveSnake(const snake: Ref<Snake, Snake>
snake.Ref<Snake, Snake>.value: Snake
value, const direction: Ref<Position, Position>
direction.Ref<Position, Position>.value: Position
value);
if (import GameLogic
GameLogic.isCollision(const snake: Ref<Snake, Snake>
snake.Ref<Snake, Snake>.value: Snake
value)) {
const gameState: Ref<"over" | "playing", "over" | "playing">
gameState.Ref<"over" | "playing", "over" | "playing">.value: "over" | "playing"
value = "over";
if (let gameInterval: number | null
gameInterval !== null) {
function clearInterval(intervalId: NodeJS.Timeout | string | number | undefined): void (+1 overload)
Cancels a `Timeout` object created by `setInterval()`.clearInterval(let gameInterval: number
gameInterval);
}
} else if (import GameLogic
GameLogic.isFoodEaten(const snake: Ref<Snake, Snake>
snake.Ref<Snake, Snake>.value: Snake
value, const food: Ref<Position, Position>
food.Ref<Position, Position>.value: Position
value)) {
const snake: Ref<Snake, Snake>
snake.Ref<Snake, Snake>.value: Snake
value.Array<Position>.push(...items: Position[]): number
Appends new elements to the end of an array, and returns the new length of the array.push({ ...const snake: Ref<Snake, Snake>
snake.Ref<Snake, Snake>.value: Snake
value[const snake: Ref<Snake, Snake>
snake.Ref<Snake, Snake>.value: Snake
value.Array<Position>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length - 1] });
const food: Ref<Position, Position>
food.Ref<Position, Position>.value: Position
value = import GameLogic
GameLogic.randomFoodPosition();
}
}, 100);
};
function onMounted(hook: any, target?: ComponentInternalInstance | null): void
onMounted(const startGame: () => void
startGame);
function onUnmounted(hook: any, target?: ComponentInternalInstance | null): void
onUnmounted(() => {
if (let gameInterval: number | null
gameInterval !== null) {
function clearInterval(intervalId: NodeJS.Timeout | string | number | undefined): void (+1 overload)
Cancels a `Timeout` object created by `setInterval()`.clearInterval(let gameInterval: number
gameInterval);
}
});
return { GameState.snake: Ref<Snake, Snake>
snake, GameState.direction: Ref<Position, Position>
direction, GameState.food: Ref<Position, Position>
food, GameState.gameState: Ref<"over" | "playing", "over" | "playing">
gameState };
}
Refactoring gameSnake.vue
Now, our gameSnake.vue
is more focused, using useGameSnake.ts
for managing state and reactivity, while the view remains within the template.
<template>
<div: HTMLAttributes & ReservedProps
div HTMLAttributes.class?: any
class="game-container">
<canvas: CanvasHTMLAttributes & ReservedProps
canvas ref?: VNodeRef | undefined
ref="const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>
canvas" CanvasHTMLAttributes.width?: Numberish | undefined
width="400" CanvasHTMLAttributes.height?: Numberish | undefined
height="400"></canvas: CanvasHTMLAttributes & ReservedProps
canvas>
</div: HTMLAttributes & ReservedProps
div>
</template>
<script setup lang="ts">
import { function ref<T>(value: T): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref, const onMounted: CreateHook<any>
onMounted, function watch<T, Immediate extends Readonly<boolean> = false>(source: WatchSource<T>, cb: WatchCallback<T, MaybeUndefined<T, Immediate>>, options?: WatchOptions<Immediate>): WatchHandle (+3 overloads)
watch, const onUnmounted: CreateHook<any>
onUnmounted } from "vue";
import { import useGameSnake
useGameSnake } from "./useGameSnake.ts";
import { import gridSize
gridSize } from "./pureGameSnake";
const { const snake: any
snake, const direction: any
direction, const food: any
food, const gameState: any
gameState } = import useGameSnake
useGameSnake();
const const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>
canvas = ref<HTMLCanvasElement | null>(value: HTMLCanvasElement | null): Ref<HTMLCanvasElement | null, HTMLCanvasElement | null> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref<HTMLCanvasElement | null>(null);
const const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx = ref<CanvasRenderingContext2D | null>(value: CanvasRenderingContext2D | null): Ref<{
readonly canvas: HTMLCanvasElement;
... 70 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref<CanvasRenderingContext2D | null>(null);
let let lastDirection: {
x: number;
y: number;
}
lastDirection = { x: number
x: 0, y: number
y: 0 };
function onMounted(hook: any, target?: ComponentInternalInstance | null): void
onMounted(() => {
if (const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>
canvas.Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>.value: HTMLCanvasElement | null
value) {
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: CanvasRenderingContext2D | {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value = const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>
canvas.Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>.value: HTMLCanvasElement
value.HTMLCanvasElement.getContext(contextId: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D | null (+4 overloads)
Returns an object that provides methods and properties for drawing and manipulating images and graphics on a canvas element in a document. A context object includes information about colors, line widths, fonts, and other graphic parameters that can be drawn on a canvas.getContext("2d");
function draw(): void
draw();
}
var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.addEventListener<"keydown">(type: "keydown", listener: (this: Window, ev: KeyboardEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)
Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.
The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture.
When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET.
When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners.
When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed.
If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted.
The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)addEventListener("keydown", function handleKeydown(e: KeyboardEvent): void
handleKeydown);
});
function onUnmounted(hook: any, target?: ComponentInternalInstance | null): void
onUnmounted(() => {
var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.removeEventListener<"keydown">(type: "keydown", listener: (this: Window, ev: KeyboardEvent) => any, options?: boolean | EventListenerOptions): void (+1 overload)
Removes the event listener in target's event listener list with the same type, callback, and options.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener)removeEventListener("keydown", function handleKeydown(e: KeyboardEvent): void
handleKeydown);
});
watch<any, false>(sources: any, cb: WatchCallback<any, any>, options?: WatchOptions<false> | undefined): WatchHandle (+3 overloads)
watch(const gameState: any
gameState, state: any
state => {
if (state: any
state === "over") {
function alert(message?: any): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/alert)alert("Game Over");
}
});
function function handleKeydown(e: KeyboardEvent): void
handleKeydown(e: KeyboardEvent
e: KeyboardEvent) {
e: KeyboardEvent
e.Event.preventDefault(): void
If invoked when the cancelable attribute value is true, and while executing a listener for the event with passive set to false, signals to the operation that caused event to be dispatched that it needs to be canceled.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault)preventDefault();
switch (e: KeyboardEvent
e.KeyboardEvent.key: string
[MDN Reference](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/key)key) {
case "ArrowUp":
if (let lastDirection: {
x: number;
y: number;
}
lastDirection.y: number
y !== 0) break;
const direction: any
direction.value = { x: number
x: 0, y: number
y: -import gridSize
gridSize };
break;
case "ArrowDown":
if (let lastDirection: {
x: number;
y: number;
}
lastDirection.y: number
y !== 0) break;
const direction: any
direction.value = { x: number
x: 0, y: any
y: import gridSize
gridSize };
break;
case "ArrowLeft":
if (let lastDirection: {
x: number;
y: number;
}
lastDirection.x: number
x !== 0) break;
const direction: any
direction.value = { x: number
x: -import gridSize
gridSize, y: number
y: 0 };
break;
case "ArrowRight":
if (let lastDirection: {
x: number;
y: number;
}
lastDirection.x: number
x !== 0) break;
const direction: any
direction.value = { x: any
x: import gridSize
gridSize, y: number
y: 0 };
break;
}
let lastDirection: {
x: number;
y: number;
}
lastDirection = { ...const direction: any
direction.value };
}
watch<[any, any], false>(sources: [any, any] | readonly [any, any], cb: WatchCallback<any, any>, options?: WatchOptions<false> | undefined): WatchHandle (+3 overloads)
watch(
[const snake: any
snake, const food: any
food],
() => {
function draw(): void
draw();
},
{ WatchOptions<Immediate = boolean>.deep?: number | boolean | undefined
deep: true }
);
function function draw(): void
draw() {
if (!const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value) return;
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function clearRect(x: number, y: number, w: number, h: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/clearRect)clearRect(0, 0, 400, 400);
function drawGrid(): void
drawGrid();
function drawSnake(): void
drawSnake();
function drawFood(): void
drawFood();
}
function function drawGrid(): void
drawGrid() {
if (!const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value) return;
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.strokeStyle: string | {
addColorStop: (offset: number, color: string) => void;
} | {
setTransform: (transform?: DOMMatrix2DInit) => void;
}
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/strokeStyle)strokeStyle = "#ddd";
for (let let i: number
i = 0; let i: number
i <= 400; let i: number
i += import gridSize
gridSize) {
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function beginPath(): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/beginPath)beginPath();
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function moveTo(x: number, y: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/moveTo)moveTo(let i: number
i, 0);
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function lineTo(x: number, y: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/lineTo)lineTo(let i: number
i, 400);
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function stroke(): void (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/stroke)stroke();
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function moveTo(x: number, y: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/moveTo)moveTo(0, let i: number
i);
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function lineTo(x: number, y: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/lineTo)lineTo(400, let i: number
i);
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
}
value.function stroke(): void (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/stroke)stroke();
}
}
function function drawSnake(): void
drawSnake() {
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value.fillStyle: string | {
addColorStop: (offset: number, color: string) => void;
} | {
setTransform: (transform?: DOMMatrix2DInit) => void;
}
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/fillStyle)fillStyle = "green";
const snake: any
snake.value.forEach(segment: any
segment => {
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value.function fillRect(x: number, y: number, w: number, h: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/fillRect)fillRect(segment: any
segment.x, segment: any
segment.y, import gridSize
gridSize, import gridSize
gridSize);
});
}
function function drawFood(): void
drawFood() {
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value.fillStyle: string | {
addColorStop: (offset: number, color: string) => void;
} | {
setTransform: (transform?: DOMMatrix2DInit) => void;
}
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/fillStyle)fillStyle = "red";
const ctx: Ref<{
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null, CanvasRenderingContext2D | ... 1 more ... | null>
ctx.Ref<{ readonly canvas: HTMLCanvasElement; getContextAttributes: () => CanvasRenderingContext2DSettings; globalAlpha: number; globalCompositeOperation: GlobalCompositeOperation; ... 67 more ...; drawFocusIfNeeded: { ...; }; } | null, CanvasRenderingContext2D | ... 1 more ... | null>.value: {
readonly canvas: HTMLCanvasElement;
getContextAttributes: () => CanvasRenderingContext2DSettings;
globalAlpha: number;
globalCompositeOperation: GlobalCompositeOperation;
... 67 more ...;
drawFocusIfNeeded: {
...;
};
} | null
value.function fillRect(x: number, y: number, w: number, h: number): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/fillRect)fillRect(const food: any
food.value.x, const food: any
food.value.y, import gridSize
gridSize, import gridSize
gridSize);
}
</script>
<style>
.game-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style>
Advantages of the Functional Core, Imperative Shell Pattern
The Functional Core, Imperative Shell pattern greatly enhances the testability and maintainability of Vue components. By decoupling the business logic from the framework-specific code, this pattern offers several key advantages:
Simplified Testing
When business logic is intertwined with Vue’s reactivity and component structure, testing can be cumbersome. Traditional unit testing becomes challenging, often leading to reliance on integration tests which are less granular and more complex. By extracting the core logic into pure functions (as in pureGameSnake.ts
), we can easily write unit tests for each function. This isolation simplifies testing dramatically, as each piece of logic can be tested independently of Vue’s reactivity system.
Enhanced Maintainability
The Functional Core, Imperative Shell pattern results in a clearer separation of concerns. Vue components become leaner, focusing mainly on the user interface and reactivity, while the pure business logic resides in separate, framework-agnostic files. This separation makes the code easier to read, understand, and modify. Maintenance becomes more manageable, especially as the application scales.
Framework Agnosticism
A significant advantage of this pattern is the portability of your business logic. The pure functions in the Functional Core are not tied to any specific UI framework. Should you ever need to switch from Vue to another framework, or if Vue undergoes major changes, your core logic remains intact and reusable. This flexibility safeguards your code against technological shifts and changes in project requirements.
Testing Complexities in Traditional Vue Components vs. Functional Core, Imperative Shell Pattern
Challenges in Testing Traditional Components
Testing traditional Vue components, where view, reactivity, and business logic are all intertwined, can be quite challenging. In such components, unit tests become difficult to implement effectively because:
- The tests often end up being more like integration tests, which are broader and less precise.
- Mocking dependencies and Vue’s reactivity system can be complicated and time-consuming.
- Ensuring that tests cover all aspects of the component’s functionality, including its reactive behavior and side effects, adds complexity.
This complexity in testing can lead to less confidence in the tests and, by extension, the stability of the component itself.
Simplified Testing with Functional Core, Imperative Shell Pattern
By refactoring components to use the Functional Core, Imperative Shell pattern, testing becomes much more straightforward:
- Isolated Business Logic: With pure functions in the Functional Core, you can write simple unit tests for your business logic without worrying about Vue’s reactivity or component states.
- Predictable Outcomes: Pure functions produce predictable outputs for given inputs, making them easy to test.
- Reduced Complexity: Since the reactive and side-effect-laden parts of your code are isolated in the Imperative Shell, you can focus on testing the interaction with Vue’s reactivity separately. This separation simplifies the testing of each part.
The end result is a more modular, testable, and maintainable codebase, where each piece can be tested in isolation, leading to higher quality and more reliable Vue components.
Conclusion
Implementing the Functional Core, Imperative Shell pattern in Vue applications leads to a more robust, testable, and maintainable codebase. It not only aids in the current development process but also prepares your code for future changes and scalability. This approach, while requiring an upfront effort in restructuring, pays off significantly in the long run, making it a wise choice for any Vue developer looking to improve their application’s architecture and quality.