Nuxt3のuseAsyncDataを使いやすくしてみた

useAsyncDataとは

Nuxt3から標準で提供されているAPI。 非同期処理に関する状態管理を行ってくれる。

nuxt.com

1秒後に50%の確率で成功する非同期関数をuseAsyncDataで使ってみた例

const { data, status, error, refresh, execute, clear } = useAsyncData(
  'key',
  () => {
    return new Promise<string>((resolve, reject) => {
      setTimeout(() => {
        if (Math.random() > 0.5) {
          resolve('Success');
        } else {
          reject('Failed');
        }
      }, 1000);
    });
  }
);

引数

  • key: string
    • 第一引数
    • 非同期関数の返り値をキャッシュするためのキー
  • handler: (ctx?: NuxtApp) => Promise<T>
    • 第二引数
    • 非同期関数
  • options: AsyncDataOptions<T>
    • 第三引数(省略可能)
    • オプション

引数に関する詳細はuseAsyncData > Params

返り値

  • data: Ref<T | null>
    • 非同期関数の実行結果を保持する
  • status: Ref<'idle' | 'loading' | 'success' | 'error'>
    • 非同期関数の実行状態を保持する
  • error: Ref<Error | null>
    • 非同期関数の実行エラーを保持する
  • refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
    • 非同期関数を実行する
  • execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
    • 非同期関数を実行する
  • clear: () => void

ここまでがuseAsyncDataの基本的な説明。

使いやすくしてみた

提供されているuseAsyncDataはあくまで非同期処理に関する状態管理を行うことを目的としているので、非同期処理周辺までカバーされていない。

どのようなアプリにも汎用的に使えるようにしているので十分すぎるくらい機能は提供されている。

ただアプリを作っていると非同期処理の開始前、成功時、エラー時、終了時に処理を行いたいケースが多くなる。

特にAPIリクエストするような非同期処理であれば、開始前にフォームのバリデーションエラーをリセットしたり、成功やエラー時にその旨を表すUIを描画したりすると思う。

実装

基本的にはuseAsyncDataと同じ引数と返り値にしている。

追加したのはAsyncFetchOptions という開始前、成功時、エラー時、終了時に処理をオプションとして渡せるようにした。

それらのオプションとhandlerを組み合わせて一つの非同期関数としてuseAsyncDataに渡すようにした。

こうすることによって各タイミングで設定した処理を実行することができるようになった。

import type { AsyncDataOptions } from '#app';

type AsyncFetchOptions<T> = {
  onStart?: () => void | Promise<void>;
  onSuccess?: (data: T) => void | Promise<void>;
  onError?: (error: unknown) => void | Promise<void>;
  onFinally?: () => void | Promise<void>;
};

export const useAsyncFetch = <T>(
  key: string,
  handler: () => Promise<T>,
  options: AsyncDataOptions<T> & AsyncFetchOptions<T>
) => {
  const asyncData = useAsyncData(
    key,
    async () => {
      await options.onStart?.();
      try {
        const data = await handler();
        await options.onSuccess?.(data);
        return data;
      } catch (error) {
        await options.onError?.(error);
        throw error;
      } finally {
        await options.onFinally?.();
      }
    },
    options
  );

  return {
    ...asyncData,
  };
};

さらに更新系のAPIリクエストにも対応してみる

useAsyncDataはおそらく非同期処理でデータ取得することを目的に提供されているが、statusとして非同期関数の実行状態を管理してくれるのはとても利便性が高い。

なので、immediateはデフォルトtrueのところをfalseに設定して、即時実行されないようにしてあとは同じように実装する。

type AsyncExecOptions<T> = {
  onStart?: () => void | Promise<void>;
  onSuccess?: (data: T) => void | Promise<void>;
  onError?: (error: unknown) => void | Promise<void>;
  onFinally?: () => void | Promise<void>;
};

export const useAsyncExec = <T>(
  handler: () => Promise<T>,
  options: AsyncExecOptions<T>
) => {
  const asyncData = useAsyncData(
    async () => {
      await options.onStart?.();
      try {
        const data = await handler();
        await options.onSuccess?.(data);
        return data;
      } catch (error) {
        await options.onError?.(error);
        throw error;
      } finally {
        await options.onFinally?.();
      }
    },
    { immediate: false }
  );

  return {
    ...asyncData,
  };
};

まとめ

Nuxt3から提供されているuseAsyncDataを使って、よりAPIリクエストを扱いやすくしてみた。もしかするとaxiosとかfetch APIを組み合わせてより特化させても面白いかもしれない。またIndexedDBなども非同期処理でデータ取得・更新を行うようになっているのでそちらでも使えるようにできると思った。

Goのポインタ使いどころまとめ

Goを使った実務で業務アプリを開発しているとポインタを正しく使えているのかわからないことがあったので、使いどころを調べてみた。

【結論】使いどころ評価

No. タイトル 使用率 コメント
1 大きなデータ構造の効率的な渡し方 大きな構造体はポインタで渡してコピーを避ける。ただし、ポインタ使用でキャッシュローカリティが低下し、パフォーマンスが下がる場合もあるので注意が必要
2 関数内での値の変更 副作用を避けるため、イミュータブル性を保つことが多く、基本的には使わない
3 可変なデータ構造の操作 業務アプリ開発だとこのデータ構造に遭遇することが少ない
4 nil値の活用 nilを使った存在確認やエラーハンドリングで頻繁に使用する
5 インターフェースとメソッドレシーバー 一貫性を保つためにポインタレシーバに統一することが多い
6 同期処理 並行処理でデータ競合を防ぐために使用する
7 再帰的なデータ型の定義 業務アプリ開発だとこのデータ構造に遭遇することが少ない

1. 大きなデータ構造の効率的な渡し方

大きな構造体や配列を関数に渡す際、値渡しをするとコピーが発生し、メモリと処理時間のオーバーヘッドが増大する。ポインタを使って参照渡しを行うことで、コピーを避け、効率的にデータを扱うことができる。

package main

import (
    "fmt"
)

// 大きな構造体の定義
type LargeStruct struct {
    Data [1_000_000]int
}

// 構造体をポインタで受け取る関数
func processStruct(ls *LargeStruct) {
    ls.Data[0] = 42
}

func main() {
    ls := LargeStruct{}
    processStruct(&ls)
    fmt.Println(ls.Data[0]) // 出力: 42
}

2. 関数内での値の変更

値渡しの場合、引数の変数に値が複製されるので関数内で変数を変更しても元の値には影響しない。ポインタを使うことで、関数内での変更を元の変数に反映させることができる。

package main

import (
    "fmt"
)

func increment(value *int) {
    *value += 1
}

func main() {
    x := 10
    increment(&x)
    fmt.Println(x) // 出力: 11
}

3. 可変なデータ構造の操作

リンクリストやツリーなどの動的なデータ構造を実装する際、ノード同士をポインタで結ぶことで柔軟な構造を構築できる。

package main

import (
    "fmt"
)

// ノードの定義
type Node struct {
    Value int
    Next  *Node
}

func main() {
    // ノードの作成
    node1 := &Node{Value: 1}
    node2 := &Node{Value: 2}
    node3 := &Node{Value: 3}

    // ノードをリンク
    node1.Next = node2
    node2.Next = node3

    // リストのトラバース
    current := node1
    for current != nil {
        fmt.Println(current.Value)
        current = current.Next
    }
}

4. nil値の活用

ポインタはnil値を取ることができるため、オブジェクトの存在確認やエラーハンドリングに利用できる。

package main

import (
    "fmt"
)

type User struct {
    Name string
    Age  int
}

func getUser(id int) *User {
    if id == 1 {
        return &User{Name: "Alice", Age: 30}
    }
    return nil
}

func main() {
    user := getUser(2)
    if user == nil {
        fmt.Println("ユーザーが見つかりません")
    } else {
        fmt.Println("ユーザー名:", user.Name)
    }
}

5. インターフェースとメソッドレシーバー

メソッドがポインタレシーバーを持つ場合、値型ではそのメソッドを呼び出せない。ポインタを使うことで、インターフェースの実装やメソッドの呼び出しが可能になる。

package main

import (
    "fmt"
)

type Shape interface {
    Area() float64
}

type Rectangle struct {
    Width  float64
    Height float64
}

// ポインタレシーバーを持つメソッド
func (r *Rectangle) Area() float64 {
    return r.Width * r.Height
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    var s Shape = &rect // ポインタ型である必要があります
    fmt.Println("面積:", s.Area())
}

6. 同期処理

sync.Mutexなどの同期プリミティブはポインタで扱う必要がある。値渡しするとコピーが作成され、ロック機構が正しく機能しなくなる。

package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu    sync.Mutex
    value int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func main() {
    counter := &SafeCounter{}
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            counter.Increment()
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("カウンターの値:", counter.value)
}

7. 再帰的なデータ型の定義

構造体が自分自身をフィールドとして持つ場合、直接の値としては定義できないが、ポインタを使うことで再帰的な定義が可能になる。

package main

import (
    "fmt"
)

type TreeNode struct {
    Value int
    Left  *TreeNode
    Right *TreeNode
}

func main() {
    root := &TreeNode{
        Value: 10,
        Left: &TreeNode{
            Value: 5,
        },
        Right: &TreeNode{
            Value: 15,
        },
    }

    fmt.Println("ルートノードの値:", root.Value)
    fmt.Println("左の子ノードの値:", root.Left.Value)
    fmt.Println("右の子ノードの値:", root.Right.Value)
}

まとめ

業務アプリケーションを開発していると、ポインタの使いどころは主に以下の項目に集中していることが改めてわかった。

  • 1. 大きなデータ構造の効率的な渡し方
  • 4. nil値の活用
  • 5. インターフェースとメソッドレシーバー
  • 6. 同期処理

一方で実務の開発では時々nilになりえないプリミティブな値をポインタにしていたりするので、注意して無駄のない安全な開発をできるように心がけようと思った。

Vueのリアクティブなデータについて調査した

調査対象

Vueでよく使う以下のデータ型のリアクティブ性について調査してみた。

  • Primitive
  • Object
  • PrimitiveRef
  • ObjectRef
  • PrimitiveShallowRef
  • ObjectShallowRef
  • Reactive
  • ShallowReactive
  • PrimitiveComputedRef
  • ObjectComputedRef
  • PrimitiveWritableComputedRef
  • ObjectWritableComputedRef
  • ObjectReadonly
  • ObjectShallowReadonly

調査結果

データ型 isRef isReactive
Primitive false false
Object false false
Ref true false
ObjectRef true false
ShallowRef true false
ObjectShallowRef true false
Reactive false true
ShallowReactive false true
ComputedRef true false
ObjectComputedRef true false
WritableComputedRef true false
ObjectWritableComputedRef true false
Readonly false false
ShallowReadonly false false
Props false true
Emits false false
Slots false false

ref()で作られたデータはRefかつReactiveなのかと思ったら、意外にもRefとReactiveが明確に分けられていることがわかった

調査に使ったもの

調査に使用したVueのバージョン

"vue": "3.4.29"

調査に使用したしたソースコード

<script setup lang="ts">
import {
  isRef,
  isReactive,
  ref,
  shallowRef,
  reactive,
  shallowReactive,
  computed,
  readonly,
  shallowReadonly,
} from 'vue';
import type { VNode } from 'vue';

// Primitive
const val = 0;
const isRefForVal = isRef(val);
const isReactiveForVal = isReactive(val);

// Object
const obj = { value: 0 };
const isRefForObj = isRef(obj);
const isReactiveForObj = isReactive(obj);

// PrimitiveRef
const refVal = ref(0);
const isRefForRefVal = isRef(refVal);
const isReactiveForRefVal = isReactive(refVal);

// ObjectRef
const objRefVal = ref({ value: 0 });
const isRefForObjRefVal = isRef(objRefVal);
const isReactiveForObjRefVal = isReactive(objRefVal);

// PrimitiveShallowRef
const shallowRefVal = shallowRef(0);
const isRefForShallowRefVal = isRef(shallowRefVal);
const isReactiveForShallowRefVal = isReactive(shallowRefVal);

// ObjectShallowRef
const shallowObjRefVal = shallowRef({ value: 0 });
const isRefForShallowObjRefVal = isRef(shallowObjRefVal);
const isReactiveForShallowObjRefVal = isReactive(shallowObjRefVal);

// Reactive
const reactiveVal = reactive({ value: 0 });
const isRefForReactiveVal = isRef(reactiveVal);
const isReactiveForReactiveVal = isReactive(reactiveVal);

// ShallowReactive
const shallowReactiveVal = shallowReactive({ value: 0 });
const isRefForShallowReactiveVal = isRef(shallowReactiveVal);
const isReactiveForShallowReactiveVal = isReactive(shallowReactiveVal);

// PrimitiveComputedRef
const computedVal = computed(() => 0);
const isRefForComputedVal = isRef(computedVal);
const isReactiveForComputedVal = isReactive(computedVal);

// ObjectComputedRef
const objComputedVal = computed(() => ({ value: 0 }));
const isRefForObjComputedVal = isRef(objComputedVal);
const isReactiveForObjComputedVal = isReactive(objComputedVal);

// PrimitiveWritableComputedRef
const writableComputedVal = computed({
  get: () => 0,
  set: (_) => {},
});
const isRefForWritableComputedVal = isRef(writableComputedVal);
const isReactiveForWritableComputedVal = isReactive(writableComputedVal);

// ObjectWritableComputedRef
const objWritableComputedVal = computed({
  get: () => ({ value: 0 }),
  set: (_) => {},
});
const isRefForObjWritableComputedVal = isRef(objWritableComputedVal);
const isReactiveForObjWritableComputedVal = isReactive(objWritableComputedVal);

// ObjectReadonly
const readonlyVal = readonly({ value: 0 });
const isRefForReadonlyVal = isRef(readonlyVal);
const isReactiveForReadonlyVal = isReactive(readonlyVal);

// ObjectShallowReadonly
const shallowReadonlyVal = shallowReadonly({ value: 0 });
const isRefForShallowReadonlyVal = isRef(shallowReadonlyVal);
const isReactiveForShallowReadonlyVal = isReactive(shallowReadonlyVal);

// Props
const props = defineProps<{
  msg: string;
}>();
const isRefForProps = isRef(props);
const isReactiveForProps = isReactive(props);

// Slots
const slots = defineSlots<{
  default: () => VNode[];
}>();
const isRefForSlots = isRef(slots);
const isReactiveForSlots = isReactive(slots);

// Emits
const emits = defineEmits<{
  (event: 'update', value: number): void;
}>();
const isRefForEmits = isRef(emits);
const isReactiveForEmits = isReactive(emits);
</script>

<template>
  <main>
    <table>
      <thead>
        <tr>
          <th>type</th>
          <th>isRef</th>
          <th>isReactive</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Primitive</td>
          <td>{{ isRefForVal }}</td>
          <td>{{ isReactiveForVal }}</td>
        </tr>
        <tr>
          <td>Object</td>
          <td>{{ isRefForObj }}</td>
          <td>{{ isReactiveForObj }}</td>
        </tr>

        <tr>
          <td>Ref</td>
          <td>{{ isRefForRefVal }}</td>
          <td>{{ isReactiveForRefVal }}</td>
        </tr>
        <tr>
          <td>ObjectRef</td>
          <td>{{ isRefForObjRefVal }}</td>
          <td>{{ isReactiveForObjRefVal }}</td>
        </tr>

        <tr>
          <td>ShallowRef</td>
          <td>{{ isRefForShallowRefVal }}</td>
          <td>{{ isReactiveForShallowRefVal }}</td>
        </tr>
        <tr>
          <td>ObjectShallowRef</td>
          <td>{{ isRefForShallowObjRefVal }}</td>
          <td>{{ isReactiveForShallowObjRefVal }}</td>
        </tr>

        <tr>
          <td>Reactive</td>
          <td>{{ isRefForReactiveVal }}</td>
          <td>{{ isReactiveForReactiveVal }}</td>
        </tr>
        <tr>
          <td>ShallowReactive</td>
          <td>{{ isRefForShallowReactiveVal }}</td>
          <td>{{ isReactiveForShallowReactiveVal }}</td>
        </tr>

        <tr>
          <td>ComputedRef</td>
          <td>{{ isRefForComputedVal }}</td>
          <td>{{ isReactiveForComputedVal }}</td>
        </tr>
        <tr>
          <td>ObjectComputedRef</td>
          <td>{{ isRefForObjComputedVal }}</td>
          <td>{{ isReactiveForObjComputedVal }}</td>
        </tr>

        <tr>
          <td>WritableComputedRef</td>
          <td>{{ isRefForWritableComputedVal }}</td>
          <td>{{ isReactiveForWritableComputedVal }}</td>
        </tr>
        <tr>
          <td>ObjectWritableComputedRef</td>
          <td>{{ isRefForObjWritableComputedVal }}</td>
          <td>{{ isReactiveForObjWritableComputedVal }}</td>
        </tr>

        <tr>
          <td>Readonly</td>
          <td>{{ isRefForReadonlyVal }}</td>
          <td>{{ isReactiveForReadonlyVal }}</td>
        </tr>
        <tr>
          <td>ShallowReadonly</td>
          <td>{{ isRefForShallowReadonlyVal }}</td>
          <td>{{ isReactiveForShallowReadonlyVal }}</td>
        </tr>

        <tr>
          <td>Props</td>
          <td>{{ isRefForProps }}</td>
          <td>{{ isReactiveForProps }}</td>
        </tr>
        <tr>
          <td>Emits</td>
          <td>{{ isRefForEmits }}</td>
          <td>{{ isReactiveForEmits }}</td>
        </tr>
        <tr>
          <td>Slots</td>
          <td>{{ isRefForSlots }}</td>
          <td>{{ isReactiveForSlots }}</td>
        </tr>
      </tbody>
    </table>
  </main>
</template>

調査した目的

普段の業務の中でwatchの第一引数にpropsのキーを指定したものを入れたときにエラーになったため自分の中で勘違いしていることがあると思ってよく使うデータ型のリアクティブ性について調べてみた。

const props = defineProps<{
  msg: string;
}>();

watch(props.msg, (val) => { //この呼び出しに一致するオーバーロードはありません。前回のオーバーロードにより、次のエラーが発生しました。ts(2769)
  console.log(val);
});