Skip to content

How to Write Clean Vue Components

Published: at 

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:

  1. View: This is the template section where you design the user interface.
  2. Reactivity: Here, Vue’s features like ref make the interface interactive.
  3. Business Logic: This is where you process data or manage user actions.

Architecture


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 & ReservedPropsdiv HTMLAttributes.class?: anyclass="game-container">
    <canvas: CanvasHTMLAttributes & ReservedPropscanvas ref?: VNodeRef | undefinedref="canvas: HTMLCanvasElement | nullcanvas" CanvasHTMLAttributes.width?: Numberish | undefinedwidth="400" CanvasHTMLAttributes.height?: Numberish | undefinedheight="400"></canvas: CanvasHTMLAttributes & ReservedPropscanvas>
  </div: HTMLAttributes & ReservedPropsdiv>
</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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
<CanvasRenderingContext2D | null>(null);
let
let snake: {
    x: number;
    y: number;
}[]
snake
= [{ x: numberx: 200, y: numbery: 200 }];
let
let direction: {
    x: number;
    y: number;
}
direction
= { x: numberx: 0, y: numbery: 0 };
let
let lastDirection: {
    x: number;
    y: number;
}
lastDirection
= { x: numberx: 0, y: numbery: 0 };
let
let food: {
    x: number;
    y: number;
}
food
= { x: numberx: 0, y: numbery: 0 };
const const gridSize: 20gridSize = 20; let let gameInterval: number | nullgameInterval: number | null = null; function onMounted(hook: any, target?: ComponentInternalInstance | null): voidonMounted(() => { if (const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>canvas.Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>.value: HTMLCanvasElement | nullvalue) {
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: HTMLCanvasElementvalue.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.
@paramcontextId The identifier (ID) of the type of canvas to create. Internet Explorer 9 and Internet Explorer 10 support only a 2-D context using canvas.getContext("2d"); IE11 Preview also supports 3-D or WebGL context using canvas.getContext("experimental-webgl"); [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/getContext)
getContext
("2d");
function resetFoodPosition(): voidresetFoodPosition(); let gameInterval: number | nullgameInterval = 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(): voidgameLoop, 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): voidhandleKeydown);
}); function onUnmounted(hook: any, target?: ComponentInternalInstance | null): voidonUnmounted(() => { if (let gameInterval: number | nullgameInterval !== 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: numbergameInterval);
} 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): voidhandleKeydown);
}); function function handleKeydown(e: KeyboardEvent): voidhandleKeydown(e: KeyboardEvente: KeyboardEvent) { e: KeyboardEvente.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: KeyboardEvente.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: numbery !== 0) break;
let direction: {
    x: number;
    y: number;
}
direction
= { x: numberx: 0, y: numbery: -const gridSize: 20gridSize };
break; case "ArrowDown": if (
let lastDirection: {
    x: number;
    y: number;
}
lastDirection
.y: numbery !== 0) break;
let direction: {
    x: number;
    y: number;
}
direction
= { x: numberx: 0, y: numbery: const gridSize: 20gridSize };
break; case "ArrowLeft": if (
let lastDirection: {
    x: number;
    y: number;
}
lastDirection
.x: numberx !== 0) break;
let direction: {
    x: number;
    y: number;
}
direction
= { x: numberx: -const gridSize: 20gridSize, y: numbery: 0 };
break; case "ArrowRight": if (
let lastDirection: {
    x: number;
    y: number;
}
lastDirection
.x: numberx !== 0) break;
let direction: {
    x: number;
    y: number;
}
direction
= { x: numberx: const gridSize: 20gridSize, y: numbery: 0 };
break; } } function function gameLoop(): voidgameLoop() { function updateSnakePosition(): voidupdateSnakePosition(); if (function checkCollision(): booleancheckCollision()) { function endGame(): voidendGame(); return; } function checkFoodCollision(): voidcheckFoodCollision(); function draw(): voiddraw();
let lastDirection: {
    x: number;
    y: number;
}
lastDirection
= { ...
let direction: {
    x: number;
    y: number;
}
direction
};
} function function updateSnakePosition(): voidupdateSnakePosition() { for (let let i: numberi =
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: numberi >= 0; let i: numberi--) {
let snake: {
    x: number;
    y: number;
}[]
snake
[let i: numberi + 1] = { ...
let snake: {
    x: number;
    y: number;
}[]
snake
[let i: numberi] };
}
let snake: {
    x: number;
    y: number;
}[]
snake
[0].x: numberx +=
let direction: {
    x: number;
    y: number;
}
direction
.x: numberx;
let snake: {
    x: number;
    y: number;
}[]
snake
[0].y: numbery +=
let direction: {
    x: number;
    y: number;
}
direction
.y: numbery;
} function function checkCollision(): booleancheckCollision() { return (
let snake: {
    x: number;
    y: number;
}[]
snake
[0].x: numberx < 0 ||
let snake: {
    x: number;
    y: number;
}[]
snake
[0].x: numberx >= 400 ||
let snake: {
    x: number;
    y: number;
}[]
snake
[0].y: numbery < 0 ||
let snake: {
    x: number;
    y: number;
}[]
snake
[0].y: numbery >= 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.
@paramstart The beginning index of the specified portion of the array. If start is undefined, then the slice begins at index 0.@paramend The end index of the specified portion of the array. This is exclusive of the element at the index 'end'. If end is undefined, then the slice extends to the end 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.
@parampredicate A function that accepts up to three arguments. The some method calls the predicate function for each element in the array until the predicate returns a value which is coercible to the Boolean value true, or until the end of the array.@paramthisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
some
(
segment: {
    x: number;
    y: number;
}
segment
=>
segment: {
    x: number;
    y: number;
}
segment
.x: numberx ===
let snake: {
    x: number;
    y: number;
}[]
snake
[0].x: numberx &&
segment: {
    x: number;
    y: number;
}
segment
.y: numbery ===
let snake: {
    x: number;
    y: number;
}[]
snake
[0].y: numbery)
); } function function checkFoodCollision(): voidcheckFoodCollision() { if (
let snake: {
    x: number;
    y: number;
}[]
snake
[0].x: numberx ===
let food: {
    x: number;
    y: number;
}
food
.x: numberx &&
let snake: {
    x: number;
    y: number;
}[]
snake
[0].y: numbery ===
let food: {
    x: number;
    y: number;
}
food
.y: numbery) {
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.
@paramitems New elements to add to 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(): voidresetFoodPosition(); } } function function resetFoodPosition(): voidresetFoodPosition() {
let food: {
    x: number;
    y: number;
}
food
= {
x: numberx: 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.
@paramx A numeric expression.
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: 20gridSize,
y: numbery: 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.
@paramx A numeric expression.
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: 20gridSize,
}; } function function draw(): voiddraw() { 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(): voiddrawGrid(); function drawSnake(): voiddrawSnake(); function drawFood(): voiddrawFood(); } function function drawGrid(): voiddrawGrid() { 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: numberi = 0; let i: numberi <= 400; let i: numberi += const gridSize: 20gridSize) {
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: numberi, 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: numberi, 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: numberi);
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: numberi);
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(): voiddrawSnake() { 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.
@paramcallbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
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: numberx,
segment: {
    x: number;
    y: number;
}
segment
.y: numbery, const gridSize: 20gridSize, const gridSize: 20gridSize);
}); } function function drawFood(): voiddrawFood() { 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: numberx,
let food: {
    x: number;
    y: number;
}
food
.y: numbery, const gridSize: 20gridSize, const gridSize: 20gridSize);
} function function endGame(): voidendGame() { if (let gameInterval: number | nullgameInterval !== 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: numbergameInterval);
} 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

Snake Game Screenshot

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.

Functional core Diagram

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:

  1. Predictability: If you give a pure function the same inputs, it always gives back the same output.
  2. 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:


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: 20gridSize = 20;

interface Position {
  Position.x: numberx: number;
  Position.y: numbery: number;
}

type type Snake = Position[]Snake = Position[];

export function function initializeSnake(): SnakeinitializeSnake(): type Snake = Position[]Snake {
  return [{ Position.x: numberx: 200, Position.y: numbery: 200 }];
}

export function function moveSnake(snake: Snake, direction: Position): SnakemoveSnake(snake: Snakesnake: type Snake = Position[]Snake, direction: Positiondirection: Position): type Snake = Position[]Snake {
  return snake: Snakesnake.
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.
@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map
((segment: Positionsegment, index: numberindex) => {
if (index: numberindex === 0) { return { x: numberx: segment: Positionsegment.Position.x: numberx + direction: Positiondirection.Position.x: numberx, y: numbery: segment: Positionsegment.Position.y: numbery + direction: Positiondirection.Position.y: numbery }; } return { ...snake: Snakesnake[index: numberindex - 1] }; }); } export function function isCollision(snake: Snake): booleanisCollision(snake: Snakesnake: type Snake = Position[]Snake): boolean { const const head: Positionhead = snake: Snakesnake[0]; return ( const head: Positionhead.Position.x: numberx < 0 || const head: Positionhead.Position.x: numberx >= 400 || const head: Positionhead.Position.y: numbery < 0 || const head: Positionhead.Position.y: numbery >= 400 || snake: Snakesnake.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.
@paramstart The beginning index of the specified portion of the array. If start is undefined, then the slice begins at index 0.@paramend The end index of the specified portion of the array. This is exclusive of the element at the index 'end'. If end is undefined, then the slice extends to the end 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.
@parampredicate A function that accepts up to three arguments. The some method calls the predicate function for each element in the array until the predicate returns a value which is coercible to the Boolean value true, or until the end of the array.@paramthisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
some
(segment: Positionsegment => segment: Positionsegment.Position.x: numberx === const head: Positionhead.Position.x: numberx && segment: Positionsegment.Position.y: numbery === const head: Positionhead.Position.y: numbery)
); } export function function randomFoodPosition(): PositionrandomFoodPosition(): Position { return { Position.x: numberx: 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.
@paramx A numeric expression.
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: 20gridSize,
Position.y: numbery: 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.
@paramx A numeric expression.
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: 20gridSize,
}; } export function function isFoodEaten(snake: Snake, food: Position): booleanisFoodEaten(snake: Snakesnake: type Snake = Position[]Snake, food: Positionfood: Position): boolean { const const head: Positionhead = snake: Snakesnake[0]; return const head: Positionhead.Position.x: numberx === food: Positionfood.Position.x: numberx && const head: Positionhead.Position.y: numbery === food: Positionfood.Position.y: numbery; }

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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
, interface Ref<T = any, S = T>Ref } from "vue";
import * as import GameLogicGameLogic from "./pureGameSnake"; interface Position { Position.x: numberx: number; Position.y: numbery: 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(): GameStateuseGameSnake(): 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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
(import GameLogicGameLogic.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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
({ x: numberx: 0, y: numbery: 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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
(import GameLogicGameLogic.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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
("playing");
let let gameInterval: number | nullgameInterval: number | null = null; const const startGame: () => voidstartGame = (): void => { let gameInterval: number | nullgameInterval = 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: Snakevalue = import GameLogicGameLogic.moveSnake(const snake: Ref<Snake, Snake>snake.Ref<Snake, Snake>.value: Snakevalue, const direction: Ref<Position, Position>direction.Ref<Position, Position>.value: Positionvalue); if (import GameLogicGameLogic.isCollision(const snake: Ref<Snake, Snake>snake.Ref<Snake, Snake>.value: Snakevalue)) { const gameState: Ref<"over" | "playing", "over" | "playing">gameState.Ref<"over" | "playing", "over" | "playing">.value: "over" | "playing"value = "over"; if (let gameInterval: number | nullgameInterval !== null) { function clearInterval(intervalId: NodeJS.Timeout | string | number | undefined): void (+1 overload)
Cancels a `Timeout` object created by `setInterval()`.
@sincev0.0.1@paramtimeout A `Timeout` object as returned by {@link setInterval} or the `primitive` of the `Timeout` object as a string or a number.
clearInterval
(let gameInterval: numbergameInterval);
} } else if (import GameLogicGameLogic.isFoodEaten(const snake: Ref<Snake, Snake>snake.Ref<Snake, Snake>.value: Snakevalue, const food: Ref<Position, Position>food.Ref<Position, Position>.value: Positionvalue)) { const snake: Ref<Snake, Snake>snake.Ref<Snake, Snake>.value: Snakevalue.Array<Position>.push(...items: Position[]): number
Appends new elements to the end of an array, and returns the new length of the array.
@paramitems New elements to add to the array.
push
({ ...const snake: Ref<Snake, Snake>snake.Ref<Snake, Snake>.value: Snakevalue[const snake: Ref<Snake, Snake>snake.Ref<Snake, Snake>.value: Snakevalue.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: Positionvalue = import GameLogicGameLogic.randomFoodPosition(); } }, 100); }; function onMounted(hook: any, target?: ComponentInternalInstance | null): voidonMounted(const startGame: () => voidstartGame); function onUnmounted(hook: any, target?: ComponentInternalInstance | null): voidonUnmounted(() => { if (let gameInterval: number | nullgameInterval !== null) { function clearInterval(intervalId: NodeJS.Timeout | string | number | undefined): void (+1 overload)
Cancels a `Timeout` object created by `setInterval()`.
@sincev0.0.1@paramtimeout A `Timeout` object as returned by {@link setInterval} or the `primitive` of the `Timeout` object as a string or a number.
clearInterval
(let gameInterval: numbergameInterval);
} }); 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 & ReservedPropsdiv HTMLAttributes.class?: anyclass="game-container">
    <canvas: CanvasHTMLAttributes & ReservedPropscanvas ref?: VNodeRef | undefinedref="const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>canvas" CanvasHTMLAttributes.width?: Numberish | undefinedwidth="400" CanvasHTMLAttributes.height?: Numberish | undefinedheight="400"></canvas: CanvasHTMLAttributes & ReservedPropscanvas>
  </div: HTMLAttributes & ReservedPropsdiv>
</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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
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 useGameSnakeuseGameSnake } from "./useGameSnake.ts"; import { import gridSizegridSize } from "./pureGameSnake"; const { const snake: anysnake, const direction: anydirection, const food: anyfood, const gameState: anygameState } = import useGameSnakeuseGameSnake(); 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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
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.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
<CanvasRenderingContext2D | null>(null);
let
let lastDirection: {
    x: number;
    y: number;
}
lastDirection
= { x: numberx: 0, y: numbery: 0 };
function onMounted(hook: any, target?: ComponentInternalInstance | null): voidonMounted(() => { if (const canvas: Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>canvas.Ref<HTMLCanvasElement | null, HTMLCanvasElement | null>.value: HTMLCanvasElement | nullvalue) {
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: HTMLCanvasElementvalue.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.
@paramcontextId The identifier (ID) of the type of canvas to create. Internet Explorer 9 and Internet Explorer 10 support only a 2-D context using canvas.getContext("2d"); IE11 Preview also supports 3-D or WebGL context using canvas.getContext("experimental-webgl"); [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/getContext)
getContext
("2d");
function draw(): voiddraw(); } 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): voidhandleKeydown);
}); function onUnmounted(hook: any, target?: ComponentInternalInstance | null): voidonUnmounted(() => { 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): voidhandleKeydown);
}); watch<any, false>(sources: any, cb: WatchCallback<any, any>, options?: WatchOptions<false> | undefined): WatchHandle (+3 overloads)watch(const gameState: anygameState, state: anystate => { if (state: anystate === "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): voidhandleKeydown(e: KeyboardEvente: KeyboardEvent) { e: KeyboardEvente.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: KeyboardEvente.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: numbery !== 0) break;
const direction: anydirection.value = { x: numberx: 0, y: numbery: -import gridSizegridSize }; break; case "ArrowDown": if (
let lastDirection: {
    x: number;
    y: number;
}
lastDirection
.y: numbery !== 0) break;
const direction: anydirection.value = { x: numberx: 0, y: anyy: import gridSizegridSize }; break; case "ArrowLeft": if (
let lastDirection: {
    x: number;
    y: number;
}
lastDirection
.x: numberx !== 0) break;
const direction: anydirection.value = { x: numberx: -import gridSizegridSize, y: numbery: 0 }; break; case "ArrowRight": if (
let lastDirection: {
    x: number;
    y: number;
}
lastDirection
.x: numberx !== 0) break;
const direction: anydirection.value = { x: anyx: import gridSizegridSize, y: numbery: 0 }; break; }
let lastDirection: {
    x: number;
    y: number;
}
lastDirection
= { ...const direction: anydirection.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: anysnake, const food: anyfood], () => { function draw(): voiddraw(); }, { WatchOptions<Immediate = boolean>.deep?: number | boolean | undefineddeep: true } ); function function draw(): voiddraw() { 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(): voiddrawGrid(); function drawSnake(): voiddrawSnake(); function drawFood(): voiddrawFood(); } function function drawGrid(): voiddrawGrid() { 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: numberi = 0; let i: numberi <= 400; let i: numberi += import gridSizegridSize) {
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: numberi, 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: numberi, 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: numberi);
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: numberi);
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(): voiddrawSnake() {
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: anysnake.value.forEach(segment: anysegment => {
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: anysegment.x, segment: anysegment.y, import gridSizegridSize, import gridSizegridSize);
}); } function function drawFood(): voiddrawFood() {
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: anyfood.value.x, const food: anyfood.value.y, import gridSizegridSize, import gridSizegridSize);
} </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:

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:

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.

Blog Conclusion Diagram