kft12
@kft12 (kft 12)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

TauriでappWindow.listenを使いたいのですが、stateが更新されたタイミングで重複登録されてしまう

解決したいこと

TauriでappWindow.listenを登録したいのですが、stateが更新されたタイミングで再レンダリングされるため、イベントが複数登録されてしまいます。

やっていること

テキストエディタアプリを作成しています。
コンポーネントがこちらのような階層になっています。

main.tsx
 └ app.tsx ← ここでstateを登録している
    ├ textEditor.tsx ← テキストエディタを表示
    └ tab.tsx ← 開いているファイルの一覧を表示

やりたいこととしては、テキストエディタにて値を編集したタイミングでstateを更新し、タブのファイル名表示に「*」をつけたいので、下記のようなコードを組んでいます。

app.tsx
export function App() {
    const defaultValue = '';
    const [inputValue, setInputValue] = useState('');

    return (
        <React.StrictMode>
            <Tab
                defaultValue={defaultValue}
                inputValue={inputValue}
            />
            <TextEditor
                value={defaultValue}
                setInputValue={setInputValue}
            />
        </React.StrictMode>
    );
}

他にも色々細かい部分が違ってはいるのですが、割愛しています。
このような感じで少なくとも問題なく動いていると思っていただけたら・・。

ただ、Tauriでアプリを作っているので、アプリケーションメニューバーを実装しOpenを押すと実際に開けるようにしました。

app.tsx
export function App() {
    const [inputValue, setInputValue] = useState('');

    appWindow.listen('open', () => {
        const filePath = await dialog.open(dialogOption);
        const text = await fs.readTextFile(filePath);
        setInputValue(readFileText);
    });

    return (
        <React.StrictMode>
            <Tab
                inputValue={inputValue}
            />
            <TextEditor
                setInputValue={setInputValue}
            />
        </React.StrictMode>
    );
}

ここからが問題でして、inputValueが更新される度に再レンダリングされるため?なのかopenを押したときの処理がどんどん重なっていきます。
1回だけopenを押したのに、再レンダリングが重なった分だけダイアログが表示されてしまいます・・。
再レンダリングはfunction App()を再度実行しているような形なのでしょうか?

また、これを回避するためにはどうしたら良いのでしょうか・・?

自分で試したこと

useStateより上の階層でappWindow.listenをしようとしたものの、setStateを呼び出したかったのでうまく出来ず・・。
useStateを使わずに子コンポーネントを任意のタイミングで再レンダリングする方法もないか調べてみたのですが、イマイチわかりませんでした。

0

1Answer

以下のように、 コンポーネント直下に式を書いた場合、その式は、コンポーネントが再レンダリングされるたびに実行されてしまうので、ここに listen する式を書くのは不適切です。

export function App() {
    const [inputValue, setInputValue] = useState('');

    appWindow.listen('open', () => {
        const filePath = await dialog.open(dialogOption);
        const text = await fs.readTextFile(filePath);
        setInputValue(readFileText);
    });
// 略
}

コンポーネントの再描画について認識が怪しいなら、React の公式ドキュメントも参考になると思います。


以下の記事にあるように、 useEffect の中で listen を呼び出すべきです。

  // 記事から引用
  useEffect(() => {
    let unlisten: any;
    async function f() {
      unlisten = await listen('back-to-front', event => {
        console.log(`back-to-front ${event.payload} ${new Date()}`)
      });
    }
    f();

    return () => {
      if (unlisten) {
        unlisten();
      }
    }
  }, [])
  • listen 呼び出しは副作用なので、レンダリング中に呼び出すべきでない
  • しかも、「リスナーを登録したあと、役目が終わったらリスナーを解除すべき(クリーンアップ)」という類の機能

という理由があるためです。エフェクトのクリーンアップについては、以下の記事が参考になると思います。

1Like

Comments

  1. @kft12

    Questioner

    ありがとうございます!
    こちらで期待通りの動きを確認できました。
    わかりやすいご回答助かりました!

Your answer might help someone💌