react-markdown(v5)で外部リンクのみ別タブで開く
はじめに
react-markdown(v5)で外部リンクのみ別タブで開くために、rendarerを自作していきます。
※メジャーアップデートによりv6ではここに書かれている内容とは仕様が変わっています。
目次
react-markdown とは
react-markdown は、マークダウンで書かれた文章を react コンポーネントとしてレンダリングするためのライブラリです。
事前準備
説明のため、create-react-app を使用して環境を作成していきます。
インストール
npm を使用し、create-react-app と react-markdown のパッケージをインストールします。
ローカルにインストールしたパッケージを簡単に実行するため、npx パッケージもインストールしておきます。
1$ npm i -D create-react-app react-markdown npx
npx create-react-app
コマンドを実行し、markdown-test
というプロジェクトを作成します。ここでは、--typescript
オプションを使用していますが、今回の目的には関係ないので、つけてもつけなくてもどっちでもいいです。
1$ npx create-react-app markdown-test --typescript
markdown-test をいう名前のディレクトリが作成され、その中に react のプロジェクトの雛形が作成されます。今回は、src ディレクトリ内の App.tsx を修正していきます。
1markdown-test 2 ┗ src 3 ┗ App.tsx
App.tsx の変更
作成された App.tsx ファイルの中身は以下のようになっています。
App.tsx1import React from 'react'; 2import logo from './logo.svg'; 3import './App.css'; 4 5function App() { 6 return ( 7 <div className="App"> 8 <header className="App-header"> 9 <img src={logo} className="App-logo" alt="logo" /> 10 <p> 11 Edit <code>src/App.tsx</code> and save to reload. 12 </p> 13 <a 14 className="App-link" 15 href="https://reactjs.org" 16 target="_blank" 17 rel="noopener noreferrer" 18 > 19 Learn React 20 </a> 21 </header> 22 </div> 23 ); 24} 25 26export default App;
これを今回の内容に合わせるため、以下のように編集します。
const markdown = `.....`
内にマークダウン記法でかかれた文章を、ReactMarkdown の source に受け渡して表示するだけの簡単な画面になります。
App.tsx1import React from 'react'; 2import ReactMarkdown from 'react-markdown'; 3 4const markdown = `# markdown test 5 6[google](https://google.co.jp) 7 8[about](/about) 9`; 10 11function App() { 12 return ( 13 <div className="App"> 14 <ReactMarkdown source={markdown} /> 15 </div> 16 ); 17} 18 19export default App;
この状態で、npm run start
コマンドを実行すると、ブラウザが起動し以下の画面が表示されます。
この状態で、google
のリンクをクリックすると現在のタブに google のページが表示されます。今回は、これを別タブで開くようにします。
※ about
のリンクはページを作成していないので、この状態でクリックしても画面の内容は変わりません。
linkTarget オプション
react-markdown には linkTarget
というオプションがあります。ここに以下のように指定すれば、リンクが別タブで開くようになりますが、全てのリンクが別タブで開いてしまうため、今回の目的には使用できません。
App.tsx1<ReactMarkdown source={markdown} linkTarget={"_blank"} />
renderer を自作する
外部リンクのみ別タブで開くようにするために、link renderer を自作します。今回、外部リンクの判定は url に http という文字列が含まれる場合とします。以下のコードを App.tsx に追加します。
App.tsx1const linkBlock = (props: { href: string; children: React.ReactNode }) => { 2 const { href, children } = props; 3 4 if (href.match('http')) { 5 return ( 6 <a href={href} target="_blank" rel="noopener noreferrer"> 7 {children} 8 </a> 9 ); 10 } 11 return <a href={href}>{children}</a>; 12};
上記で作成した linkBlock を renderers の link に指定します。
App.tsx1<ReactMarkdown 2 source={markdown} 3 renderers={{ 4 link: linkBlock, 5 }} 6/>
この状態で、ブラウザの google
のリンクをクリックすると別タブで開くようになります。about
のリンクをクリックしても現在のタブに読み込まれます。
Single Page Application (SPA) の場合
SPA の場合、リンクが <a>
タグのため、内部リンクであっても画面が再読み込みされてしまいます。そのため、内部リンクの部分を react-router-dom の Link に変更します。
App.tsx1import { Link } from 'react-router-dom'; 2 3const linkBlock = (props: { href: string; children: React.ReactNode }) => { 4 const { href, children } = props; 5 6 if (href.match('http')) { 7 return ( 8 <a href={href} target="_blank" rel="noopener noreferrer"> 9 {children} 10 </a> 11 ); 12 } 13 // ページ内リンク 14 if (href.slice(0, 1) == '#') { 15 return <a href={href}>{children}</a>; 16 } 17 return <Link to={href}>{children}</Link>; 18};
これで内部リンクをクリックしても、ページが再読み込みされることなく内容が再描画されます。
next.js の場合
next.js の next/link
を使用する場合、children をそのまま使用すると以下のようなエラーが表示されます。
1Error: React.Children.only expected to receive a single React element child.
next/link
の children は単一の element しか受け取れないにもかかわらず、props で渡される children は以下のように配列になっているためです。
1[ { '$$typeof': Symbol(react.element), 2 type: [Function: TextRenderer], 3 key: 'text-5-2-0', 4 ref: null, 5 props: { nodeKey: 'text-5-2-0', children: 'about', value: 'about' }, 6 _owner: null, 7 _store: {} } ]
そのため、next/link
に渡す値に、children の先頭の element を渡すようにします。
index.js1import Link from 'next/link'; 2 3const linkBlock = (props) => { 4 const { href, children } = props; 5 6 if (href.match('http')) { 7 return ( 8 <a href={href} target="_blank"> 9 {children} 10 </a> 11 ); 12 } 13 14 let child = children; 15 if (Array.isArray(children) && children.length == 1) { 16 child = children[0]; 17 } 18 // ページ内リンク 19 if (href.slice(0, 1) == '#') { 20 return <a href={href}>{child}</a>; 21 } 22 return ( 23 <Link href={href}> 24 {child} 25 </Link> 26 ); 27};
これでエラーにならずに内部リンクが表示されます。