generated at
Gyazzみたいなエディタ作りでReact.jsを学ぶ:hooksを使った書き方

クラスベースのコンポーネントはもう古い?
hooksで書くより回りくどいと感じたtakker
(クラスベース未経験の発言なので留意)
そういう実験ができる規模で始めてるのでいろいろ意見ください、書き直して比較してみますinajob
了解です!takker
ざっくりとですが、たとえば Editor.js ならこう書けそうですtakker
Editor-hook.js
// importこれであってるか自信ない import React, { useState, useEffect, useCallback, useMemo } from 'react'; import Line from './Line' const useLines = (initialLines) => { const [lines, setLines] = useState(initialLines); useEffect(() => setLines(initialLines), [initialLines]);
これなんだろう?(無くても動いた)inajob
useLines に渡されるlines( initialLines )そのものの変更に対応するために入れましたtakker
<Editor lines={lines} /> のlinesを変更するロジックを入れると変化がわかるはず
現状だとAppから渡されるlinesの中身が変わらないため、この行があってもなくても挙動がわからない
あー、なるほど・・inajob
まだデータのロードとかは頭が回ってないですね。きっとそこまで作ったらわかるのでしょう。
useEffectでstate更新について
これ、Lineコンポーネントに適切にkeyを指定すると要らなくなりますねmiyamonz
たぶん
アプリケーションのコード全体見てないので、どこで状態管理してるのかとか分からんので一概には言えないですが
Editor-hook.js
const modify = useCallback( (lineIdx, text) => setLines((prev) => { prev[lineIdx] = text; return [...prev]; }), [] ); return [lines, modify]; }; export const Editor = (props) => { const [cursor, setCursor] = useState({ row: 0, col: 0, }); const [lines, modify] = useLines(props.lines); const lineProps = useMemo( () => lines.map((line, index) => ({ value: line, handleChange: (e) => modify(index, e.target.value), onKeyDown: (e) => setCursor((prev) => { switch(e.key) { case "ArrowUp": return { row: prev.row - 1, col: prev.col }; case "ArrowDown": return { row: prev.row + 1, col: prev.col }; default: // 同じobjectを返せば再レンダリングされない return prev; } }), })), [lines, modify] );   return (<div>     {lineProps.map((props, index) => (      <Line      key={index}      isFocus={index === cursor.row}        {...props}       />     ))}   </div>); };
逆にめんどくさくなっているような……
useLines() に一部切り出したけど、中途半端かも
この規模だとあんまりうれしくない
event listenerまでまとめて配列にして生成したのは、再レンダリングを防ぐため
arrow functionの形で渡すと、毎回新しいobjectを生成することになるため、何度もevent listenerの登録解除が繰り返されてしまう
小規模のコードなら、あまりきにしなくてもいいっちゃいい
とてもありがたい!自分ではこれまだ書けないですね・・inajob
そして逆に面倒くさくなっている気持ちは確かに・・・
これ自分が設計ミスっていると思うんですよね。ただより適切な設計がどういうものか自分にもわからないです……takker
ScrapBubbleの複雑さと同じ匂いを感じる
クラスコンポーネントでは、設計に悩む感じがなかったので、その点だけ見るとクラスベースの方が良さそうだな・・inajob
それゆえに?見やすいというのも大事
元の設計ではevent listenerの差し替えによる再レンダリングとか考えてなかったけどどう動くんだろう?
動作内容自体は同じのはずですtakker
immer使ったらもう少しきれいになりそうtakker
ざっと書くつもりが、だいぶ大事になってしまった
行編集と文字編集とがばらばらになっているのがめんどく感じた
前者はReactで管理しているが、後者は<textarea />が管理している
とはいえ、文字編集までReactで管理させると、Scrapboxのように全てを自前で実装しなければならなくなるので、かなりハードルが高くなる
そうなんですよ、日本語入力とか考えるとこのくらいの役割分担の方が結果楽になりそうだなと思っていますinajob
一応先行例Zatsu wikiはあるので、このコードを参考に後々実装してみるのもありかもtakker
ひとまずこの実装は汚いけど一度書いているので、まず知見のあるこっちでやってみます。
<Line /> を例にあげたほうが適切でした。こっちはかなりすっきりしますtakker
Line-hook.js
import React, { useRef, useEffect } from 'react'; export const Line = (props) => { const ref = useRef(); useEffect(() =>{ if (props.isFocus) { ref.current?.focus?.(); } } ,[props.isFocus]);   return (     <textarea        ref={ref}        value={props.value}        onChange={props.onChange}        onKeyDown={props.onKeyDown}     /> ); };
お、まだ理解してないけどソースはすっきりした気がしますinajob
refでDOMにアクセスしている点は同じですねtakker
focus() の実行タイミングを変えています
Componentの再レンダリング時( componentWillUpdate )とunmount時( componentWillUnmount )から
props.isFocus の変更時( useEffect(() => {...}, [props.isFocus]) )に変えた
useEffect()は、第二引数の配列に指定した変数の変更をトリガーに実行されるevent listenerのようなイメージ
わかりやすい説明!inajob
処理が一カ所にまとまった点がポイント
タイミング、これで問題ないかな、、後で実際に試してみますinajob
DOM構築後にfocusしてほしい
確認した。動いた。
まさにこの例だ。クラスベースの書き方とフックを使った書き方が併記されている
僕は関数コンポーネントだけでKozanebaなどを作ってます。Reactを始めたタイミングでもう「クラスコンポーネントもあるけど関数コンポーネントに移行していくよ」という流れだったので〜nishio
使い分けるものではなくて、統一した方が良いんですかね?(上のEditor.jsの例はクラスベースの方が良い気がしてる・・)inajob
移行という表現だし今から書くならクラスベースじゃない方が良さそうだな・・
「使い分ける」のためには両方を知って「どちらが向いてるか」を判断できるようになる必要があるが、僕は面倒だったので「移行していくって言ってるんだったら関数コンポーネントだけでいいや」と学習をサボったnishio
ドキュメントのサンプルがクラスコンポーネントの書き方でだけ書かれてるとかで少し困った
普段仕事でもReactで書いてますが、関数コンポーネントとhooksでしか書いてませんmiyamonz
hooksの使い方を理解すれば、特に問題なく使えるかと思われます。
→hookで作り直してみることにする

今わかっているおかしなところ
lintがエラーを出している
pre
src\components\Editor.js Line 16:5: Do not mutate state directly. Use setState() react/no-direct-mutation-state Line 24:6: Do not mutate state directly. Use setState() react/no-direct-mutation-state Line 28:6: Do not mutate state directly. Use setState() react/no-direct-mutation-state
setStateするStateを作る時に既存のStateを上書きしているのがダメ
わかっていたけど、これでも動くっしょと思って取り敢えず実装している
全部コピーすればよいのかな?
こんな感じに、前のobjectを破棄して作り直せばいいと思うtakker
immutable.js
this.setState({ cursor: { row: this.state.cursor.row + 1, col: this.state.cursor.col, } lines: this.state.lines })
なんか大がかりになってしまった
stateを cursor lines とで別々に持たせればすっきりしそう
あ、もしかしてクラスベースだと、 this.state にすべての状態を詰め込まないといけないのか?
linesは別のオブジェクトにしないといけない? e.g. [...this.state.lines] inajob
オブジェクトがいくつも入れ子になってる時の非破壊的更新に関しては、手で書いて複雑だなーと思い出したタイミングでimmerを使うようになりましたnishio
immer!みてみるinajob
取り敢えずまだ不要そう
名前だけ聞いたことあったけど、非破壊更新を便利にやるやつだったのかtakker
immer自体はReactと関係ないようだ
React hooks版
ちょっとwrapした程度の簡単なコード
このくらいなら楽にPreact hooksとも組み合わせられそうだ
→ hookで書き直した結果この部分はすっきりした
テキストエリアにフォーカスを与えているところ
こうするしかないのかな?
たぶんそれしかないです。DOM操作はref経由でやるしかないtakker
→ このまま(useEffectに逃がしたが、本質は同じ)
マウスでフォーカス当てた後、入力すると(前回当たってた方に)戻されるsta

hookで書き直してみる
ひとまずuseMemoとかは無しで愚直にやってみるinajob
Editor.js
import React, { useState, useEffect } from 'react'; import Line from './Line'; export const Editor = (props) => { const [cursor, setCursor] = useState({ row: 0, col: 0, }); const [lines, setLines] = useState(props.lines); return ( <div> {lines.map((line, index) => ( <Line key={index} isFocus={index === cursor.row} value={line} onChange={(e) => ((i) => { setLines((prev) => { prev[i] = e.target.value; return [...prev]; }) })(index)} onKeyDown={(e) => setCursor((prev) => { switch(e.key) { case "ArrowUp": return { row: prev.row - 1, col: prev.col }; case "ArrowDown": return { row: prev.row + 1, col: prev.col }; default: // 同じobjectを返せば再レンダリングされない return prev; } })} /> ))} </div>); }; export default Editor

最適化を除くとクラスベースなものと比べて読みにくいということはないかな
メモ化とかを頑張ったので可読性が下がったという理解
早すぎる最適化的な問題かな?
単に理解が追い付いていないだけかも
linePropsもあえてJSXの中に展開して書く事でひとまず短いうちは読みやすい書き方にした
Line.jsは上でtakkerさんが示してくれたままのもの
いくつかこれには無駄がある
Editor.js
匿名関数がレンダリングのたびに生成・設定される
> event listenerまでまとめて配列にして生成したのは、再レンダリングを防ぐため
> arrow functionの形で渡すと、毎回新しいobjectを生成することになるため、何度もevent listenerの登録解除が繰り返されてしまう
気にするのは、画面がちらついて困るようになってからでもいいかもtakker
Line.js
特に無さそう?
ひとまず最適化の話は考えずエディタとしての機能を足していこう
その流れでまたReact的な側面で詰まるだろうからそこで学びを得たい