React Contextの理解と応用についてお話ししましょう

React Contextの理解と応用についてお話ししましょう

序文

コンテキストは文脈と訳されます。プログラミングの分野ではよく見かける概念ですが、Reactにも存在します。

React の公式ドキュメントでは、Context は Advanced に分類されており、React の高度な API に属していますが、公式はアプリの安定バージョンで Context を使用することを推奨していません。

ほとんどのアプリケーションではコンテンツを使用する必要はありません。

アプリケーションを安定させたい場合、context を使用しないでください。context は実験的な API であり、React の将来のリリースで機能しなくなる可能性があります。

しかし、これはコンテキストに注意を払う必要がないという意味ではありません。実際、多くの優れた React コンポーネントは、Context を使用して機能を完成させています。たとえば、react-redux の <Provider /> は、Context を通じてグローバル ストアを提供します。ドラッグ コンポーネント react-dnd は、Context を通じてコン​​ポーネント内の DOM ドラッグ アンド ドロップ イベントを配布します。ルーティング コンポーネント react-router は、Context を通じてルーティングの状態を管理します。 React コンポーネント開発では、Context をうまく使用すると、コンポーネントが強力かつ柔軟になります。

今日は、開発中に Context について学んだことと、それをコンポーネントの開発にどのように使用しているかについてお話ししたいと思います。

注: この記事で言及されているすべてのアプリは Web アプリを指します。

React Context の初見

コンテキストの公式定義

React ドキュメントの Web サイトでは、「Context とは何か」という定義は提供されていませんが、Context が使用されるシナリオと Context の使用方法について説明しています。

公式ウェブサイトでは、Context を使用するシナリオが次のように説明されています。

場合によっては、各レベルでプロパティを手動で渡すことなく、コンポーネント ツリーを通じてデータを渡す必要があります。強力な「コンテキスト」API を使用して、React でこれを直接実行できます。

簡単に言えば、コンポーネント ツリー内のレイヤーごとに props や state を渡してデータを渡したくない場合は、Context を使用してレベル間のコンポーネント データ転送を実装できます。


データを渡すには props または state を使用し、データは上から下に流れます。


Context を使用すると、コンポーネント間でデータを渡すことができます。

コンテキストの使い方

コンテキストが機能するには、通常は親ノードであるコンテキスト プロデューサー (プロバイダー) と、通常は 1 つ以上の子ノードであるコンテキスト コンシューマー (コンシューマー) の 2 つのコンポーネントが必要です。したがって、コンテキストの使用はプロデューサー-コンシューマー モデルに基づいています。

親コンポーネント、つまり Context プロデューサーでは、静的プロパティ childContextTypes を通じて子コンポーネントに提供される Context オブジェクトのプロパティを宣言し、Context を表すプレーン オブジェクトを返すインスタンス getChildContext メソッドを実装する必要があります。

'react' から React をインポートします
'prop-types' から PropTypes をインポートします

クラス MiddleComponent は React.Component を拡張します {
 与える () {
 <ChildComponent /> を返します。
 }
}

ParentComponentクラスはReact.Componentを拡張します。
 //コンテキストオブジェクトのプロパティを宣言する static childContextTypes = {
 propA: PropTypes.文字列、
 メソッドA: PropTypes.func
 }
 
 // Contextオブジェクトを返します。メソッド名は合意されたgetChildContext() {
 戻る {
  propA: 'propA',
  メソッドA: () => 'メソッドA'
 }
 }
 
 与える () {
 <MiddleComponent /> を返します。
 }
}

Context コンシューマーの場合、親コンポーネントによって提供される Context に次の方法でアクセスします。

'react' から React をインポートします
'prop-types' から PropTypes をインポートします

クラス ChildComponent は React.Component を拡張します {
 //使用する必要があるコンテキストプロパティを宣言します。static contextTypes = {
 propA: PropTypes.文字列
 }
 
 与える () {
 定数{
  プロップA、
  方法A
 } = this.context
 
 console.log(`context.propA = ${propA}`) // context.propA = propA
 console.log(`context.methodA = ${methodA}`) // context.methodA = 未定義
 
 戻る ...
 }
}

子コンポーネントは、親コンポーネントの Context オブジェクトのプロパティにアクセスする前に、静的プロパティ contextTypes を通じて宣言する必要があります。そうしないと、プロパティ名が間違って記述されていなくても、取得されるオブジェクトは未定義になります。

ステートレス サブコンポーネントの場合、次の方法で親コンポーネントのコンテキストにアクセスできます。

'react' から React をインポートします
'prop-types' から PropTypes をインポートします

const ChildComponent = (props, context) => {
 定数{
 プロップA
 } = コンテキスト
 
 console.log(`context.propA = ${propA}`) // context.propA = propA
 
 戻る ...
}
 
子コンポーネント.contextProps = {
 propA: PropTypes.文字列 
}

次のリリースでは、React は Context API を調整し、プロデューサー-コンシューマー モデルの使用をより明確にしました。

'react' から React をインポートします。
'react-dom' から ReactDOM をインポートします。

定数ThemeContext = React.createContext({
 背景: '赤'、
 色: 「白」
});

静的メソッド React.createContext() を使用して Context オブジェクトを作成します。この Context オブジェクトには、<Provider /> と <Consumer /> の 2 つのコンポーネントが含まれます。

クラスAppはReact.Componentを拡張します。
 与える () {
 戻る (
  <ThemeContext.Provider 値 = {{背景: '緑'、色: '白'}}>
  <ヘッダー />
  </テーマコンテキスト.プロバイダー>
 );
 }
}

<Provider /> の値は、現在の getChildContext() と同等です。

クラス Header は React.Component を拡張します {
 与える () {
 戻る (
  <Title>こんにちは、React コンテキスト API</Title>
 );
 }
}
 
クラス Title は React.Component を拡張します {
 与える () {
 戻る (
  <テーマコンテキスト.コンシューマー>
  {コンテキスト => (
   <h1 スタイル = {{背景: context.background、色: context.color}}>
   {this.props.children}
   </h1>
  )}
  </ThemeContext.Consumer>
 );
 }
}

<Consumer /> の子要素は関数である必要があり、<Provider /> によって提供されるコンテキストは関数パラメータを通じて取得されます。

Context の新しい API は React のスタイルに近いことがわかります。

コンテキストを直接取得できるいくつかの場所

実際、インスタンスのコンテキスト プロパティ (this.context) に加えて、React コンポーネントには、親コンポーネントによって提供されるコンテキストに直接アクセスできる場所が他にも多数あります。たとえば、構築方法:

  • コンストラクタ(props, context)

たとえば、ライフサイクルは次のようになります。

  • コンポーネントはプロパティを受け取ります(次のプロパティ、次のコンテキスト)
  • コンポーネントを更新する必要があります(次のプロパティ、次の状態、次のコンテキスト)
  • コンポーネントを更新します(次のプロパティ、次の状態、次のコンテキスト)

関数指向のステートレス コンポーネントの場合、関数のパラメータを通じてコン​​ポーネントのコンテキストに直接アクセスできます。

const StatelessComponent = (props, コンテキスト) => (
 ......
)

以上がコンテキストの基本です。より詳しい説明については、こちらを参照してください。

コンテキストについての私の理解

さて、基本的なことを話した後で、React のコンテキストについての私の理解についてお話ししたいと思います。

コンテキストをコンポーネントスコープとして扱う

React を使用する開発者は、React アプリが本質的に React コンポーネント ツリーであることを知っています。各 React コンポーネントは、このツリーのノードに相当します。アプリのルート ノードを除き、他の各ノードには親コンポーネント チェーンがあります。

たとえば、上の図では、<Child /> の親コンポーネント チェーンは <SubNode /> -- <Node /> -- <App /> であり、<SubNode /> の親コンポーネント チェーンは <Node /> -- <App /> であり、<Node /> の親コンポーネント チェーンには <App /> という 1 つのコンポーネント ノードのみがあります。

これらのツリー接続されたコンポーネント ノードは、実際には Context ツリーを形成します。各ノードの Context は、親コンポーネント チェーン上のすべてのコンポーネント ノードによって getChildContext() を通じて提供される Context オブジェクトで構成されるオブジェクトです。


JS スコープ チェーンの概念に精通している開発者は、JS コード ブロックの実行中に対応するスコープ チェーンが作成されることを知っているはずです。このスコープ チェーンは、変数や関数など、実行時の JS コード ブロックの実行中にアクセスできるアクティブなオブジェクトを記録します。JS プログラムは、スコープ チェーンを介してコード ブロックの内外の変数や関数にアクセスします。

JS スコープ チェーンを例えとして使用すると、React コンポーネントによって提供される Context オブジェクトは、実際にはアクセス用に子コンポーネントに提供されるスコープのようなもので、Context オブジェクトのプロパティはスコープ内のアクティブ オブジェクトとして見ることができます。コンポーネントのコンテキストは、getChildContext() を通じて親ノード チェーン上のすべてのコンポーネントによって返される Context オブジェクトで構成されるため、コンポーネントは Context を通じて親コンポーネント チェーン上のすべてのノード コンポーネントによって提供される Context のプロパティにアクセスできます。

そこで、JS スコープ チェーンの考え方を借用し、コンポーネントのスコープとして Context を使用しました。

コンテキストの制御性と影響に焦点を当てる

しかし、コンポーネントスコープとしてのコンテキストは、一般的なスコープの概念とは異なります(私がこれまで触れてきたプログラミング言語に関する限り)。コンテキストの制御可能性と影響に焦点を当てる必要があります。

日常の開発では、スコープやコンテキストの使用は非常に一般的で、自然であり、無意識にさえ行われます。ただし、React でコンテキストを使用するのはそれほど簡単ではありません。親コンポーネントは、childContextTypes を通じて、親コンポーネントが提供する Context を「宣言」する必要があり、子コンポーネントは contextTypes を通じて親コンポーネントの Context プロパティを「適用」する必要があります。したがって、React の Context は「許可された」コンポーネント スコープであると私は考えています。

この「承認された」アプローチの利点は何でしょうか?私の理解する限りでは、まず第一に、フレームワーク API の一貫性を維持し、propTypes と同様に宣言型のコーディング スタイルを使用することです。さらに、コンポーネントが提供するコンテキストの制御可能性と影響範囲をある程度確保することができます。

React App のコンポーネントはツリーのような構造で、レイヤーごとに拡張され、親子コンポーネントは 1 対多の線形依存関係です。コンテキストをランダムに使用すると、この依存関係が実際に破壊され、コンポーネント間に不要な依存関係が追加され、コンポーネントの再利用性が低下し、アプリの保守性に影響を与える可能性があります。

上の図から、元の線形依存コンポーネント ツリーでは、子コンポーネントが親コンポーネントの Context を使用するため、<Child /> コンポーネントが <Node /> と <App /> の両方に依存することがわかります。これら 2 つのコンポーネントから分離されると、<Child /> の可用性は保証されなくなり、<Child /> の再利用性が低下します。


私の意見では、react-redux がこのように行っているとしても、コンテキストを介してデータや API を公開することはエレガントな方法ではありません。したがって、不必要な影響を減らすためのメカニズム、つまり制約が必要です。

childContextTypes と contextTypes の 2 つの静的プロパティを制約することにより、データであろうと関数であろうと、コンポーネント自体、またはコンポーネントに関連する他の子コンポーネントだけがコンテキストのプロパティに自由にアクセスできることが、ある程度保証されます。コンポーネント自体または関連する子コンポーネントのみが、どの Context プロパティにアクセスできるかを明確に知ることができ、内部か外部かを問わず、コンポーネントに関連しない他のコンポーネントについては、親コンポーネント チェーン内の各親コンポーネントの childContextTypes によってどの Context プロパティが「宣言」されているかが不明であるため、contextTypes を通じて関連するプロパティに「適用」することはできません。したがって、コンポーネントのスコープ コンテキストに「権限」を与えることで、コンテキストの制御可能性と影響の範囲をある程度確保できると理解しています。

コンポーネントを開発する過程では、常にこれに注意し、Context を軽々しく使用しないようにする必要があります。

最初にコンテキストを使用する必要はありません

React は高レベル API であるため、Context の使用を優先することは推奨されていません。私の理解はこうです:

  • Context はまだ実験段階であり、今後のリリースで大きな変更が行われる可能性があります。実際、これはすでに起こっています。したがって、将来のアップグレードで大きな影響やトラブルを回避するために、アプリで Context を使用することはお勧めしません。
  • App では Context を使用することは推奨されませんが、コンポーネントの場合は影響範囲が App よりも小さいため、コンポーネント ツリーの依存関係を破壊せずに高い凝集性を実現できる場合は、Context の使用を検討できます。
  • コンポーネント間のデータ通信や状態管理には、props または state の使用を優先し、次に他のサードパーティの成熟したライブラリの使用を検討してください。上記の方法が最適な選択ではない場合は、Context の使用を検討してください。
  • コンテキストの更新は setState() によってトリガーされる必要がありますが、これは信頼できません。 Context は、コンポーネント間のアクセスをサポートします。ただし、中間のサブコンポーネントが、shouldComponentUpdate() が false を返すなど、何らかのメソッドを通じて更新に応答しない場合は、Context の更新が Context を使用してサブコンポーネントに到達するという保証はありません。したがって、コンテキストの信頼性には注意が必要です。ただし、API の新しいバージョンでは更新の問題は解決されています。

つまり、Context が制御可能であることを保証できる限り、Context を使用しても問題はありません。Context を合理的に適用できれば、実際に Context は React コンポーネント開発に非常に強力なエクスペリエンスをもたらすことができます。

コンテキストをデータ共有の媒体として使用する

公式コンテキストは、コンポーネント間のデータ通信に使用できます。私はそれをデータを共有するための橋、媒体として理解しています。データ共有は、アプリ レベルとコンポーネント レベルの 2 つのカテゴリに分けられます。

  • アプリレベルのデータ共有

アプリ ルート ノード コンポーネントによって提供される Context オブジェクトは、アプリ レベルのグローバル スコープと見なすことができるため、アプリ ルート ノード コンポーネントによって提供される Context オブジェクトを使用して、アプリ レベルのグローバル データを作成します。既成の例については、 react-redux を参照してください。以下は、 <Provider /> コンポーネントのソース コードのコア実装です。

エクスポート関数createProvider(storeKey = 'store', subKey) {
 const subscriptionKey = サブキー || `${storeKey}サブスクリプション`

 クラス Provider は Component を拡張します {
  子コンテキストを取得する() {
   戻り値: [storeKey]: this[storeKey], [subscriptionKey]: null }
  }

  コンストラクタ(props, context) {
   super(プロパティ、コンテキスト)
   ストアキーをprops.storeに設定します。
  }

  与える() {
   Children.only(this.props.children) を返します
  }
 }

 // ......

 プロバイダー.propTypes = {
  ストア: storeShape.isRequired、
  子: PropTypes.element.isRequired、
 }
 プロバイダー.childContextTypes = {
  [storeKey]: storeShape.isRequired、
  [サブスクリプションキー]: サブスクリプションシェイプ、
 }

 プロバイダを返す
}

デフォルトのcreateProvider()をエクスポートする

アプリのルート コンポーネントが <Provider /> コンポーネントでラップされると、基本的にアプリのグローバル プロパティ ストアが提供され、これはアプリ全体でストア プロパティを共有することと同じです。もちろん、<Provider /> コンポーネントを他のコンポーネントにラップして、コンポーネント レベルでストアをグローバルに共有することもできます。

  • コンポーネントレベルのデータ共有

コンポーネントの機能がコンポーネント自体では完了できず、追加のサブコンポーネントに依存する必要がある場合は、コンテキストを使用して複数のサブコンポーネントで構成されるコンポーネントを構築できます。たとえば、 react-router 。

react-router の <Router /> は、ナビゲーション リンクとリダイレクトされたコンテンツが通常分離されているため、ルーティング操作と管理を単独で完了することはできません。そのため、ルーティング関連の作業を一緒に完了するには、<Link /> や <Route /> などのサブコンポーネントに依存する必要もあります。関連するサブコンポーネントを連携させるために、 react-router の実装では、 Context を使用して <Router />、 <Link />、 <Route /> などの関連するコンポーネント間でルーターを共有し、ルーティングの統一された操作と管理を完了します。

以下は、上記をよりよく理解するための関連コンポーネント <Router />、<Link />、<Route /> の部分的なソース コードです。

// ルータ.js

/**
 * 履歴をコンテキストに配置するためのパブリック API。
 */
クラス Router は React.Component を拡張します {
 静的プロパティタイプ = {
 履歴: PropTypes.object.isRequired、
 子: PropTypes.node
 };

 静的コンテキストタイプ = {
 ルーター: PropTypes.object
 };

 静的な子コンテキストタイプ = {
 ルーター: PropTypes.object.isRequired
 };

 子コンテキストを取得する() {
 戻る {
  ルーター: {
  ...このコンテキストルーター、
  履歴: this.props.history、
  ルート: {
   場所: this.props.history.location、
   一致: this.state.match
  }
  }
 };
 }
 
 // ......
 
 コンポーネントマウント() {
 const { children, history } = this.props;
 
 // ......
 
 this.unlisten = history.listen(() => {
  this.setState({
  一致: this.computeMatch(history.location.pathname)
  });
 });
 }

 // ......
}

ソース コードには他のロジックもありますが、<Router /> の中核は、子コンポーネントのルーター属性を持つ Context を提供し、同時に履歴を監視し、履歴が変更されると setState() を通じてコン​​ポーネントの再レンダリングをトリガーすることです。

// リンク.js

/**
 * 履歴対応の <a> をレンダリングするためのパブリック API。
 */
クラスLinkはReact.Componentを拡張します。
 
 // ......
 
 静的コンテキストタイプ = {
 ルーター: PropTypes.shape({
  履歴: PropTypes.shape({
  プッシュ: PropTypes.func.isRequired、
  置き換え: PropTypes.func.isRequired、
  createHref: PropTypes.func.isRequired
  })。が必要です
 })。が必要です
 };

 handleClick = イベント => {
 this.props.onClick の場合、 this.props.onClick(イベント);

 もし (
  !event.defaultPrevented &&
  イベント.ボタン === 0 &&
  !this.props.target &&
  !isModifiedEvent(イベント)
 ){
  イベントをデフォルトにしない();
  // <Router /> コンポーネントによって提供されるルーター インスタンスを使用します。const { history } = this.context.router;
  const { replace, to } = this.props;

  (置換)の場合{
  history.replace(to);
  } それ以外 {
  history.push(to);
  }
 }
 };
 
 与える() {
 const { replace, to, innerRef, ...props } = this.props;

 // ...

 const { history } = this.context.router;
 定数場所 =
  typeof を === "string" にする
  ? createLocation(to, null, null, history.location)
  : に;

 場所のhrefを履歴に追加します。
 戻る (
  <a {...props} onClick={this.handleClick} href={href} ref={innerRef} />
 );
 }
}

<Link /> の核となるのは、<a> タグをレンダリングし、<a> タグのクリック イベントをインターセプトし、<Router /> が共有するルーターを介して履歴に対してルーティング操作を実行し、<Router /> に再レンダリングを通知することです。

// ルート.js

/**
 * 単一のパスを一致させてレンダリングするためのパブリック API。
 */
クラス Route は React.Component を拡張します {
 
 // ......
 
 状態 = {
 一致: this.computeMatch(this.props, this.context.router)
 };

 // 一致するパスを計算します。一致する場合は一致するオブジェクトを返し、そうでない場合は null を返します。
 マッチを計算する(
 { computedMatch、場所、パス、厳密、正確、敏感 },
 ルーター
 ){
 if (computedMatch) は computedMatch を返します。
 
 // ......

 const { ルート } = ルーター;
 const パス名 = (場所 || route.location).pathname;
 
 matchPath(pathname, { path, strict, exact, sensitive }, route.match) を返します。
 }
 
 // ......

 与える() {
 const { マッチ } = this.state;
 const { children, component, render } = this.props;
 const { history, route, staticContext } = this.context.router;
 定数 location = this.props.location || route.location;
 const props = { 一致、場所、履歴、静的コンテキスト };

 if (component) return match? React.createElement(component, props): null;

 if (render) return match? render(props): null;

 if (typeof children === "function") return children(props);

 if (children && !isEmptyChildren(children))
  React.Children.only(children) を返します。

 null を返します。
 }
}

<Route /> には <Router /> に似たソースコードがあり、ネストされたルートを実現できますが、その核となるのは、現在のルートのパスが Context によって共有されるルーターを介して一致するかどうかを判断し、コンポーネントをレンダリングすることです。

上記の分析から、 react-router 全体が実際には <Router /> のコンテキストを中心に構築されていることがわかります。

コンテキストを使用したコンポーネントの開発

以前は、スロット配布コンポーネントという単純なコンポーネントが Context を通じて開発されていました。この章では、このスロット配布コンポーネントの開発経験を活用して、コンテキストを使用してコンポーネントを開発する方法について説明します。

スロット分配コンポーネント

まず、スロット分散コンポーネントとは何かについて説明します。この概念は、Vuejs で初めて導入されました。スロット配布とは、コンポーネントの組み合わせによって親コンポーネントの内容を子コンポーネントのテンプレートに挿入する技術です。Vuejs ではスロットと呼ばれています。

この概念をより直感的に理解できるように、スロット分散に関するデモを Vuejs から移行しました。

提供されたスロットを持つコンポーネント <my-component /> の場合、テンプレートは次のようになります。

<div>
 <h2>私はサブコンポーネントのタイトルです</h2>
 <スロット>
 配信するコンテンツがない場合にのみ表示されます</slot>
</div>

親コンポーネントの場合、テンプレートは次のようになります。

<div>
 <h1>私は親コンポーネントのタイトルです</h1>
 <私のコンポーネント>
 <p>これは初期コンテンツです</p>
 <p>これは初期のコンテンツです</p>
 </my-component>
</div>

最終的なレンダリング結果:

<div>
 <h1>私は親コンポーネントのタイトルです</h1>
 <div>
 <h2>私はサブコンポーネントのタイトルです</h2>
 <p>これは初期コンテンツです</p>
 <p>これは初期のコンテンツです</p>
 </div>
</div>

コンポーネント <my-component /> の <slot /> ノードが、最終的に親コンポーネントの <my-component /> ノードの下のコンテンツに置き換えられることがわかります。

Vuejs は名前付きスロットもサポートしています。

たとえば、レイアウト コンポーネント <app-layout />:

<div class="コンテナ">
 <ヘッダー>
 <スロット名="ヘッダー"></スロット>
 </ヘッダー>
 <メイン>
 <スロット></スロット>
 </メイン>
 <フッター>
 <スロット名="フッター"></スロット>
 </フッター>
</div>

親コンポーネント テンプレートでは次のようになります。

<アプリレイアウト>
 <h1 slot="header">これはページタイトルである可能性があります</h1>
 <p>メインコンテンツの段落。 </p>
 <p>別の段落。 </p>
 <p slot="footer">連絡先情報はこちら</p>
</アプリレイアウト>

最終的なレンダリング結果:

<div class="コンテナ">
 <ヘッダー>
 <h1>これはページタイトルの可能性があります</h1>
 </ヘッダー>
 <メイン>
 <p>メインコンテンツの段落。 </p>
 <p>別の段落。 </p>
 </メイン>
 <フッター>
 <p>連絡先情報はこちらです</p>
 </フッター>
</div>

スロット分散の利点は、コンポーネントをテンプレートに抽象化できることです。コンポーネント自体はテンプレート構造のみを考慮し、具体的な内容は親コンポーネントに任せます。同時に、DOM 構造を記述する HTML の文法表現を崩しません。これは非常に有意義な技術だと思いますが、残念ながら、React のこの技術に対するサポートはあまり親切ではありません。そこで、Vuejs のスロット配布コンポーネントを参考に、React ベースのスロット配布コンポーネントのセットを開発しました。これにより、React コンポーネントにもテンプレート機能が備わります。

<AppLayout /> コンポーネントの場合は、次のように記述します。

クラスAppLayoutはReact.Componentを拡張します。
 静的 displayName = 'AppLayout'
 
 与える () {
 戻る (
  <div class="コンテナ">
  <ヘッダー>
   <スロット名="ヘッダー"></スロット>
  </ヘッダー>
  <メイン>
   <スロット></スロット>
  </メイン>
  <フッター>
   <スロット名="フッター"></スロット>
  </フッター>
  </div>
 )
 }
}

外側のレイヤーで使用する場合は、次のように記述できます。

<アプリレイアウト>
 <アドオンスロット="ヘッダー">
 <h1>これはページタイトルの可能性があります</h1>
 </アドオン>
 <アドオン>
 <p>メインコンテンツの段落。 </p>
 <p>別の段落。 </p>
 </アドオン>
 <アドオンスロット="フッター">
 <p>連絡先情報はこちらです</p>
 </アドオン>
</アプリレイアウト>

コンポーネント実装のアイデア

これまで考えてきたことを踏まえて、まずは実装案を整理してみましょう。

スロット配布コンポーネントが、スロット コンポーネント <Slot /> と配布コンポーネント <AddOn /> という 2 つのサブコンポーネントに依存する必要があることは容易にわかります。スロット コンポーネントは、コンテンツを配布するためのスロットを積み重ねて提供する役割を担います。配信コンポーネントは、配信コンテンツを収集し、それをスロット コンポーネントに提供して配信コンテンツをレンダリングする役割を担います。これは、スロットのコンシューマーに相当します。

明らかに、ここには問題があります。<Slot /> コンポーネントは <AddOn /> コンポーネントから独立しています。<AddOn /> のコンテンツを <Slot /> に入力するにはどうすればよいでしょうか?この問題を解決するのは難しくありません。 2 つの独立したモジュールを接続する必要がある場合は、それらの間にブリッジを構築するだけです。それで、この橋をどうやって作るのでしょうか?先ほど想像したコードを振り返ってみましょう。

<AppLayout /> コンポーネントの場合は、次のように記述します。

クラスAppLayoutはReact.Componentを拡張します。
 静的 displayName = 'AppLayout'
 
 与える () {
 戻る (
  <div class="コンテナ">
  <ヘッダー>
   <スロット名="ヘッダー"></スロット>
  </ヘッダー>
  <メイン>
   <スロット></スロット>
  </メイン>
  <フッター>
   <スロット名="フッター"></スロット>
  </フッター>
  </div>
 )
 }
}

外側の層で使用する場合は、次のように記述します。

<アプリレイアウト>
 <アドオンスロット="ヘッダー">
 <h1>これはページタイトルの可能性があります</h1>
 </アドオン>
 <アドオン>
 <p>メインコンテンツの段落。 </p>
 <p>別の段落。 </p>
 </アドオン>
 <アドオンスロット="フッター">
 <p>連絡先情報はこちらです</p>
 </アドオン>
</アプリレイアウト>

<Slot /> であっても <AddOn /> であっても、それらは実際には <AppLayout /> の範囲内にあります。 <Slot /> は <AppLayout /> コンポーネントの render() メソッドによって返されるコンポーネント ノードであり、 <AddOn /> は <AppLayout /> の子ノードです。したがって、 <AppLayout /> は <Slot /> と <AddOn /> の間のブリッジと見なすことができます。では、<AppLayout /> はどのようにして <Slot /> と <AddOn /> 間の接続を確立するのでしょうか?ここでは、この記事の主人公である「コンテキスト」を使用します。次の質問は、Context を使用して <Slot /> と <AddOn /> 間の接続を確立するにはどうすればよいかということです。

ブリッジ <AppLayout /> については先ほど説明しました。外側のコンポーネントでは、<AppLayout /> が <AddOn /> を通じてスロットを埋めるためのコンテンツを収集する役割を担います。 <AppLayout /> は、コンテキストを利用して入力コンテンツを取得するためのインターフェースを定義します。レンダリングの際、<Slot /> は <AppLayout /> によってレンダリングされるノードであるため、<Slot /> は Context を通じて <AppLayout /> で定義された充填コンテンツを取得するためのインターフェースを取得し、このインターフェースを通じて充填コンテンツを取得してレンダリングすることができます。

アイデアに従ってスロット配分コンポーネントを実装する

<AddOn /> は <AppLayout /> の子ノードであり、<AddOn /> は特定のコンポーネントであるため、名前または displayName で識別できます。したがって、レンダリングの前、つまり render() が返される前に、<AppLayout /> は子ノードを走査し、 slot の値をキーとして使用して <AddOn /> の各子ノードをキャッシュします。 <AddOn /> がスロットを設定しない場合は、名前のない <Slot /> を埋めているものとみなされます。これらの名前のないスロットには、$$default などのキーを指定できます。

<AppLayout /> の場合、コードは次のようになります。

クラスAppLayoutはReact.Componentを拡張します。
 
 静的な子コンテキストタイプ = {
 リクエストAddOnRenderer: PropTypes.func
 }
 
 // 各 <AddOn /> のコンテンツをキャッシュするために使用されます。addOnRenderers = {}
 
 // Context { を通じて子ノードに getChildContext () インターフェイスを提供します
 const requestAddOnRenderer = (名前) => {
  if (!this.addOnRenderers[名前]) {
  未定義を返す
  }
  戻り値 () => (
  this.addOnRenderers[名前]
  )
 }
 戻る {
  リクエストAddOnRenderer
 }
 }

 与える () {
 定数{
  子供たち、
  ...レストプロップ
 } = this.props

 (子供)の場合{
  // kv モードで <AddOn /> の内容をキャッシュする const arr = React.Children.toArray(children)
  定数名Checked = []
  this.addOnRenderers = {}
  arr.forEach(アイテム => {
  定数 itemType = アイテム.type
  if (item.type.displayName === 'アドオン') {
   const slotName = item.props.slot || '$$default'
   // コンテンツの一意性を保証する if (nameChecked.findIndex(item => item === stubName) !== -1) {
   新しいエラーをスローします(`スロット(${slotName}) は使用されています`)
   }
   this.addOnRenderers[stubName] = item.props.children
   nameChecked.push(スタブ名)
  }
  })
 }

 戻る (
  <div class="コンテナ">
  <ヘッダー>
   <スロット名="ヘッダー"></スロット>
  </ヘッダー>
  <メイン>
   <スロット></スロット>
  </メイン>
  <フッター>
   <スロット名="フッター"></スロット>
  </フッター>
  </div>
 )
 }
}

<AppLayout /> は、Context インターフェイス requestAddOnRenderer() を定義します。requestAddOnRenderer() インターフェイスは、名前に基づいて関数を返します。返された関数は、名前に基づいて addOnRenderers のプロパティにアクセスします。addOnRenderers は、<AddOn /> のコンテンツ キャッシュ オブジェクトです。

<Slot /> の実装は非常にシンプルで、コードは次のようになります。

// プロパティ、コンテキスト
const スロット = ({ name, children }, { requestAddOnRenderer }) => {
 const addOnRenderer = requestAddOnRenderer(名前)
 戻り値 (addOnRenderer && addOnRenderer()) ||
 子供
 ヌル
}

Slot.displayName = 'スロット'
Slot.contextTypes = { requestAddOnRenderer: PropTypes.func }
Slot.propTypes = { 名前: PropTypes.文字列 }
Slot.defaultProps = { 名前: '$$default' }

<Slot /> はコンテキストを通じて <AppLayout /> が提供する requestAddOnRenderer() インターフェースを取得し、最終的なレンダリングの主なオブジェクトは <AppLayout /> にキャッシュされた <AddOn /> のコンテンツであることがわかります。指定された <AddOn /> のコンテンツが取得されない場合、 <Slot /> 自体の子要素がレンダリングされます。

<AddOn /> はさらにシンプルです:

定数AddOn = () => null

AddOn.propTypes = { スロット: PropTypes.string }
AddOn.defaultTypes = { スロット: '$$default' }
AddOn.displayName = 'アドオン'

<AddOn /> は何もせず、null を返します。その機能は、スロットに配布されたコンテンツを <AppLayout /> にキャッシュさせることです。

<AppLayout /> をより多用途にすることができます

上記のコードにより、<AppLayout /> は基本的にスロット分散機能を持つコンポーネントに変換されますが、<AppLayout /> がユニバーサルではないことは明らかです。これを独立したユニバーサル コンポーネントに昇格できます。

このコンポーネントにSlotProviderという名前を付けました

関数 getDisplayName (コンポーネント) {
 コンポーネント.displayName || コンポーネント.name || 'コンポーネント' を返します
}

const slotProviderHoC = (ラップされたコンポーネント) => {
 戻りクラスはReact.Componentを拡張します{
 静的 displayName = `SlotProvider(${getDisplayName(WrappedComponent)})`

 静的な子コンテキストタイプ = {
  リクエストAddOnRenderer: PropTypes.func
 }
 
 // 各 <AddOn /> のコンテンツをキャッシュするために使用されます。addOnRenderers = {}
 
 // Context { を通じて子ノードに getChildContext () インターフェイスを提供します
  const requestAddOnRenderer = (名前) => {
  if (!this.addOnRenderers[名前]) {
   未定義を返す
  }
  戻り値 () => (
   this.addOnRenderers[名前]
  )
  }
  戻る {
  リクエストAddOnRenderer
  }
 }

 与える () {
  定数{
  子供たち、
  ...レストプロップ
  } = this.props

  if (子) {
  // kv モードで <AddOn /> の内容をキャッシュする const arr = React.Children.toArray(children)
  定数名Checked = []
  this.addOnRenderers = {}
  arr.forEach(アイテム => {
   定数 itemType = アイテム.type
   if (item.type.displayName === 'アドオン') {
   const slotName = item.props.slot || '$$default'
   // コンテンツの一意性を保証する if (nameChecked.findIndex(item => item === stubName) !== -1) {
    新しいエラーをスローします(`スロット(${slotName}) は使用されています`)
   }
   this.addOnRenderers[stubName] = item.props.children
   nameChecked.push(スタブ名)
   }
  })
  }
  
  戻り値 (<WrappedComponent {...restProps} />)
 }
 }
}

エクスポート const SlotProvider = slotProviderHoC

React の高階コンポーネントを使用して、元の <AppLayout /> を独立した一般的なコンポーネントに変換します。元の <AppLayout /> の場合、この SlotProvider 高階コンポーネントを使用して、スロット配布機能を持つコンポーネントに変換できます。

'./SlotProvider.js' から {SlotProvider} をインポートします。

クラスAppLayoutはReact.Componentを拡張します。
 静的 displayName = 'AppLayout'
 
 与える () {
 戻る (
  <div class="コンテナ">
  <ヘッダー>
   <スロット名="ヘッダー"></スロット>
  </ヘッダー>
  <メイン>
   <スロット></スロット>
  </メイン>
  <フッター>
   <スロット名="フッター"></スロット>
  </フッター>
  </div>
 )
 }
}

デフォルトのSlotProvider(AppLayout)をエクスポートします。

上記の経験から、コンポーネントを設計・開発する際には、

  • コンポーネントの機能を完了するには、ルート コンポーネントと複数のサブコンポーネントが連携して動作する必要がある場合があります。たとえば、スロット配布コンポーネントでは、実際には SlotProvider を <Slot /> および <AddOn /> と一緒に使用する必要があります。SlotProvider はルート コンポーネントですが、<Slot /> と <AddOn /> はどちらもサブコンポーネントです。
  • ルート コンポーネントに対するサブコンポーネントの位置、またはサブコンポーネント間の位置は定義されていません。 SlotProvider の場合、<Slot /> の位置は不定です。SlotProvider の上位コンポーネントにラップされたコンポーネントのテンプレートの任意の場所に配置されます。<Slot /> と <AddOn /> の場合、直接の位置も不定です。1 つは SlotProvider にラップされたコンポーネント内、もう 1 つは SlotProvider の子です。
  • サブコンポーネントは、いくつかのグローバル API またはデータに依存する必要があります。たとえば、<Slot /> の実際のレンダリングされたコンテンツは、SlotProvider によって収集された <AddOn /> のコンテンツから取得されます。

このとき、データを共有するための媒体として仲介者を使用する必要があります。redux などのサードパーティ モジュールを導入するよりも、Context を直接使用する方がエレガントになります。

新しいコンテキストAPIを試す

新しいバージョンの Context API を使用して、以前のスロット配布コンポーネントを変換します。

// スロットプロバイダー.js

関数 getDisplayName (コンポーネント) {
 コンポーネント.displayName || コンポーネント.name || 'コンポーネント' を返します
}

エクスポートconstSlotContext = React.createContext({
 リクエストAddOnRenderer: () => {}
})

const slotProviderHoC = (ラップされたコンポーネント) => {
 戻りクラスはReact.Componentを拡張します{
 静的 displayName = `SlotProvider(${getDisplayName(WrappedComponent)})`

 // 各 <AddOn /> のコンテンツをキャッシュするために使用されます。addOnRenderers = {}
 
 requestAddOnRenderer = (名前) => {
  if (!this.addOnRenderers[名前]) {
  未定義を返す
  }
  戻り値 () => (
  this.addOnRenderers[名前]
  )
 }

 与える () {
  定数{
  子供たち、
  ...レストプロップ
  } = this.props

  if (子) {
  // kv モードで <AddOn /> の内容をキャッシュする const arr = React.Children.toArray(children)
  定数名Checked = []
  this.addOnRenderers = {}
  arr.forEach(アイテム => {
   定数 itemType = アイテム.type
   if (item.type.displayName === 'アドオン') {
   const slotName = item.props.slot || '$$default'
   // コンテンツの一意性を保証する if (nameChecked.findIndex(item => item === stubName) !== -1) {
    新しいエラーをスローします(`スロット(${slotName}) は使用されています`)
   }
   this.addOnRenderers[stubName] = item.props.children
   nameChecked.push(スタブ名)
   }
  })
  }
  
  戻る (
  <SlotContext.Provider 値={
   リクエストAddOnRenderer: this.requestAddOnRenderer
   }>
   <ラップされたコンポーネント {...restProps} />
  </slotcontext.provider>
  )
 }
 }
}

const slotprovider = slotproviderhocをエクスポートします

以前のChildContextTypesとgetChildContext()は、いくつかのローカル調整を除いて削除されています。

// slot.js

'./slotprovider.js'から{slotContext}をインポート

const slot =({name、children})=> {
 戻る (
 <slotcontext.consumer>
  {(context)=> {
  const addonrenderer = requestaddonrenderer(name)
   return(addonrenderer && addonrenderer())||
   子供||
   ヌル
  }}
 </slotcontext.consumer>
 )
}

slot.displayname = 'slot'
slot.proptypes = {name:proptypes.string}
slot.defaultProps = {name: '$$ default'}

コンテキストは以前はプロデューサー - 消費者モードで使用されていたため、コンポーネント自体は比較的単純だったため、新しいAPIを使用した変換後に大きな違いはありませんでした。

要約する

  • PropsやStateと比較して、Reactのコンテキストは、クロスレベルのコンポーネント通信を実現できます。
  • コンテキストAPIの使用は、プロデューサーと消費者モデルに基づいています。プロデューサー側では、コンポーネントの静的プロパティ保育術を介して宣言され、コンテキストオブジェクトはインスタンスメソッドgetChildContext()を使用して作成されます。消費者側では、コンポーネントの静的プロパティコンテキストタイプを介して使用するコンテキストプロパティを適用し、インスタンスコンテキストを介してコンテキストプロパティにアクセスします。
  • コンテキストを使用するには、コンポーネントの凝集性、コンポーネントツリーの依存関係を破壊しないでください。
  • コンテキストを介してAPIを公開すると、いくつかの問題をある程度解決するために利便性がもたらされる可能性がありますが、個人的には、それは良い習慣ではなく、注意する必要があると思います。
  • コンテキストアップデートの古いバージョンは、信頼できないSetState()に依存していますが、この問題はAPIの新しいバージョンで解決されています。
  • コンポーネントの範囲と考えることはできますが、コンポーネントを使用する前に、コンポーネントの制御性と影響力の範囲に注意する必要があります。
  • コンテキストは、アプリレベルまたはコンポーネントレベルでデータを共有するための媒体として使用できます。
  • コンポーネントを設計および開発するとき、このコンポーネントに複数のコンポーネントの関連性と組み合わせが必要な場合、コンテキストを使用する方がエレガントになる場合があります。

上記は私の共有コンテンツです。

参考文献

コンテキスト-https://reactjs.org/docs/context.html
React 16.3はここにあります:新しいコンテキストAPI -http://cnodejs.org/topic/5a7bd5c4497a08f571384f03
スロット付きのコンテンツ配布-https://vuejs.org/v2/guide/components.html#content-distribution-with-slots

これは、Reactコンテキストの私の理解と応用に関する終わりです。

以下もご興味があるかもしれません:
  • Reactにおけるコンテキスト適用シナリオの分析
  • Reactコンテキストを使用してvueスロット関数を実装する
  • ReactのPropsの簡単な比較
  • Reactでpropsを使用する方法と制限する方法
  • Reactの3つの主要属性におけるpropsの使用の詳細な説明
  • Reactのコンテキストとプロパティの説明

<<:  crontab スケジュールされたタスクが実行されない理由の分析と解決

>>:  MySql インデックスの詳細な紹介と正しい使用方法

推薦する

Nginx の負荷分散構成、ダウンタイム発生時の自動切り替えモード

厳密に言えば、nginx には負荷分散バックエンド ノードのヘルス チェック機能はありませんが、デフ...

Ubuntu 18.04 向け VMware Tools のインストールと構成のチュートリアル

この記事では、Ubuntu 18.04でのVMware Toolsのインストールと設定について記録し...

Spring Boot + jar パッケージングのデプロイメント Tomcat 404 エラーの問題を解決する

1. Spring Boot は jsp jar パッケージをサポートしていません。jsp は wa...

Element-ui の組み込み 2 つのリモート検索 (ファジークエリ) の使用方法の説明

問題の説明フロントエンドリモート検索やファジークエリと呼ばれる種類のクエリがあります。 Ele.me...

MySQL データベース開発仕様 [推奨]

最近、問題のある新しい SQL が本番データベースに入力される数を最小限に抑えるために、開発仕様を整...

Alibaba Cloud ドメイン名と IP バインディングの手順と方法

1 Alibaba Cloud コンソールに入り、ドメイン名コンソールを見つけて、バインドするドメイ...

html mailto(メール)の実用化について

ご存知のとおり、mailto は Web デザインと制作において非常に実用的な HTML タグです。...

例を通してMySQLパーティションテーブルの原理と一般的な操作を学びます

1. パーティションテーブルの意味パーティション テーブル定義は、任意のサイズに設定できるルールに従...

react+antd.3x は IP 入力ボックスを実装します

この記事では、IP入力ボックスを実装するための react+antd.3x の具体的なコードを参考ま...

MySQLでよく使われる演算子と関数の概要

まずデータ テーブルを作成しましょう。 使用テスト; テーブル「従業員」を作成します( emp_no...

React Hook の使用例 (一般的なフック 6 つ)

1. useState: 関数コンポーネントに状態を持たせる使用例: // カウンター impor...

収集する価値のあるCSS命名規則(ルール) よく使われるCSS命名規則

CSS命名規則(ルール) よく使われるCSS命名規則ヘッダー: ヘッダーコンテンツ: コンテンツ/コ...

Web 標準アプリケーション: Tencent QQ ホームページの再設計

Tencent QQのホームページがリニューアルされ、Webフロントエンド開発がますます注目を集めて...

Vueにおける仮想DOMの理解のまとめ

これは本質的に、ビュー インターフェース構造を記述するために使用される共通の js オブジェクトです...

Typora コードブロックのカラーマッチングとタイトルシリアル番号実装コード

効果: タイトルには独自のシリアル番号があり、コードブロックには配色があり、コードブロックの左上隅に...