Jotaiによる再構成可能な再利用可能なコンポーネント


更新3/12/22:状態の支柱を渡す代わりに状態を保持するJOTAIプロバイダーを使用します.GitHub Commit.
強いcodebaseの一部は、環境の中で仕事を強化するために造られたツールです、そして、再利用できるコンポーネントは重要な役割を果たします.よく設計された共通のコードは、楽しい開発と大規模な頭痛の違いをすることができます、私は常に考えて新しい方法を見つけるしようとしている何かです.私は最近、国家管理のためにJOTAIと協力し始めました、そして、それは再利用可能な反応成分の構成のために若干のおもしろいパターンにつながりました.あなたがJotaiに慣れていないならば、あなたはそうすることができますcheck it out here , または読んでくださいそれはあまり複雑ではない!
この例では、項目のリストをレンダリングする汎用ピッカーコンポーネントを使用しており、ユーザーがいくつかを選択することができます.
この記事を通して、若干の型定義とスタイルは簡潔さのために省略されました.visit the GitHub repository すべてのコードを見るには
ここでは、基本的な機能を実装するごくまれな例を示します.
function Picker({ options }: PickerProps) {
  const [selectedItems, setSelectedItems] = useState<Record<string, boolean>>({});

  const handleClick = (name: string) => {
    return () => setSelectedItems(prev => ({ ...prev, [name]: !prev[name] }))
  }

  return (
    <div>
      {options.map(o => (
        <div key={o.name} onClick={handleClick(o.name)}>
          <p key={o.name}>{o.name}</p>
          <input type={'checkbox'} checked={selectedItems[o.name]} onChange={handleClick(o.name)}/>
        </div>
      ))}
    </div>
  );
}
このコンポーネントは良いです.追加の機能を必要とするユースケースを実行するまで.たとえば、検索バー!検索機能を追加する最も簡単な方法はenableSearch 後方互換性のための支柱とコンポーネントの範囲内でロジックをろ過すること.
function Picker({ options, enableSearch }: PickerProps) {
  const [selectedItems, setSelectedItems] = useState<Record<string, boolean>>({});
  const [search, setSearch] = useState("");

  const handleClick = (name: string) => {
    return () => setSelectedItems(prev => ({ ...prev, [name]: !prev[name] }))
  }

  return (
    <div>
      {enableSearch && (
        <input value={search} onChange={e => setSearch(e.target.value)} />
      )}
      {options
        .filter(o => o.name.includes(search))
        .map(o => (
          <div key={o.name} onClick={handleClick(o.name)}>
            <p key={o.name}>{o.name}</p>
            <input type={'checkbox'} checked={selectedItems[o.name]} onChange={handleClick(o.name)} />
          </div>
        ))}
    </div>
  );
}
明らかに、コンポーネントはまだ軽量で読みやすいですが、この記事のために、スケーラビリティを改善し始めましょう.機能を追加し続けるならPicker 検索フィルタリングを追加する方法では、コンポーネントはますます複雑に時間をかけて成長します.我々が加えるより多くの小道具と機能、より高いチャンスは、衝突している論理がある、あるいは、構成要素が単にあまりに簡単に維持するにはあまりに大きくなるということです.ここでの本当の問題は、我々が一緒に構成されることができるより小さい部分を建設する代わりに機能性で連続的にそれを満たすことによって、内部にコンポーネントを構築しているということです.

組成


JOTAIからいくつかの助けを借りて、我々は構成可能な再利用可能なロジックを作ることができますちょうど反応神が意図したように.まず、部品を論理単位に分解しましょう.
  • 状態コンテナPicker ): 内部状態を所有します.
  • リストレンダラList ): 状態から読み込み、項目をレンダリングします.
  • 検索入力Search ): ユーザー入力に応じて状態を変更します.
  • リストアイテムListItem ): ユーザーが対話するときに項目をレンダリングし、状態を変更します.
  • このような方法で破壊すると、いくつかの追加オーバーヘッドが生成されますが、コンポーネントが複雑になるにつれてコードの清潔度が大幅に向上します.以下に、その組成がどのように見えますか
    <Picker options={items}>
      <Search />
      <List />
    </Picker>
    
    これによってProvider 状態コンテナー内の状態を維持しながら、コンポーネントをより小さく構成するためのコンポーネント.状態は、それが大幅に渡される必要がある小道具の量を減らすように、読みやすさの面で大きな意味を持つフックによってアクセスされます.我々は、コンポーネントを通過することについて心配することなくより小さな構成要素にコンポーネントを壊すのが自由です、そして、州を扱うどんなロジックも現在サブコンポーネントの中に含まれることができます.サブコンポーネントに直接影響するロジックの小道具を準備することができます.例えば、私たちは、より多くのオプションをSearch コンポーネント
    ...
    
      <Search caseSensitive debounceMs={500} />
    
    ...
    
    
    これをする方法は以前に小道具を追加し続けることでしたPicker コンポーネントと内部コンポーネントに渡すことは、本質的にスケーラブルなソリューションではありません.

    内部状態


    次に、内部状態を見てみましょう.

    州コンテナ


    function Picker({ options, children }: PickerProps) {
      const setOptions = useUpdateAtom(pickerState.optionsAtom, pickerScope);
    
      useEffect(() => {
        setOptions(options);
      }, [options, setOptions]);
    
      return (
        <div>
          {children}
        </div>
      );
    }
    
    export default function provider(props: PickerProps) {
      return (
        <Provider scope={pickerScope}>
          <Picker {...props} />
        </Provider>
      )
    }
    
    ここで注意する重要なことは、Jotaiの使用ですProvider ラッピングPicker そして、useUpdateAtom フック.両方ともscope これはProvider すべての状態をキャプチャし、グローバルにアクセスできるようにしません.さらに、スコープのすべての子供Provider この状態でコンポーネントを構成することができるコア機構である同じ状態にアクセスできます.このセットアップのもう一つの利点はPicker アンマウント、その内部状態が自動的に破壊されます.
    また、状態オブジェクトの形状も見てみる価値があります.
    type PickerState = {
      optionsAtom: WritableAtom<Option[], Option[]>;
      hiddenAtom: WritableAtom<Record<string, boolean>, Record<string, boolean>>;
      selectedAtom: WritableAtom<Record<string, boolean>, Record<string, boolean>>;
    }
    
    hiddenAtom 現在隠されている項目のマップを保持します.selectedAtom 選択した項目のマップを保持し、optionsAtom もともと渡されたアイテムのリストを保持するPicker . マップアトムからの値は、各リスト項目のプロパティを設定してリストにマージされます.
    type Option = {
      name: string;
      hidden?: boolean;
      selected?: boolean;
    }
    
    場合は、マージとどのようにマージの作品を見たい、見てくださいinitializeState.ts and combinedUpdatesAtom.ts .

    リストレンダラ


    このコンポーネントは、リストのレンダリングに関連するロジックのみを実装します.清潔!
    function List() {
      const options = useAtomValue(pickerState.optionsAtom, pickerScope);
    
      return (
        <div>
          {options.map(o => <ListItem key={o.name} option={o} />)}
        </div>
      )
    }
    

    検索入力


    検索入力は、アイテムのリストをフィルタリングするために必要なすべてのロジックを入力します.この場合、結果を現在の項目のリストと比較する前に、検索文字列を含む項目をチェックします.それがどんな違いを発見するならば、それは更新によってRerenderを引き起こしますhiddenAtom .
    function Search() {
      const [search, setSearch] = useState("");
      const options = useAtomValue(pickerState.optionsAtom, pickerScope);
      const setHidden = useUpdateAtom(pickerState.hiddenAtom, pickerScope);
    
      useEffect(() => {
        const updates = options.reduce((hidden: Record<string, boolean>, current) => {
          hidden[current.name] = !current.name.includes(search);
          return hidden;
        }, {});
    
        if (options.some(o => !!o.hidden !== updates[o.name])) setHidden(updates);
      }, [options, search, setHidden]);
    
      return <input value={search} onChange={e => setSearch(e.target.value)} />;
    }
    

    リストアイテム


    リストオブジェクト内の状態オブジェクトにアクセスすることで、実際の入力コンポーネントがレンダリングされている場所にクリックハンドリングロジックを移動できます.
    function ListItem({ option: o }: ListItemProps) {
      const [selected, setSelected] = useAtom(pickerState.selectedAtom, pickerScope);
    
      const toggleSelected = () => {
        setSelected({ ...selected, [o.name]: !o.selected });
      }
    
      if (o.hidden) return null;
      return (
        <div key={o.name} onClick={toggleSelected}>
          <p key={o.name}>{o.name}</p>
          <input type={'checkbox'} checked={!!o.selected} onChange={toggleSelected}/>
        </div>
      )
    }
    

    ラッピング


    全体の代わりにPicker コンポーネントの成長は、我々はそれに機能を追加すると、今ちょうどそれは成長する状態オブジェクトですそして、それは良いことだ!よく組織化された状態木は多くの文脈を提供して、新しい目が何が起こっているかについて理解するのを助けます.分割コンポーネントはまた、それぞれが一目で何をやっているかを明らかにする.あなたが気づいたかもしれないように、すべてのコンポーネントは実際に2つのことをしています.
    複数のアプリケーションを含むコードベースに対しては、このリファクタは、コンポーネントから内部状態を処理するすべてのロジックを引くことによってさらに一歩を踏み出すことができます.そのように、我々は一度ロジックを書いて、テストすることができて、異なる出演でピッカーを構築するのにそれを使うことができます、あるいは、モバイルまたはコマンドラインのような異なる基礎的なレンダリング・エンジンでさえ!