🙆‍♀️

react-gridsheetについて解説など

2021/02/10に公開

お久しぶりです。
前回の記事ではたくさんのいいね(?)ありがとうございました🐶
やっぱりzennはすごいですね。自分のブログに書いたら多分誰も見向きもしなかったと思いますw

https://zenn.dev/righ/articles/ab4033f6d7787a

反響があったら技術解説をするなどと大仰なことを口走ってしまったのでそろそろ書きます🌚
言い訳から入りますが、この記事はLTの発表資料として使うための急ごしらえで内容をあまり精査していないのでマサカリが飛んでくることを覚悟してます。Twitterだと気づかない可能性があるのでなにかあったら記事コメントでお願いします🙏

内容は前回のリリースからの変更点とこれからの展望についてです。この中でReduxについて少し触れます。

変更点

大きく分けて3つあります。

コンテキストメニューの追加

いままでは特殊操作はショートカットキーのみでしたが、右クリックでメニューを開けるようにしました。

行・列の追加および削除をサポート

コンテキストメニューから選択している行・列を追加および削除できます。いずれも Undo, Redo に対応しています。
追加・削除に伴って行・列のスタイルやラベルをずらすのがそこそこめんどくさかったですがなんとかできました🦹‍♀️

Renderer および Parser の指定方法を改善

以前のバージョンでは作成したクラスをそのまま options に指定するようにしていましたが、インスタンス化したものを渡すようにしました。
これはコンストラクタで条件や補完値を受け取れるようにした影響です。 0.2系からのBreaking change になっているので、もしご利用中の方がいらっしゃったら実装の修正をお願いします。

一応デモとして前回と違うExampleを貼っておきます。ライブラリをアップデートしたくらいで内容は殆ど変わっていませんが、コンテキストメニューの表示は確認できると思います。

(chromeだとiframe越しのセルのドラッグがきかないことがあるみたいです)

ちなみに codesandbox上のエディタで型補完が効かなくなっているんですが、codesandboxの不具合のような気がしているので issue を上げました。
https://github.com/codesandbox/codesandbox-client/issues/5452

これからの展望

機能的にはだんだん揃ってきたように見えますが課題はまだ盛り沢山です。正直言うと全体の完成度は2割位なんじゃないかという疑惑があります😇
いつ完成するんだろうねほんと

高速化

このライブラリはデータを全て表示するという愚直な仕組みなのでデータが大きくなると速度が大幅に劣化します😭
環境によりますが、現状では縦横の合計セル数が5000セルくらいまでならわりと軽快に動作し10000セルくらいから徐々に遅さを感じ始めることでしょう。

ひとまず意識低いことを言うと、初期実装では1000セルくらいでセルの移動で数秒ラグってしまうくらい遅かったのでこれでもだいぶ改善されていますw
データの持ち方と描画条件に原因があったわけですが、これらがReduxの導入によって解決しました👼

具体的には以下をやりました。

前者については一応Contextでも同じことはできますが、後者はReduxの利点です。(Context で useSelectorするためのライブラリも一応あるようです)
当ライブラリでは store の reactionsオブジェクトのキー(セルID)が true かどうかによって描画可否を判断しています。大半のセルは描画する必要がないのでこれが高速化に大きく貢献しました。
このやり方は更新対象を柔軟に指定できて便利なんですが、指定を忘れるとセルの描画が更新されないなど、バグりやすい実装となっていることは正直否めません🥺

しかし前述したようにこれは根本解決になっていません。
例えば10万セルあったとするとたった一箇所のデータの更新でも10万回の描画可否判断が走ってしまいます。たとえ描画しないとしてもそれだけの数の判断処理が動けば遅くなるのは当然のことです。

いろいろと考えた末に「見えている領域だけに描画を制限しよう」という結論に至りました。(シャワー浴びてるときにひらめきました🚿)
最初はそういうコンセプトで自分で実装しようと思っていたんですが、すでにあるようで以下のライブラリを見つけました。

https://github.com/bvaughn/react-window

https://github.com/Autodesk/react-base-table

この技術にはウィンドウィング(windowing)という名前がついてるようです。私は情弱だったので知りませんでしたが...😅
さきほどのLTで教えてもらったんですがどうやらゲームプログラミングの技術みたいです。

見た目的には後者の react-base-table のほうが現状に近いし高機能なわけですが、当ライブラリでは react-window を使って実装することになりそうです👌
これにはいくつか理由があるんですが、react-base-table が行指向っぽいというのが最大の理由です。
おそらくCSSを調整すればあまり変わらないようにできるんですが、行と列の操作が非対称になるというのは実装コストに影響しそうな気配を感じます。またライブラリがかなりリッチに実装されているためそちらの変更に引きづられるのを恐れています。
それと比較すると react-window は薄く必要十分なのでライブラリに組み込むのに適しているように思います。

すこしネガティブなことを言いましたが、いずれのライブラリもかなりクオリティが高いので覚えておいて絶対に損はありません🙆‍♂️

余談ですがreact-windowの作者は先程のExampleにも表示されているReactのコントリビューター上位の人です。すごい。

Reduxをやめる

さっきReduxで高速化したのに、やめるということなんですが理由は大きく2つあります🤘

  • ウィンドウィングの高速化だけで十分になった
  • Reduxでstoreが共有されていて複数のシートを描画できない

Reduxはもともと高速化のために導入したものですが、ウィンドウィングによる高速化をすると現在のRedux による高速化の施策はいらなくなります。

後者は単に私が仕様を把握していなかっただけなんですが、現状複数のコンポーネント間で状態が同期されてしまい実質一つしか描画できません。
これは完全にバグなのでReduxのDropとは別に対応することになるかもしれませんが、仮に直せたとしてもReduxを選ぶメリットが Redux DevTools が便利というだけになってしまうので最終的には削除することになりそうです🗑

Redux について少しだけ

せっかく解説ということもう少しだけReduxに触れておきます。
今回 Redux を利用するにあたって Redux Toolkit というラッパーを使いました。
これは Redux が公式に提供しているライブラリで Redux を扱いやすくしたツール群です。
私が利用しているのは主に createSlice というもので、これはaction および reducer を生成してくれる君です。自分でアクション名を管理しなくてよいというのは想像以上に便利でもうこれ以外の方法でReduxを書きたくないと思うほどでした💯

こんな感じに書きます。詳しくは上のリンクを参照。

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface CounterState {
  value: number
}

const initialState = { value: 0 } as CounterState

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.value++
    },
    decrement(state) {
      state.value--
    },
    incrementByAmount(state, action: PayloadAction<number>) {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

ちなみに Redux Devtools はChrome のアドオンですが store のデバッグに非常に有用でした。
使ってない方は是非ご確認ください👍

https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ja

データ構造の見直し

操作の非対称性について言及しましたが、これは現状のデータ構造である二重配列も同様の問題をはらんでいます。
行の操作は単に matrix.splice(...) のように操作すればいいんですが、列の操作は外側の配列をループして中の配列全てに対して splice をかけるようになり、実行コストが行と列で異なるのですね。

これを { A1: 値1, B2: 値2 } のようなフラットなオブジェクトの形式にしようと考えています。開発者都合ですが内部のオブジェクト操作がセルオプションと同じように扱えるため実装の単純化ができます。
メモリサイズがどのように変化するかは一応懸念点ではありますが、案外大丈夫かなとも思っています(要計測)

ただし、入出力は現状の形式(二重配列)でもできるように維持するつもりです。

ユニットテスト

前回そろそろ書くと言ったんですが、データ構造の大幅な見直しがあるのでその後になりそうです😿

数式のサポート

やるつもりはありますが、今の所優先度は一番低いです🤮
数式のサポートは構文解析の実装が必要というのもあるんですが、依存するセルの変更を検知して再描画などを考慮しないといけません。そうなると現状の再描画の判断を自分たちで管理するというのは実装・実行ともにコストが高く、かつバギーな実装になるのは目に見えています。
このような理由から高速化は完全にwindowingに任せたいというモチベーションに繋がっています。

ということでこれからも開発を続けていくので温かい目で見守ってください🙇‍♂️
https://github.com/walkframe/react-gridsheet

Discussion