docs はこれを明示的に述べています:
宣言的に行うことができるものには、参照を使用しないでください。
たとえば、Dialogコンポーネントでopen()メソッドとclose()メソッドを公開する代わりに、isOpenプロパティをそれに渡します。
私はアイデアを手に入れ、再利用可能なコンポーネントについてほぼ同意します。コンポーネント内に隠された状態(参照を介して操作する必要がある)がないと、推論と再利用が容易になります。これがアプリケーションの具体的なダイアログにも適用できるかどうか疑問に思っています。もしそうなら、適切なツールを使用するための理解が欠けているようです。
たとえば、私のアプリケーションでは、ファイルを作成、名前変更、および削除するためのオプションを含むメニューを表示するファイルツリーがあります。各オプションには適切なダイアログが表示され、これらのダイアログは独自のコンポーネントにカプセル化されます。コンポーネント階層の最下部には、再利用可能なDialog
コンポーネントがあります。
FileTree
FileMenu
CreateFileDialog
Dialog
RenameFileDialog
Dialog
DeleteFileDialog
Dialog
ここで2つのアプローチを見ることができます。
FileTree
は、3つのダイアログすべての可視状態を管理します。つまり、参照はありません。私の具体的なケースでは、Dialog
はサードパーティのステートレスコンポーネントですが、2番目のアプローチにステートフルラッパーを追加することもできます。そして私には、それははるかに優れているようです。「正しい」コンポーネントにカプセル化された状態処理コードのインスタンスは1つしかありません。 FileTree
に可視状態の3つのインスタンスがあることは、これらの状態(createFileDialogVisible
などの長い名前またはcreateFileDialog.visible
などの名前空間オブジェクト)を区別する方法が必要であることを意味します。
(実際には、新しいファイルの名前など、管理する追加の状態がありますが、それはタイトルが要求する範囲の外です。それは、すべてのダイアログ状態をFileTree
。)
オプション2の方が明らかに優れていると思うので、ガイドラインが最初のものを奨励することを目的としているとは思えません。別のアプローチがないのですか、それともこのガイドラインの範囲外にあるものですか?
私の推論における、私が間違っていることの良い候補であると考えるステップ:
次のコードサンプルは、ステートレスダイアログ(Dlg
)、ステートフルラッパー(Dialog
)、およびそのステートフルダイアログのConsumer
を示しています。アプローチ1では、FileTree
はDialog
に対応しますが、可視状態が複数存在する場合は例外です。アプローチ2では、FileTree
はConsumer
に対応し、ここでも複数の参照があります。
import * as React from 'react';
function Dlg({ visible, onClose }) {
return visible ? (
<div>
dialog is shown
<button onClick={onClose}>close</button>
</div>
) : (
<dvi>dialog is hidden</dvi>
);
}
function Dialog(props, ref) {
const [visible, setVisible] = React.useState<boolean>(false);
const show = () => setVisible(true);
const hide = () => setVisible(false);
React.useImperativeHandle(ref, () => ({ show, hide }));
return <Dlg visible={visible} onClose={hide} />;
}
Dialog = React.forwardRef(Dialog);
function Consumer() {
const dialogRef = React.useRef(null);
const showDialog = () => {
// eslint-disable-next-line no-throw-literal
if (dialogRef.current === null) throw 'ref is null';
dialogRef.current.show();
};
return (
<div>
<div>
<button onClick={showDialog}>open</button>
</div>
<Dialog ref={dialogRef} />
</div>
);
}
間違っていることが判明したのは、私の推論の2番目のステップです。
親コンポーネントで状態を管理すると、記述されたコードの重複/名前空間の問題が避けられなくなります
すでにフックを使用している場合、これを独自のフックに抽出して、この方法でAPIを提供するのは簡単です。 refsと比較した場合の追加の利点は、最初のレンダリングの前にフックがnullではないため、型チェッカーを満足させるためにnullチェックを行う必要がないことです。実現すべきことは次のとおりです:Dialog
コンポーネントは純粋な動作であり、Dlg
がすでに行ったこと以外にレンダリングを追加しませんでした。動作はフックで抽象化するのが最適です。
このアプローチは、たとえば material-ui-popup-state で使用されます。この質問の例に適用されます:
_import * as React from 'react';
function Dlg({ visible, onClose }) { /* unchanged */ }
function useDialog() {
const [visible, setVisible] = React.useState(false);
const show = () => setVisible(true);
const hide = () => setVisible(false);
const bindDlg = () => ({
visible,
onClose: hide,
});
return { show, hide, bindDlg };
}
function Consumer() {
const dialog = useDialog();
return (
<div>
<div>
<button onClick={dialog.show}>open</button>
</div>
<Dlg {...dialog.bindDlg()} />
</div>
);
}
_
_material-ui-popup-state
_は、<Menu {...bindMenu(popupState)}>
の代わりに<Menu {...popupState.bindMenu()}>
を作成するという点で、少し異なります。そのようにする理由は2つあります。
bindMenu
は通常の関数として宣言できます。useCallback
を介して関数をメモするかどうかは問題ありません。bindMenu
の他にbindPopover
などもあります。フックの一部にならないようにすることで、フック自体が、プロップを接続するコンポーネントにとらわれないままになります。複数のダイアログを同時に開くことができるかどうかは明確ではありませんでした。
可能な場合は、独自の可視状態を処理できます。
彼らはできないので、実際にはFileTreeで小さな状態マシンを使用するか、またはReact Contextに保持してドリルを防止します。
このようにして、false、「作成」、「名前変更」、「削除」に設定できる一般的な「setOpenDiog」状態があります。現在の選択に応じて正しいものを表示します。