前回はスロットについてでした。
今回は鬼門でもある高階コンポーネントについてです。
パッケージは@wordpress/compose
にあります。
Gutenbergのソースコードを見ていると当たり前に出てくるのがこのパッケージです。
compose()
や createHigherOrderComponent()
はよく使います。
このパッケージを理解するには関数型言語についてある程度理解する必要があります。
普段関数型言語を使ってないので慣れるのにだいぶかかりました。
私自身厳密に理解しているわけではないので間違っている箇所があるかもしれません。
それを理解したうえで読んで頂ければ。
このページは難しいかもしれないので直ぐには理解できなくてもかまいません。
ブロック作成のコードを追っていくと時々出くわす程度ですので。
compose()
は関数を合成する関数で、
createHigherOrderComponent()
はHOC(higher order component/高階コンポーネント)
を作成する関数です。
HOCは高階関数(higher order function)の考え方をコンポーネントに応用したパターンでReactの知識です。
合成関数やら高階関数などのワードが当たり前のように出てきます。
まずは関数型言語から見ていきます。
第一級関数
JavaScriptでは関数を変数に代入したり、引数に渡したり、戻り値として返したり、値のように使える。
こうした言語の関数を第一級関数といいます。
// 変数に代入出来る
const func = function()
{
console.log('Hello World');
}
// 引数で渡せる
function f1(callback)
{
callback();
}
f1(func);
// 戻り値で返せる
function f2()
{
return function()
{
console.log('Hello Kurage');
}
}
f2()();
高階関数
関数のうち、
- 関数を引数で受け取る関数
- 関数を返す関数
を高階関数といいます。
さっきのf1()
とf2()
のことですね。
カリー化
カリー化の前に問題です!
- 三つの数値を渡すとその合計を返す関数を作ってください!
多分こんな感じの関数を定義するでしょう。
function f(x, y, z)
{
return x + y + z;
}
f(1000, 100, 10); // 1110
では次の問題です。
- 関数は引数を一つしか受け取ってはならない!
こんな条件を付けられたらどうします?
次のように一つの引数を受け取る関数を返す関数
を連鎖させていくと実現できます。
function f(x)
{
// fy
return function(y)
{
// fz
return function(z)
{
return x + y + z;
}
}
}
f(1000)(100)(10) // 1110
ちょっと何やってるかわからないって人のためにf(1000)(100)(10)
を分かりやすく書くと、
const fy = f(1000); // function f(1000)を実行、戻り値は`fy`関数
const fz = fy(100); // `fy`関数実行、引数に100を渡す、戻り値は`fz`関数
const va = fz(10); // `fz`関数実行、引数に10を渡す、戻り値はこれまでの引数の和
console.log(va); // 1110
こんな感じになります。
m9(`Д´) 引数はいつも一つ!
これがカリー化です。
ただfunction
を使うと関数型に慣れてないと分かりやすいものの、ちょっとむさくるしいのでアロー関数で書き直します。
const f = x => y => x => x + y + z;
f(1000)(100)(10); // 1110
やっていることはさっきと全く一緒ですがスッキリします。
これがなんの役に立つかというと部分適用です。
部分適用
カリー化した関数の引数を全部渡す必要ないよね!
function f(x)
{
// fy
return function(y)
{
// fz
return function(z)
{
return x + y + z;
}
}
}
const fz = f(1000)(100); // fz関数を返す(すでに引数のx=1000, y=100が確定している。
fz(10); // 1110 ... 1000 + 100 + 10
fz(11); // 1111 ... 1000 + 100 + 11
fz(12); // 1112 ... 1000 + 100 + 12
fz(1122) // 2222 ... 1000 + 100 + 1122
さて、
f(1000)(100);
この時点で既にx
とy
の値はそれぞれ1000
と100
に確定している。
fz(z)
関数は
1000 + 100 + z
を返す関数になるわけだ。
何が役に立つのか・・・。
実例考えるのめっちゃ面倒なので適当な例。
例えばデータベースに接続する関数を想定します。
DBの種類、IP,、ポート、パスワード、IDが必要だとします。
大体次のようなカリー化した関数だとします()。
const dbConnect => type => IP => Port => id => pass =>
{
const dc = new Db(`type=${type}; IP=${IP}; Port={$Port}`);
return dc.connect(id, pass);
}
使うときはこんな感じになりますが、
dbConnect(type)(IP)(Port)(id)(pass)
このうち、DBの種類、IPとPortは固定しているとします。
const mc = dbConnect('MySQL')('***.***.**.*')(3306)
mc
を作っておくと、後はIDとパスワードを渡せばコネクションを取得出来ます
const conKurage = mc('kurage')('biribiri);
const conHitode = mc('hitode')('kirakira);
柔軟に切り替えられるようになります。
このコードどういう状況? ってツッコミは無しでお願いします。
合成関数
次のようなコードがあったとします。
const x = p => p - 10;
const y = p => p * 2;
const z = p => p + 1;
x( y( z( 100 ) ) ); // 192
z( 100 )の戻り値をy()に渡して、さらのその戻り値をx()に渡すという単純なコードです。
x( y( z( 100 ) ) );
z() -> y() -> x()
の順に実行される。
このような別の関数を呼び出すような関数を合成関数というようです。
これってやっていることは次のようなことだよね
const zv = z(100);
const yv = y(zv);
const xv = x(yv);
xy; // 192
特徴は戻り値と引数が同じ型!
特徴は戻り値と引数が同じ型!
特徴は戻り値と引数が同じ型!
さて、x( y( z( 100 ) ) )
だとダルいわ!
もっと簡単に出来る(関数の順番を決めて順に実行する)でしょう!
そこでcompose()
関数を作ります。
const x = p => p - 10;
const y = p => p * 2;
const z = p => p + 1;
const compose = (...funcs) => x =>
{
let param = x;
for(const func of funcs.reverse())
{
param = func(param); // 特徴は戻り値と引数が同じ型!
}
return param;
}
compose(x, y, z)(100); // 192
今回自作したcompose()関数は渡された関数の配列を逆順(reverse())で実行していきます。
compose(x, y, z)(100)
この実行結果は以下と同じです。
x( y( z( 100 ) ) );
100をz()
に渡し、その戻り値をy()
に渡し、さらにその戻り値をz()
に渡した結果を取得します。
一旦合成関数f()
を作っておき、後からいろんな引数を渡す例です。
const f = compose(x, y, z);
f(100);
f(200);
f(300);
さてこの繰り返し、reduceを使ってもっと短縮できますよね。
const compose = (...funcs) => x =>
{
return funcs.reduceRight((param, current) => current(param), x);
}
高階コンポーネントの前に
高階コンポーネントの前に単純なJavaScriptを使ってイメージしてみましょう。
テキストを出力する前に装飾したいとします。
CSSにそれぞれ.italic
, .bold
, .color
を定義していたとします。
次のような関数を定義しました。
const compose = (...funcs) => x =>
{
return funcs.reduceRight((param, current) => current(param), x);
}
function italic(value)
{
return `<span class="italic">${value}</span>`;
}
function bold(value)
{
return `<span class="bold">${value}</span>`;
}
function color(value)
{
return `<span class="dark">${value}</span>`;
}
const stylist = compose(italic, bold, color);
const x1 = stylist('Hello World');
const x2 = stylist('Hello Kurage');
結果は
<span class='italic'><span class='bold'><span class='dark'>Hello World</span></span></span>
<span class='italic'><span class='bold'><span class='dark'>Hello Kurage</span></span></span>
stylist()
はcolorで囲まれ、boldで囲まれ、italicで囲まれる関数を返します。
stylist('Hello World');
は
italic( bold( color ( 'Hello World' ) ) )
と同じことになります。
これがHTML上どうなのとかいうのはおいておきます。
ところでこれらの関数italic()
, bold()
, color()
は内容は同じことやってますよね。
span
にclass
を追加しているだけです。
これらは一つの関数に纏めることが出来るはずです。
クラス名を自由に渡せる関数を作るにはどうすればいいでしょうか?
試しに、
function createStyle(value, className)
{
return `<span class="${className}">${value}</span>`;
}
ではどうでしょう?
これではうまくいかないことでしょう。
実際に試行錯誤してみてください!
ではどうする?
関数を返す関数を作ります。
function createStyle(className)
{
return function(value)
{
return `<span class="${className}">${value}</span>`;
}
}
これでさっきのitalic()
, bold()
, color()
を簡単に作れるようになります。
const compose = (...funcs) => x =>
{
return funcs.reduceRight((param, current) => current(param), x);
}
function createStyle(className)
{
return function(value)
{
return `<span class="${className}">${value}</span>`;
}
}
const italic = createStyle('italic');
const bold = createStyle('bold');
const color = createStyle('color');
const stylist = compose(italic, bold, color);
const x1 = stylist('Hello World');
const x2 = stylist('Hello Kurage');
console.log(x1);
console.log(x2);
さらに直接compose()
に直接渡して使うやり方です。
const stylist = compose(
italic,
bold,
createStyle('hover'),
color
);
<span class='italic'><span class='bold'><span class='hover'><span class='color'>Hello World</span></span></span></span>
<span class='italic'><span class='bold'><span class='hover'><span class='color'>Hello Kurage</span></span></span></span>
さらに、クラス名だけでなくエンティティ名も変えてみます。
function createStyle(className, tagName='span')
{
return function(value)
{
return `<${tagName} class="${className}">${value}</${tagName}>`;
}
}
const italic = createStyle('italic');
const bold = createStyle('bold', 'p');
const color = createStyle('color');
const stylist = compose(
italic,
bold,
createStyle('hover', 'div'),
color
);
<span class='italic'><p class='bold'><div class='hover'><span class='color'>Hello World</span></div></p></span>
<span class='italic'><p class='bold'><div class='hover'><span class='color'>Hello Kurage</span></div></p></span>
span
p
div
span
の順でネストしていることが確認出来ます。
こうした関数型の考え方は慣れるまでが大変です。
このページではアロー関数を使わず極力function
を使うようにしてます。
いろいろ練習してみてください。
イメージ的には、ケーキ工場みたいですね。
ラインにスポンジが流れてきたらその上にクリームが塗る人がいて、
さらにその上にイチゴを載せる人がいて、
さらに板チョコ載せる人がいて、
みたいな。
const line = compose(
流れてきたケーキの上に板チョコを載せる返す関数を返す,
流れてきたケーキの上にイチゴを載せて返す関数を返す,
流れてきたケーキにクリームを載せて返す関数を返す,
);
const cake = line('スポンジ');
ちょっと分かりにくいかな?
高階コンポーネント
さっきの例からReactの高階コンポーネントの使い方を見ていきます。
コンポーネントを受け取ってコンポーネントを返す関数
を定義していきます。
コンポーネント自体がprops
を受け取ってJSXエレメント
を返す関数なのでちょっとややこしくなります。
前のページのコードを流用します。
import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
const withItalic = Component =>
{
return props =>
{
props.style = {...props.style, fontStyle: 'italic'}
return <Component {...props} />
}
}
const withBold = Component =>
{
return props =>
{
props.style = {...props.style, fontWeight: 'bold'}
return <Component {...props} />
}
}
const withColouring = Component =>
{
return props =>
{
props.style = {...props.style, color: 'red' }
return <Component {...props} />
}
}
const Text = props => (<p style={props.style}>{props.message}</p>)
const ItalicText = withItalic(Text);
const BoldText = withBold(Text);
const ColouringText = withColouring(Text);
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
return (
<div {...useBlockProps()}>
<Text message="Hello World" />
<ItalicText message="Hello Italic" />
<BoldText message="Hello Bold" />
<ColouringText message="Hello Red!" />
</div>
);
}
HOC + Compose
実はWordPressにもcompose()
があります。
今回はそっちを使っていきましょう。
import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { compose } from '@wordpress/compose';
const withItalic = Component =>
{
return props =>
{
props.style = {...props.style, fontStyle: 'italic'}
return <Component {...props} />
}
}
const withBold = Component =>
{
return props =>
{
props.style = {...props.style, fontWeight: 'bold'}
return <Component {...props} />
}
}
const withColouring = Component =>
{
return props =>
{
props.style = {...props.style, color: 'red' }
return <Component {...props} />
}
}
const Text = props => (<p style={props.style}>{props.message}</p>)
const stylist = compose(
withItalic,
withBold,
withColouring
);
const AllStyleText: any = stylist(Text);
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
return (
<div {...useBlockProps()}>
<Text message="Hello World" />
<AllStyleText message="Hello World" />
</div>
);
}
高階関数を作成する関数
上記のwithColouring()
に引数を渡したいので変更しました。
const withColouring = color => Component =>
{
return props =>
{
props.style = {...props.style, color }
return <Component {...props} />
}
}
const stylist = compose(
withItalic,
withBold,
withColouring('blue')
);
階層を一つ増やしています。
const withColouring = color => Component =>
分かりにくい方はfunction
で書き直して見てください。
呼び出すときはこのようにしてます。
withColouring('blue')
コンポーネントを受け取ってコンポーネントを返す関数
を作成する関数になりました。
props
に変更を与えたり、コンポーネントをラップしたり応用が出来ます。
WordPressのcompose
さて、ここでようやくWordPressの話になります。
すでにcompose()
の話は出たので説明する必要がありません。
WordPressにはWith***
と行った関数がいくつか用意してあります。
withSelect
やwithDispatch
などいかにもな物やifCondition
などがあります。
withSelect
やwithDispatch
については現段階では説明出来ないので
とりあえず単純なifCondition
を試してみますか。
import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { compose, ifCondition } from '@wordpress/compose';
const validComponent = compose(
ifCondition(props => props.age >= 20),
ifCondition(props => props.id.length >= 3)
)
const MyComponent = ({age, id}) => (<p>{id}: {age}</p>);
const ValidMyComponent: any = validComponent(MyComponent);
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
return (
<div {...useBlockProps()}>
<MyComponent id="yamamoto" age={19} />
<ValidMyComponent id="yamada" age={22} />
<ValidMyComponent id="me" age={30} />
<ValidMyComponent id="sato" age={53} />
<ValidMyComponent id="taro" age={13} />
</div>
);
}
ifCondition
の引数はpropsを受け取り論理値を返します。
この関数はpropsを与えてfalseが帰ってきたらコンポーネントをnullで返すやつです。
つまりid
の文字列が3文字以上、かつage
が20以上の時はコンポーネントは表示されますが、そうでなければ表示されません。
例で取り上げただけでバリデーションするための関数というわけでは無いので注意してください。
createHigherOrderComponent()
よく見かけるこの関数ですが、これは中身を見た方が早いかもしれません。
export function createHigherOrderComponent<
TInner extends ComponentType< any >,
TOuter extends ComponentType< any >
>( mapComponent: ( Inner: TInner ) => TOuter, modifierName: string ) {
return ( Inner: TInner ) => {
const Outer = mapComponent( Inner );
Outer.displayName = hocName( modifierName, Inner );
return Outer;
};
}
const hocName = ( name: string, Inner: ComponentType< any > ) => {
const inner = Inner.displayName || Inner.name || 'Component';
const outer = pascalCase( name ?? '' );
return `${ outer }(${ inner })`;
};
第一引数はコンポーネントを受け取ってコンポーネントを返す関数
ですが、
この関数をラップして返すコンポーネントにdisplayNameを生やす
機能に拡張したものです。
使ってみた方が分かりやすそうなので、
import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { wordpress, alignNone, addCard, download, external } from '@wordpress/icons';
import { getPlugin, getPlugins, PluginArea, registerPlugin } from '@wordpress/plugins';
import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
import { createSlotFill } from '@wordpress/components';
import { compose, createHigherOrderComponent } from '@wordpress/compose';
const withItalic = Component =>
{
return props =>
{
props.style = {...props.style, fontStyle: 'italic'}
return <Component {...props} />
}
}
const withBold = Component =>
{
return props =>
{
props.style = {...props.style, fontWeight: 'bold'}
return <Component {...props} />
}
}
const withColouring = Component =>
{
return props =>
{
props.style = {...props.style, color: 'red' }
return <Component {...props} />
}
}
const withItalicEx = createHigherOrderComponent(
withItalic,
'MyItalic'
);
const withBoldEx = createHigherOrderComponent(
withBold,
'MyBold'
);
const withColouringEx = createHigherOrderComponent(
withColouring,
'MyColouring'
);
const Text = props => <p style={props.style}>{props.message}</p>;
// TestコンポーネントにdisplayNameを生やす
Text.displayName = "I am Text!";
const stylist = compose(
withItalicEx,
withBoldEx,
withColouringEx
);
const AllStyleText: any = stylist(Text);
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
return (
<div {...useBlockProps()}>
<AllStyleText message="Hello Kurage" />
</div>
);
}
const ColouringWrap = withColouringEx(Text);
const BoldWrap = withBoldEx(ColouringWrap);
const ItalicWrap = withItalicEx(BoldWrap);
// createHigherOrderComponent()でラップしたオブジェクトのdisplayNameを呼ぶ
[ColouringWrap, BoldWrap, ItalicWrap]
// @ts-ignore
.forEach(_ => console.log(_.displayName));
まず注目するのはcreateHigherOrderComponent()
を使ってラップしてる箇所です。
const withItalicEx = createHigherOrderComponent(...)
withItalic
をwithItalicEx
に拡張しました。
withItalicEx
を実行して返ってきたコンポーネントにはdisplayName
が生えてます。
他も同様に生やしていきます。
そして生やしたものどうしで再び合成関数を作成します。
const stylist = compose(
withItalicEx,
withBoldEx,
withColouringEx
);
画面の表示に変わりはありません。
ではどこが変わったのか!?
コンポーネントツリーを見てみると
ブロック名で確認しやすくなってます。
次にコンソールログを見てください。
MyColouring(I am Text!)
MyBold(MyColouring(I am Text!))
MyItalic(MyBold(MyColouring(I am Text!)))
上の階層は下の階層のdisplayName
を含んでいることが確認できます。
そろそろ疲れてきたので結果は載せませんが
console.log(ColuringWrap.displayName)
等も試してみてください。
注意する点
実はText.displayName
を生やすタイミングが変わると他のdisplayName
にも影響します。
もしText.displayName
をコードの下らへんに設定するとこうなります。
ifConditionとcreateHigherOrderComponent()
icCondition
も内部でcreateHigherOrderComponent()
を使用してます。
function ifCondition< Props extends {} >(
predicate: ( props: Props ) => boolean
) {
return createHigherOrderComponent(
( WrappedComponent: ComponentType< Props > ) => ( props: Props ) => {
if ( ! predicate( props ) ) {
return null;
}
return <WrappedComponent { ...props } />;
},
'ifCondition'
);
}
次回はフックについてです。