React Helmet のソースコードを読んでみたので、そのメモ。
間違い等がありましたら、ご指摘お願いします。
React Helmet とは?
React で ヘッドタグを操作したい時に使う ライブラリ。
使い方は、特定のコンポーネントで、
import {Helmet} from "react-helmet"
// 省略
const hoge = () => {
return(
<Helmet>
<title>hoge</title>
<meta name="description" content="hoge"/>
</Helmet>
// 省略
)
}
を加えるだけ。これで、 の中の や が head タグ内に加わります。
では、読んでみましょう!
React Helmet のコード
こちらのソースコード を見ても分かるように、src フォルダ には たったの3つしかファイルはありません。
量だけなら簡単に読めそうですね。
まずは、Helmet.js から読んでみましょう。
import React from "react";
import PropTypes from "prop-types";
import withSideEffect from "react-side-effect";
// 省略
const Helmet = Component =>
class HelmetWrapper extends React.Component {
// 省略
render() {
const {children, ...props} = this.props;
let newProps = {...props};
if (children) {
newProps = this.mapChildrenToProps(children, newProps);
}
return <Component {...newProps} />;
}
const NullComponent = () => null;
const HelmetSideEffects = withSideEffect(
reducePropsToState,
handleClientStateChange,
mapStateOnServer
)(NullComponent);
const HelmetExport = Helmet(HelmetSideEffects);
const Helmet = Component => class HelmetWrapper extends React.Component
という変わった書き方になっていますが、これは Higher-order Components というものになり、HelmetSideEffects の機能を使うための実装になります。
この HelmetSideEffects の中身の withSideEffect
のソースコードはこちらにありますが、Helmet のコードで 簡単に言えば、
1:reducePropsToState
で 受け取った props を state に reduce し、
2:reducePropsToState
で作った state (withSideEffect内部のみで分かる形) を受け取り、クライアントが動いているなら(Dom が使えるなら = canuseDom) handleClientStateChange
を、サーバーで動いているなら (Dom が使えないなら) mapStateOnServer
を実行し、state を view に反映させるなどしています。
ここで出てきた reducePropsToState
と handleClientStateChange
と mapStateOnServer
が書いてあるファイルが、HelmetUtils.js になります。
しかし withSideEffect
で最初に実行される reducePropsToState
を動かすには、<Helmet></Helmet>
の中にある要素 ( Children ) を Props にしてコンポーネントに渡す必要があります。
この Children を Props に渡す動作をしているのが、Helmet.js です。
内容は割愛しますが、上のコードの
if (children) {
newProps = this.mapChildrenToProps(children, newProps);
}
部分の this.mapChildrenToProps(children, newProps)
が、children と props を結合させている実装です。
その後、children が Props と結合した後で 呼び出されるのが、reducePropsToState
と handleClientStateChange
(とサーバーサイドなら mapStateOnServer
) です。
まずは、受け取った Props を state に変換する reducePropsToState
を見てみましょう。
const reducePropsToState = propsList => ({
baseTag: getBaseTagFromPropsList(
[TAG_PROPERTIES.HREF, TAG_PROPERTIES.TARGET],
propsList
),
bodyAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.BODY, propsList),
defer: getInnermostProperty(propsList, HELMET_PROPS.DEFER),
encode: getInnermostProperty(
propsList,
HELMET_PROPS.ENCODE_SPECIAL_CHARACTERS
),
htmlAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.HTML, propsList),
linkTags: getTagsFromPropsList(
TAG_NAMES.LINK,
[TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF],
propsList
),
metaTags: getTagsFromPropsList(
TAG_NAMES.META,
[
TAG_PROPERTIES.NAME,
TAG_PROPERTIES.CHARSET,
TAG_PROPERTIES.HTTPEQUIV,
TAG_PROPERTIES.PROPERTY,
TAG_PROPERTIES.ITEM_PROP
],
propsList
),
// 省略
scriptTags: getTagsFromPropsList(
TAG_NAMES.SCRIPT,
[TAG_PROPERTIES.SRC, TAG_PROPERTIES.INNER_HTML],
propsList
),
styleTags: getTagsFromPropsList(
TAG_NAMES.STYLE,
[TAG_PROPERTIES.CSS_TEXT],
propsList
),
title: getTitleFromPropsList(propsList),
titleAttributes: getAttributesFromPropsList(
ATTRIBUTE_NAMES.TITLE,
propsList
)
});
見れば分かるように、連想配列を返していますが、その key に linkTags・metaTags・scriptTags・styleTags・title があるのが読めますね。
ここでは、それぞれの連想配列の key で props から 該当するattribute (href や relなど) がある要素(titleやlinkやmetaなど) のみを取り出す関数を呼び出しています。
ここまでで、withSideEffect
の最初に段階 reducePropsToState
が読めました。
次は、handleClientStateChange
(とサーバーサイドなら mapStateOnServer
) です。ここでは、state を view に反映させる = head に title や meta や link を書き込むため、canUseDom でクライアントが動かせる場合に動く handleClientStateChange
だけを見てみます。
ソースコードの該当箇所は、こちらです。
const handleClientStateChange = newState => {
if (_helmetCallback) {
cancelAnimationFrame(_helmetCallback);
}
if (newState.defer) {
_helmetCallback = requestAnimationFrame(() => {
commitTagChanges(newState, () => {
_helmetCallback = null;
});
});
} else {
commitTagChanges(newState);
_helmetCallback = null;
}
};
newState という引数に、reducePropsToState
で返している連想配列が入ります。
newState に defer があろうとなかそうと、commitTagChanges を実行していますね。では、この commitTagChanges を見てみましょう。
const commitTagChanges = (newState, cb) => {
const {
baseTag,
bodyAttributes,
htmlAttributes,
linkTags,
metaTags,
noscriptTags,
onChangeClientState,
scriptTags,
styleTags,
title,
titleAttributes
} = newState;
updateAttributes(TAG_NAMES.BODY, bodyAttributes);
updateAttributes(TAG_NAMES.HTML, htmlAttributes);
updateTitle(title, titleAttributes);
const tagUpdates = {
baseTag: updateTags(TAG_NAMES.BASE, baseTag),
linkTags: updateTags(TAG_NAMES.LINK, linkTags),
metaTags: updateTags(TAG_NAMES.META, metaTags),
noscriptTags: updateTags(TAG_NAMES.NOSCRIPT, noscriptTags),
scriptTags: updateTags(TAG_NAMES.SCRIPT, scriptTags),
styleTags: updateTags(TAG_NAMES.STYLE, styleTags)
};
// 省略
};
見れば分かるように、body・html タグでは updateAttributes
を、titleタグでは updateTitle
を、その他のタグでは updateTags
を実行しています。
それぞれの実装を見たら、setAttribute
や document.title=
や appendChild
があると思います。
ここまでで、React-Helmet の簡単な流れを把握することができました。
withSideEffect
など一見難しそうに見える実装もありましたが、中身を見ると意外と読めますね。