LoginSignup
3
0

More than 1 year has passed since last update.

複雑化したSvelteのStoreにimmerを使って立ち向かう

Last updated at Posted at 2021-05-02

はじめに

Svelte で複雑な Store の一部分のデータを更新したときに、初期データが変わってしまう問題に遭遇しました。それを解決するために immer を使う方法を試したので、備忘録として残しておきます。

TL;DR

import { produce } from 'immer'

// 中略
update(
  produce((draft) => {
    draft.posts[postIndex].comment.push(comment)
  })
)

環境

  • svelte 3.38.1
  • immer 9.0.2

モチベーション

以下のような複雑なストアがあるとします。

store.js
type User = { name: string posts: Post[] }
type Post = { title: string body: string comments: Comment[] }
type Comment = { body: string }

import { writable } from 'svelte/store'

const initialData: User = {
  name: 'name1',
  posts: []
}
const { subscribe, update } = writable(initialData)
export userStore = {
  subscribe,
  addComment(postIndex: number) {
    // TODO: コメントを追加する処理
  }
}

カスタムストアというテクニックを用いて、自由すぎる変更が行えないようにします(カスタムストアのドキュメント)。
さて、コメントを追加する処理をどうやって書きましょうか。

直接変更してしまう例
// 中略
addComment(postIndex: number, comment: Comment) {
  update(prevUser => {
    prevUser.posts[postIndex].comment.push(comment)
    return prevUser
  })
  console.log(initialData) // store を更新後に、初期データを表示してみる
}

log を見てみると、initialData が変わってしまっていることがわかります。
これはいわうゆる参照渡しのような挙動が起きているからです(「参照渡しが起きている」とは言っていません!)
(参考:https://qiita.com/yuta0801/items/f8690a6e129c594de5fb)
もし、initialData に reset する必要があるとしたら大変です。
これを防ぐには、破壊的変更を加えないで、新しいデータを return する必要があります。

イミュータブルに書く例
addComment(postIndex: number, comment: Comment) {
  update((prevUser) => ({
    ...prevUser,
    posts: prevUser.posts.map((post) => ({
      ...post,
      comments: [...post.comments, comment],
    }))
  }))
  console.log(initialData); // 今度は、変更されない
}

今度は変更されませんが、...の連続で書きづらいし読みづらいです。

immer を使う

immer はこうした複雑なデータを破壊的変更を加えずに、簡単に更新するためのライブラリです。
https://bundlephobia.com/result?p=immer@9.0.2

gzip 状態で 5kB ほどと軽めのライブラリです(Svelteのランタイムが軽すぎるせいで、これでも重く感じる人もいるかも)。
他の界隈を覗くと、React でも 「Redux で複雑なデータの reducer を書くのが辛いので immer を使う」ということが多々あるようです。
immer を使って先ほどのaddCommentを書いてみます。

addComment(postIndex: number, comment: Comment) {
  update(prevUser => (
    produce(prevUser, draft => {
      draft.posts[postIndex].comment.push(comment)
    })
  ))
  console.log(initialData) // 変更されない
}

なんだかネストが深くて読みづらいです。
実は、以下のように、produce メソッドの第一引数を省略して、関数を直接渡すことで以下のようにスッキリ書くことができます。
(参考)

addComment(postIndex: number, comment: Comment) {
  update(produce(draft => { // 関数を直接渡す
    draft.posts[postIndex].comment.push(comment)
  }))
  console.log(initialData) // 変更されない
}

検証しきれていないこと

  • deep clone をするという手もあるが、immer と比べてどちらがパフォーマンスが良いのか
3
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0