クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【React】JSONデータをJSXに読み込んで表示する(Reactとメタ言語の比較-3)

Reactの続きです。前回はreact-helmetでheadタグの中身を動的に打ち替えました。今回はHTMLのテキスト部分を外部JSONデータからJSXに読み込みます。インラインのタグを認識するためにdangerouslySetInnerHTMLを、JSX内でループするためにmap()を使いました。それではいきましょう!

【目次】

※参考:前回記事
【React】react-helmetでheadタグの中身を動的に打ち替える(Reactとメタ言語の比較-2) - クモのようにコツコツと

※参考:Reactを習得するためにやったことまとめ
qiita.com

前回のおさらい

Appコンポーネントを作成し、そこにreact-helmetをインポート。headタグの中身を動的にに打ち替えた!

headタグの元のソース https://cdn-ak.f.st-hatena.com/images/fotolife/i/idr_zz/20210204/20210204071311.jpg

react-helmetで打ち替えたheadタグ https://cdn-ak.f.st-hatena.com/images/fotolife/i/idr_zz/20210204/20210204071212.jpg

※参考:【React】react-helmetでheadタグの中身を動的に打ち替える(Reactとメタ言語の比較-2) - クモのようにコツコツと

このheadタグのtitle、descriptionはheaderタグのh1、pタグとも共通するテキスト。そのため、以前、メタ言語スターターターキットのEJSでやったように外部JSONファイルからテキストを読み込みたい。

※参考:【メタ言語】フロントエンド開発スターターキットを作った(EJS、Sass(SCSS)、TypeScript) - クモのようにコツコツと

JSONファイルを作成(テキスト部分)

以前、メタ言語スターターキットのEJSでやったように、テキスト部分を外部のjsonファイルにする。


ここで疑問に思ったのは、jsonファイルは「public」フォルダか「src」フォルダ、どちらに置くべきか。

You may create subdirectories inside src. For faster rebuilds, only files inside src are processed by webpack. You need to put any JS and CSS files inside src, otherwise webpack won’t see th

JSファイルとCSSファイルをsrc内に配置する必要があります。そうしないと、webpackはそれらを認識しません。

webpackでバンドルして欲しいファイルは「public」フォルダではなく「src」でいいんだな。

※参考:Folder Structure | Create React App


「src」フォルダに「data.json」を作成。テキスト部分のデータを入れる。

{
    "data": {
        "header": {
            "title":"Reactとメタ言語の比較",
            "text": "以前作ったメタ言語スターターキットの内容をReact環境で再現してみる"
        },
        "main": {
            "title":"タイトルです",
            "text": "テキストです。テキストです。テキストですったらテキストです。"
        },
        "inner": [
            {
            "title": "CSS(文字色)",
            "text": "CSSでタイトルの文字色変更。"
            },
            {
            "title":"JS(文字列)",
            "text": "JSでテキストの文字列追加「<span class='inner__text--hello'></span>」"
            }
        ],
        "footer": {
            "text":"©️ react-from-meta-lang"
        }
    }
}

headerfooterなどを少し打ち替えたが、基本的にはEJSの時と同じ内容。

Appコンポーネントでheadタグのテキスト読み込み

前回、react-helmetでtitleとdescriptionを設定したAppコンポーネント。ここに先ほど作成した「data.json」のテキストデータを読み込む。

まず「data.json」をインポート

import React from 'react';
import {Helmet} from "react-helmet";
import Header from './Header';
import Main from './Main';
import Footer from './Footer';
import Data from './data.json'; // 追加

変数titleTextdescriptionTextを設定

const titleText = Data.data.header.title;
const descriptionText = Data.data.header.text;

前回はここにテキストを直で書いたが、data.jsonheaderキーのテキストを取得する。

Helmetの設定部分。前回と変更なし

function App() {
  return (
    <div className="App">
        <Helmet
            title={ titleText }
            meta={[
                { name: 'description', content: descriptionText }
            ]}
         />
        <Header />
        <Main />
        <Footer />
    </div>
  );
}

Appコンポーネントをエクスポート(変更なし)

export default App;

ローカル環境でアプリ起動

$ npm start

よし!headタグはちゃんと打ち変わっている! https://cdn-ak.f.st-hatena.com/images/fotolife/i/idr_zz/20210204/20210204071212.jpg

この調子で他のコンポーネントのJSX部分にもJSONデータを読み込む。

HeaderコンポーネントにJSONデータを読み込む

続いてHeaderコンポーネント(Header.tsx)。ここのテキストはheadタグと共通

「data.json」をインポート

import React from 'react';
import Data from './data.json'; // 追加

変数titletextを設定

const title = Data.data.header.title;
const text = Data.data.header.text;

パスは先ほどのheadタグと共通


JSXに読み込み

function Header() {
  return (
    <header className="header">
        <h1 className="header__title">{ title }</h1>
        <p className="header__text" dangerouslySetInnerHTML={{ __html: text }}></p>
    </header>
  );
}
  • h1タグにtitleを読み込む
  • pタグににtextを読み込む(dangerouslySetInnerHTML

詳細は後述するが本文のpタグにはインラインで入れ子のタグが入り得る。テキストとして読み込むとそこがタグにならなかった。

そのため、dangerouslySetInnerHTMLを使ってinnerHTML形式で読み込むことにした。

※参考:DOM 要素 – React

dangerouslyってすごい名前だがw下記の理由のため

dangerouslySetInnerHTML は、ブラウザ DOM における innerHTML の React での代替です。一般に、コードから HTML を設定することは、誤ってあなたのユーザをクロスサイトスクリプティング (XSS) 攻撃に晒してしまいやすいため、危険です。そのため、React では直接 HTML を設定することはできますが、それは危険であることを自覚するために dangerouslySetInnerHTML と入力し __html というキーを持つオブジェクトを渡す必要があります。

御意。


Headerコンポーネントをエクスポート(変更なし)

export default Header;

FooterコンポーネントにJSONデータを読み込む

本文のMainコンポーネントはちょっとややこしいので、先にシンプルなFooterコンポーネント(Footer.tsx)から。

「data.json」をインポート

import React from 'react';
import Data from './data.json'; // 追加

変数textでJSONデータからフッター部分のテキストを取得

const text = Data.data.footer.text;

textpタグで読み込む(innerHTML形式)

function Footer() {
  return (
    <footer className="footer">
        <p className="footer__text" dangerouslySetInnerHTML={{ __html: text }}></p>
    </footer>
  );
}

Footerコンポーネントをエクスポート(変更なし)

export default Footer;

Headerコンポーネントと同じ要領〜♪


ここまでやってブラウザを確認。 f:id:idr_zz:20210202065525j:plain 問題なさそう!見た目は変わらないがテキストはJSONデータが読まれている。

MainコンポーネントにJSONデータを読み込む

次は本文部分であるMainコンポーネント(Main.tsx)

「data.json」をインポート

import React from 'react';
import './hello.js';
import Data from './data.json'; // 追加

変数titletextでJSONデータを取得

const title = Data.data.main.title;
const text = Data.data.main.text;

h1タグとリード文のテキストをJSONデータから取得


function Main() {
  return (
    <main>
      <section className="main">
          <h1 className="main__title">{ title }</h1>
          <p className="main__title" dangerouslySetInnerHTML={{ __html: text }}></p>
          <section className="inner">
              <h2 className="inner__title">CSS(文字色)</h2>
              <p className="inner__text">CSSでタイトルの文字色変更。</p>
          </section>
          <section className="inner">
              <h2 className="inner__title">JS(文字列)</h2>
              <p className="inner__text">JSでテキストの文字列追加→「<span className='inner__text--hello'></span>」</p>
          </section>
      </section>
    </main>
  );
}
  • h1タグにtitleを読み込む
  • .main__titletextを読み込む

h2タグ階層のsectionタグはまだ直のテキストのまま。


Mainコンポーネントをエクスポート(変更なし)

export default Main;

Innerコンポーネントを作成

メタ言語スターターキットのEJSの時はh2階層のsectionタグは_innerモジュールに切り出していた。

<main>
    <section class="main">
        <h1 class="main__title"><%- main.title %></h1>
        <p class="main__text"><%- main.text %></p>
        <%- include('_inner', {inner: inner}); %>
    </section>
</main>

Reactでも同じことをしたい。まずInnerコンポーネント(Inner.tsx)を作成する。

Mainコンポーネントで読み込んでた「hello.js」をここで読み込む

import React from 'react';
import './hello';

Innerコンポーネント作成

function Inner() {
  return (
    <div className="inner">        
        <section className="inner__sec">
            <h2 className="inner__title">CSS(文字色)</h2>
            <p className="inner__text">CSSでタイトルの文字色変更。</p>
        </section>
        <section className="inner__sec">
            <h2 className="inner__title">JS(文字列)</h2>
            <p className="inner__text">JSでテキストの文字列追加→「<span className='inner__text--hello'></span>」</p>
        </section>
    </div>
  );
}

Mainコンポーネントのh2タグ階層部分のsectionをここに入れる。


Innerコンポーネントをエクスポート

export default Inner;

MainコンポーネントでInnerコンポーネントを読み込み

Mainコンポーネント(Main.tsx)でInnerコンポーネントを読み込む

Innerコンポーネントをインポート

import React from 'react';
import Inner from './Inner';
import Data from './data.json';

また、「hello.js」はここでは不要になったので削除する。


先ほどのh2タグ階層のsection部分にInnerコンポーネントを配置する。

function Main() {
  return (
    <main>
      <section className="main">
          <h1 className="main__title">{ title }</h1>
          <p className="main__title" dangerouslySetInnerHTML={{ __html: text }}></p>
          <Inner />
      </section>
    </main>
  );
}

スリムになった♪


ブラウザ確認 f:id:idr_zz:20210202065525j:plain 問題なさそう!

InnerコンポーネントにJSONデータを読み込む(map()でループ)

メタ言語スターターキットのEJSではh2タグ階層のsectionタグを「_innerモジュール」に分けてfor文でループしていた。

<% for (var i = 0; i < inner.length; i++) { %>
        <section class="inner">
            <h2 class="inner__title"><%- inner[i].title %></h2>
            <p class="inner__text"><%- inner[i].text %></p>
        </section>
<% } %>

※参考:【メタ言語】フロントエンド開発スターターキットを作った(EJS、Sass(SCSS)、TypeScript) - クモのようにコツコツと


Reactでも同じことをしたい。

まず「data.json」をインポート

import React from 'react';
import './hello';
import Data from './data.json';

変数innerJsonで本文を取得

const innerJson = Data.data.inner;

innerキーまでを取得するので値は配列になる。


innerJsonmap()でループする

function Inner() {
  return (
    <div className="inner">
      {innerJson.map((innerJson, index) => 
        <section key={ index }>
          <h2 className="inner__title">{ innerJson.title }</h2>
          <p className="inner__text" dangerouslySetInnerHTML={{ __html: innerJson.text }}></p>
        </section>
      )}
    </div>
  );
}

以前こちらでReactのいろいろなループ方法を試したが、やはりmap()を使ったループはJSXの中に直接書けるのがメリットに感じた!

※参考:【React】ループの書き方(for文エラー回避、配列、map()) - クモのようにコツコツと

こちらの記事を書かれたJJさんからいただいたリプ

※参考:React.jsでループするには。 - Qiita


ブラウザ確認 f:id:idr_zz:20210202065525j:plain よっしゃ!問題なさそう!

おまけ:text型式だとインラインのタグが認識されない

ちなみにpinnerHTML型式ではなくテキスト型式で読み込むと…

function Inner() {
  return (
    <div className="inner">
      {innerJson.map((innerJson, index) => 
        <section key={ index }>
          <h2 className="inner__title">{ innerJson.title }</h2>
          <p className="inner__text">{ innerJson.text }</p>
        </section>
      )}
    </div>
  );
}

「Hello.js」でエラーが起こる。 f:id:idr_zz:20210208062725j:plain

TypeError: Cannot set property 'innerHTML' of null

プロパティにinnerHTMLが設定できませんと。プロパティ(セレクター)が認識されていない。


「hello.js」インポートを無効にしてみると…

// import './hello';

おっと、、タグがテキストとして表示された! f:id:idr_zz:20210208063246j:plain なるほど、これは認識されないわけだ。

ということでインラインのタグが入り得る本文部分はdangerouslySetInnerHTMLinnerHTML型式で統一した。

※参考:DOM 要素 – React


ソース(GitHub)※今回のコミットまで

※参考:GitHub - ryo-i/react-from-meta-lang at b5806001337be58355f4843de94666397ab52857

プレビュー(GitHub Pages)

※参考:Reactとメタ言語の比較

最後に

ということで、メタ言語スターターキットのEJSの時と同様にJSONデータからテキストを読み込むことができました。webpackでバンドルするファイルは「public」ではなく「src」フォルダでいいことがわかったのでjsonデータは「src」フォルダに追加しました。

また、本文のインラインのタグはtext型式だと認識されなかったため、dangerouslySetInnerHTMLinnerHTML型式で読み込む必要があるとわかりました。

h2階層のsectionもEJSの時と同様にループすることができました。JSX内ではfor文を直接書くとエラーになるたため、JSX内で書けるmap()を使うのがやはり楽に感じました。

次回はメタ言語スターターキットのSass(SCSS)部分をCSS in JSに置き換えてみたく思います。スタイルがコンポーネントに直接書いていくのでclass名がどうなるか興味津々です♪

それではまた!


※参考:Reactを習得するためにやったことまとめ
qiita.com