はじめに
◯ 背景
Vuex でデッカいオブジェクト、言いかえれば、深くネストしたオブジェクトを扱いたいと思いました。
◯ 問題
この時に問題になるのが、深くネストしたオブジェクトを mutate をすることが、とても煩雑になることです。
◯ 解決策 1 - normalizr
そのようなケースでは木構造を一旦、データベースのテーブルのような形に変形して対処するようです。この変形を normalize, 正規化と表現されているのを目にします。
表示するときは木構造で扱いたいのにテーブルにされたら困る、とも思ったのですが。結局、テーブルの形式から再度、木構造に直せば良いだけかなと思いました。
ただ、たしかにテーブルの構造から木構造に戻すことはできるのですが、getters, rootGetters などを使い補完も効かないため、まどろっこしい書き方になるため、なかなか辛い作業でした。かなり簡単な処理であるにも関わらず。
特に自分の場合、木が再帰的な構造をしているような、若干複雑な構造になっているため、コードが getters 祭りになってしまいました...orz map, filter も、とても使いにくいです。
◯ 解決策 2 - Vuex ORM
こういったデータベース的な使い方をする場合、以下の方が言われているように Ruby, PHP などで提供されている ORM フレームワーク的な API にした方が、綺麗なるのかなと思いました。
今回は Vuex の経験そのものが浅く理解できなかったので使えなかったのですが、次回以降ぜひ使わせていただきたいです。
normalizr の操作
以下「愚直」に normalize して七転八倒した際の備忘録です。サンプルコードを示します。
Step 1.
normalizr というライブラリを使います。
yarn add normalizr
Step 2.
index.js
に以下の内容をコピペします。あるいはインタープリタ >
にそのままコピペで実行できます。基本的な考え方は 子へのプロパティ名だけを指定する と言うことです。子とのリレーションを定義するだけで自動的にくみあげてくれるなんて、本当に有難いライブラリです。
const { normalize, schema } = require('normalizr');
const util = require('util')
const originalData = [
{
title: 'オブジェクト',
type: 'category',
contents: [
{
title: '名前空間',
type: 'article',
path: '/namespace/',
}
],
},
{
title: '高速化したい',
type: 'category',
contents: [
{
title: '二分探索木',
type: 'article',
path: '/initial/'
}
]
}
]
const article = new schema.Entity('articles', {}, {
idAttribute: 'path'
});
const category = new schema.Entity('categories', {}, {
idAttribute: 'title'
});
const content = new schema.Union({ article, category }, 'type')
const contents = new schema.Array(content)
category.define({ contents })
const normalizedData = normalize(originalData, new schema.Array(content));
console.log(util.inspect(normalizedData, {showHidden: false, depth: null}))
idAttribute
主キーとなるプロパティを指定できます。デフォルトでは id
となります。
noarmalize 関数
第一引数は、解析するデータ。第二引数は、解析するデータの根のスキーマを指定します。
schema.Union,
2つ種類はいる可能性のあるスキーマを定義します。以下の例では article
または content
となります。第二引数には、データがどちらのスキーマに分類されているかを確認するために参照するべきプロパティ名を指定します。
const content = new schema.Union({ article, category }, 'type')
schema.Array
配列です。
schema.Entity
データベースのテーブルに相当するものを定義します。第一引数には、テーブルの名前を書きます。
第二引数には、他のスキーマへのプロパティのみを書きます。スキーマに該当しないプロパティは、何も書きません。例えば article
は第二引数が空のオブジェクト {}
です。
しかし、ここでは category
も空のオブジェクト {}
です。これは循環参照をしているため、後から contents
を付け足しています。付け足す際には define
を使います。
category.define({ contents })
Step 3.
実行します。
node index.js
$ node index.js
{
entities: {
articles: {
'/namespace/': { title: '名前空間', type: 'article', path: '/namespace/' },
'/initial/': { title: '二分探索木', type: 'article', path: '/initial/' }
},
categories: {
'オブジェクト': {
title: 'オブジェクト',
type: 'category',
contents: [ { id: '/namespace/', schema: 'article' } ]
},
'高速化したい': {
title: '高速化したい',
type: 'category',
contents: [ { id: '/initial/', schema: 'article' } ]
}
}
},
result: [
{ id: 'オブジェクト', schema: 'category' },
{ id: '高速化したい', schema: 'category' }
]
}
$
entities
の配下には、 articles
, categories
というテーブルが出力されます。レコードは、主キーを持っていて O(1) で探索できるようになっています。
result
の配下には、元の木構造の根が出力されます。これを使って、木構造を復元するような形になるのでしょうか。
Vuex での利用
Object.assign
でまとめて state に叩き込むと便利な気がします。1 つのテーブルに対して 1 つの store を割り当てて、 store を class のような使い方をすると、良いような気がしました、個人的に。
export default {
namespaced: true,
mutations: {
init (state, categories) {
Object.assign(state, categories)
},
}
}
◯ ハマったこと
Vuex にしてもプロパティを追加する際は Vue.set
使わないとリアクティブにならないので注意してください。自分は、ハマってしまいました笑
// これは NG
// Object.assign(state, { property: value })
// これは OK
Vue.set(state, 'property', value)
参考にさせていただいた資料
◯ 問題の確認
◯ normalizr の使い方
◯ 気になったこと - React
normalizr は React の Redux でよく使われているそうです。そして、いま Redux は Hooks に置き換えられつつあります。normalizr が Hooks で使われているのを見かけず、使って良いのか、若干、疑問があります。
Vue の composition API について考えつつ、代替案があるのか、考えたのですが思いつかず、そのまま normalizr を使用しました。
React, nested state で検索して見たのですが、ヒットするもののいまいちわからず。
I'm working with a deeply nested state object in React...
I think you should be using the functional form of setState, so you can have access to the current state and update it...
でも結局、やっぱり使ってもいいみたい。ics.media の方が言われてるなら、大丈夫かな。
Hooksの登場で責任分担をある程度分散できるようにはなりました。しかし、ある程度の規模以上のアプリケーションになった場合は、Reduxの採用を視野に入れた方がいいでしょう。 逆にシンプルなWebアプリケーションの場合、Reduxを採用するのはオーバースペックです。