20211011のReactに関する記事は8件です。

React初心者がReactについて適当にまとめる

都内のSESで働いている人がReactについて簡単にまとめた記事です。 なかなかプログラミングは得意じゃないですが得意じゃないなりに頑張ってますので色々ご教授いただけると幸いです。。 ※この記事ではCodeSandboxを使用して学習したものを載せています。 Reactって? Reactの公式HPを見てもらったらわかると思いますがユーザーインターフェース構築のためのJavaScriptライブラリです。 よく似たものとしてフレームワークというものがありますがフレームワークではありません。(Angularとか?) コンポーネントというものを作成してユーザーインターフェースを効率的に構築できる。 「ReactはJSでユーザーインタフェースを効率よく構築できるよ!!」ってことだと思います。笑 JSX記法 JSX記法はReactの大きな特徴だと思います。 JSX記法とはJavaScript内でHTMLを記述する方法です。 例 まずindex.htmlのどの部分にReactを差し込むかを記す目印を書いてあげます。 index.html <!DOCTYPE html> <html lang="en"> <head> <!-- 省略 --> </head> <body> <!-- どの部分にReactを差し込むかを刺す目印 --> <div id="root"></div> </body> </html> 次にindex.jsで差し込みたい内容をアロー関数で用意してあげます。 index.js import Reactdom from "react-dom"; const App = () => { return <h1>こんにちわ</h1> //・・・① }; ReactDom.render(<App />, document.getElementById("root")); //・・・② 上記の書き方で画面にはこんにちわと表示されます。 簡単な説明 ①Appという名前の関数を用意してその戻り値としてHTMLタグを戻す。 ②ReactDomのrender関数を使って指定の場所に表示する render(render対象,render箇所); 上記の例で第一引数が<App />となっていますが、Reactでは関数名をHTMLのようにタグで囲むことによってコンポーネントとして扱うことができます。 JSXのルール return文のあとが複数行になる場合は()で囲ってあげる必要があります。 index.js import react-dom from "ReactDom"; const App = () => { return (           <h1>こんにちわ</h1> <p>Hello</p> ); }; ReactDom.render(<App />, document.getElementById("root")); ()で囲うだけだとエラーになってしまいます。 ルールとして return以降は一つのタグで囲われていなければならないというのがあります。 解決方法は3通りあります。 ①divタグで囲む方法 const App = () => { return ( <div>           <h1>こんにちわ</h1> <p>Hello</p> </div> ); }; ②Reactに用意されているFragmentをimportして使う方法 import {Fragment} from "react"; const App = () => { return ( <Fragment>           <h1>こんにちわ</h1> <p>Hello</p> </Fragment> ); }; ③空のタグで囲む方法 const App = () => { return ( <>           <h1>こんにちわ</h1> <p>Hello</p> </> ); }; ②と③はdivタグとは違ってDOMが生成されないのでエラーを回避するためだけに外を囲みたい場合などは有効手段になります。 コンポーネントって? index.jsにこれまで書いていましたが何画面も書くとなると何千行とかになってしまいますよね。(想像しただけでも怖い笑) React開発では画面の要素を様々な粒度のコンポーネントに分割することで再利用性や保守性を高めるのが基本になります。 コンポーネント化ってどうやるの? まず、App.jsを作成します。 先ほど作成したindex.js内で記述した関数をApp.js内に記述します。 App.js const App = () => { return ( <>           <h1>こんにちわ</h1> <p>Hello</p> </> ); }; export default App; App.jsを他のファイルでも使えるようにexportする必要があります。 そしてApp.jsをindex.jsでimportします index.js import react-dom from "ReactDom"; import App from "./App"; ReactDom.render(<App />, document.getElementById("root")); 以前と変わらず表示されたと思います。 このように、各ファイルをコンポーネントを定義をしておいて、他のファイルから読み込んでパーツを組み合わせるように画面を構築していくことはReact開発の醍醐味だと思います。 これらがしっかりできると楽しいと思います。笑 コンポーネントのファイルは.jsでも大丈夫ですがわかりやすくするために.jsxに変更できます。 コンポーネントファイルの拡張子は.jsxに変更するといいと思います。 とりあえず。。。 とりあえず長くなりそうなのでここまでにします。 ここまで見てくださりありがとうございます。 何かあればお願いします。 まだまだ、雑魚なので精進します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

もうPHP/RoRからReact Server Componentsに変えたほうが良い

はじめに 今からReactを始めるなら、React Server Components がおすすめです。 パラダイムチェンジが起き、ここ5年ほどでSSR(Server Side Rendering:PHPやRuby on Railsなど)からSPA(Single Page Application:ReactやVueなど)に変わっていきました。 今後はさらに、SPAとSSRの良いとこ取りに移行しようとしています。 自己紹介 5年前(React v0.1の時代)に、下の記事を書いたものです。VTeacherというサービスを開発するエンジニアをしています。社長もやっています。昔はCyberAgentやGREEにいました。 出来る限り短く説明するReactJS入門 https://qiita.com/rgbkids/items/8ec309d1bf5e203d2b19 開発しているサービス(VTeacher) VTeacherは↓の動画のように、ライブ配信中に簡単にQRコードを挿入できるサービスです。例えば、QRコードから自分のショップに誘導できたり、PAY系サービスのQRコードを表示できるので、ライブ配信のマネタイズを容易にします。 https://www.youtube.com/watch?v=eRAD3haXXzc 今までやってきたこと 分野 内容 内容 Front Mobile Objective-C, Swift, Java, Kotlin, Android NDK(C++), RxSwift, RxJava, VIPER, Realm Web HTML, CSS, JavaScript, Node.js, npm, yarn, Babel, Webpacker, JQuery VR/AR/MR Unity(C#), C, C++, Java, Three.js, WebGL, Oculus(Rift/GearVR/Go/Quest), THETA(S/V) Back PHP PHP(Ethna/Codeigniter/Zend Framework), Linux, Apache, Nginx, MySQL, Postgresql, WordPress, WebAPI(REST), PHPUnit, Selenium, Codeception JVM Java(JSP/EJB/Struts/Velocity/Seasar2), Scala, JavaScript, Oracle, MySQL, Solaris, Linux(RedHat/CentOS), JRun, HitachiCosmiNexus, Web Logic, Tomcat, Play, Apache, Nginx, WebAPI(REST), JUnit, Jmeter Infrastructure AWS(EC2, Lambda, Batch, ECR, ECS, EKS, S3, RDS, VPC, CloudFront, Route 53, API Gateway, Cloud9, MediaLive, MediaPackage, MediaStore, Elastic Transcoder, IAM, Certificate Manager, Key Management Service, Simple Email Service) DevOps Docker, Jenkins, CircleCI, GitHub Actions 他 Shell, Ruby, Perl, VB, VBA, VB.NET, ASP 今回はこの投稿の続編となります。 前回と同様、「少し早めのキャッチアップ」がコンセプトです。 Reactのバージョン 2020年12月にFacebookからReact Server Componentsのデモが公開されました。 現在のReactのバージョンは18ですが、React Server Componentsの正式導入は19以降と予想されています。これまでにReact Server Componentsのための布石と言えるような実験的な機能がリリースされてきました。業界人の予想通り、全てがReact Server Componentsのためであれば、今までの常識も変わるでしょうから、先入観がないほうが受け入れやすいと思います。 Reactチームが出したデモコードを解析しながら、チームで便利なちょっとしたWebアプリを作ってみたらいかがでしょうか? DBはPostgreSQLを使います。目指すところは React Server Components + Relay + GraphQL になりますが、Relay+GraphQLはReact Server Componentsのデモでは利用できません。 注1. TypeScriptの導入欲求はいったん抑えておきましょう。 React Server Componentsは複雑に入り組んでいるため、tsファイル化に時間を取ることはあまりおすすめできません。代わりに、VS Codeを使用した //@ts-check コメントを検討しましょう。ファイルの先頭に記述すると、普通のJavaScriptであっても型チェックをしてくれます。 注2. Next.jsの採用欲求はいったん抑えておきましょう。 Next.js用のReact Server Componentsのデモが別に出ています。本家(Facebook)がNext.jsの得意領域に進出したとも見え、共存方法を模索中であるような段階です。 デモのインストール デモのインストール方法はREADMEを参照してください。Dockerの利用が手早いと思います。 React Server Components デモのコード(GitHub) https://github.com/reactjs/server-components-demo Dockerのインストール ※ちなみにMac版のDockerはdocker-composeを含みます。 https://www.docker.com/ またはこちらの記事を参考に。 参考:React Server Components (demo) を Amazon Linux 2 で試す (Docker編) https://zenn.dev/rgbkids/articles/8025b3297e07d4 参考:React Server Components (demo) を Amazon Linux 2 で試す (PostgreSQL編) https://zenn.dev/rgbkids/articles/fa1d3eb1e9b065 localhostで確認ができたら、次に進みましょう。 - http://localhost:4000/ このデモをスケルトンとして、自作のコンポーネントを追加していきます。 必要なファイル以外を削除 src以下は下記のものを残して、残りのファイルは削除で大丈夫です。 App.server.js Root.client.js Cache.client.js db.server.js LocationContext.client.js index.client.js 予習・復習 Reactの書き方。初めての方、久しぶりの方に向けて。基本構文はこちらです。 export default function Hoge() { return ( <div> This is Hoge. </div> ); } Hogeというファイル名で、これを定義することにより、 <Hoge /> のようにタグ表記できるようになります。<Hoge /> の中身は、returnに記述されているHTMLで、Webブラウザから見ると、それが表示されます。このあたりの技術をJSXと言い、Facebookが開発しています。returnには他のコンポーネントも記述できます。 React Sever Components の種類 React Sever Components は通称です。利用にあたり3種類のファイルを使用します。 サーバーコンポーネント ファイル名の命名規則は .server.js サーバー側でレンダリング 他資源へのアクセス(react-fetchからREST API等、react-pgからDB参照、将来的にはRelay+GraphQL等) クライアントコンポーネント ファイル名の命名規則は .client.js クライアント側でレンダリング 他資源へのアクセス(react-fetchからREST API等) 通常のReactコンポーネントと同じく、stateが使えます。 共通コンポーネント ファイル名の命名規則は .js サーバー側でもクライアント側でも利用可能なコンポーネントです。共通処理。 ネーミング(命名規約)の注意点 ToDOというコンポーネントを考えた時、つい、下記のようなファイル構成にしてしまいました。 ToDo.server.js ToDo.client.js ToDo.js しかし、これはimportの際、defaultの名前が重複してしまうので(この場合はToDo。import時に名前を設定できるけど)、おすすめしません。Facebookのデモもこのような構成になっていません。 コンポーネントをきちんと設計して、コンポーネント単位で分けましょう。 サーバーコンポーネントしか許されていない処理をクライアントコンポーネントで実行すると、エラーになります。 例:クライアントコンポーネントでdb(react-pg)を利用する場合、実行時に TypeError: Cannot read property 'db' of undefined になってしまう。 import {db} from './db.server' (略) const notes = db.query( `select * from notes where title ilike $1`,['%%'] ).rows; 最初はサーバーコンポーネントだけを用意したほうがやりやすいです。 その中でクライアントコンポーネントにできるもの・する必要があるものは変更します。 ここから始まるApp.server.js React Server Componentsは、ここからはじまります。このファイルにサーバーコンポーネントを記述します。 現時点では、とりあえず、このようにしておきましょう。 App.server.jsの修正 export default function App({selectedId, isEditing, searchText}) { return ( <div> </div> ); } 最初にサーバーコンポーネントを作成 自作のコンポーネントを追加していきましょう。 まずはサーバーコンポーネントを用意しましょう。先ほど説明した通り、最初はサーバーコンポーネントだけを用意して、その後にクライアントコンポーネントにできるもの・する必要があるものを探していきましょう。 srcディレクトリ直下に Hoge.server.js を作成し、下記のコードをコピーしてください(サーバーコンポーネントなので、規則に沿って server.js になります)。 src/Hoge.server.js(新規作成します) export default function Hoge() { return ( <div> This is Hoge.server.js! </div> ); } このHoge(Hoge.server.js)をApp.server.jsに記述します。 src/App.server.js(既に存在するので変更して保存) import Hoge from './Hoge.server'; export default function App({selectedId, isEditing, searchText}) { return ( <div className="main"> <Hoge /> </div> ); } サーバーコンポーネントはサーバー側でレンダリングされます。現時点では通常のSSR(PHPやRuby on Rails)と変わりません(のちほどクライアントコンポーネントを作成します)。 他資源へのアクセス サーバーコンポーネントは、db(react-pg)にアクセスできます(ただしdbへの直接アクセスはアプリ設計上は推奨されていません)。 REST APIの利用にあたってはfetch(react-fetch)が利用できます。fetchはクライアントコンポーネントからも利用できますが、重い処理となりそうなところはサーバーコンポーネントで処理することでクライアントに返すデータ量を削減できます(React Server Componentsの目標であるバンドルサイズゼロ)。 Hoge.server.jsを下記のように変更してみましょう。 Webブラウザで確認するとdb・fetchで取得した値が表示されると思います。 src/Hoge.server.js(変更してみましょう) import {db} from './db.server'; // db(react-pg) import {fetch} from 'react-fetch'; // fetch(react-fetch) export default function Hoge() { // db const notes = db.query( `select id from notes` ).rows; // fetch const note = fetch(`http://localhost:4000/notes/1`).json(); let {id, title, body, updated_at} = note; return ( <div> <p>db:</p> <ul> {notes.map((note) => ( <li>{note.id}</li> ))} </ul> <p>fetch:</p> {id}{title}{body}{updated_at} </div> ); } 「実験」 Hoge.server.jsをコピーしてHoge.client.jsを作成してみましょう。 App.server.jsのimportをHoge.clientにしてみましょう。 実行時に TypeError: Cannot read property 'db' of undefined になります。 (fetchは可能です) 実験後は元に戻しておきましょう(App.server.jsのimportをHoge.serverに戻す)。 クライアントコンポーネントを作成 サーバーコンポーネントとクライアントコンポーネントを入れ子にして記述してみましょう。React Server Components は、原則、サーバーコンポーネントから始まります。 下記のようなコンポーネント設計をしてみます。 - ServerComponentHello (Hello.server.js) ∟ ClientComponentLeft (Left.client.js) - ServerComponentWorld (World.server.js) ∟ ClientComponentRight (Right.client.js) src/App.server.js(変更してみましょう) import Hello from './Hello.server'; import World from './World.server'; export default function App({selectedId, isEditing, searchText}) { return ( <div className="main"> <Hello /> <World /> </div> ); } src/Hello.server.js(新規作成します) サーバーコンポーネント。dbから値を取得し、子となるクライアントコンポーネント(Left)に引き継ぎます。 import {db} from './db.server'; import Left from './Left.client'; export default function Hello() { const notes = db.query( `select id from notes` ).rows; let text = ""; notes.map((note) => { text += `${note.id},`; }); return ( <Left text={text} /> ); } src/World.server.js(新規作成します) サーバーコンポーネント。fetchで値を取得し、子となるクライアントコンポーネント(Right)に引き継いでいます。 import {fetch} from 'react-fetch'; import Right from './Right.client'; export default function World() { const note = fetch(`http://localhost:4000/notes/1`).json(); let {id, title, body, updated_at} = note; let text = `${id}${title}${body}${updated_at}`; return ( <Right text={text} /> ); } src/Left.client.js(新規作成します) クライアントコンポーネント。渡された値を左側に表示します(cssで設定)。 export default function Left({text}) { return ( <div className="left"> {text} </div> ); } src/Right.client.js(新規作成します) クライアントコンポーネント。渡された値を右側に表示します(cssで設定)。 export default function Right({text}) { return ( <div className="right"> {text} </div> ); } public/style.css(既存ファイルを変更します。※末尾に追記) .left { float: left; width: 50%; } .right { float: right; width: 50%; } Webブラウザから確認しましょう。 http://localhost:4000/ 下記のように表示されると思います。 1,2 ...(略) 1Meeting ...(略) 「補足」 ちなみにClientComponentの子にServerComponentを置いてもエラーにはなりませんが、そのServerComponentからはdbへのアクセスができません(fetchはできます)。 - ServerComponentHello (Hello.server.js) ∟ ClientComponentLeft (Left.client.js) ∟ ServerComponentWorld (World.server.js) ※dbアクセス不可能 ∟ ClientComponentRight (Right.client.js) React Server Components のメリット SSRとSPAの良いとこどり。 React Server Components は「レンダリングのパフォーマンス改善(目標バンドルサイズゼロ)」がメリットです。 (React Server Components を利用するだけで表示が軽くなるわけではなく、SPAにおけるWarterFall問題など、コンポーネント設計はきちんとする必要があります) 参考: RFC https://github.com/reactjs/rfcs/pull/188 > Reactサーバーコンポーネントの利点の1つは、開発者が単一の言語とフレームワークを使用してアプリケーションを記述し、サーバーとクライアント間でコードを共有できることです。 「実験」 わざと遅延を発生させてみましょう。 React Server Componentsのデモにはfetch用のsleepが用意されています。 これを実行して、わざと遅延を生じさせます。 src/World.server.js(変更しましょう) import {fetch} from 'react-fetch'; import Right from './Right.client'; export default function World() { let _ = fetch(`http://localhost:4000/sleep/3000`); // 3秒の遅延 const note = fetch(`http://localhost:4000/notes/1`).json(); let {id, title, body, updated_at} = note; let text = `${id}${title}${body}${updated_at}`; return ( <Right text={text} /> ); } Webブラウザで確認してみましょう。 3秒後に表示されるようになったと思います。 - http://localhost:4000/ 「検証」 WebブラウザにChromeを使用し、Chromeの開発ツール(右クリックで検証)を開き、Networkタブを選択し、react?location=... の Previewを見ると、サーバー側からクライアント側に返されるデータを見ることができます。 Suspense 今までの実験的機能はReact Server Componentsのために用意されてきたとも言われています。それらの実験的機能はデモに利用されています。これをTIPSとして紹介していきます。 Suspense(サスペンス)はReact16で導入された実験的な機能です。 コードのロードを「待機」して宣言的にロード中状態(スピナーのようなもの)を指定することができます。 https://ja.reactjs.org/docs/concurrent-mode-suspense.html デモに従い、 <Suspense /> を利用しましょう。 import {Suspense} from 'react'; を行い、3秒の遅延処理がある <World /> を、 <Suspense> ... </Suspense> で囲みます。<Suspense> のfallbackには待ち時間に表示しておくタグを渡しておきます。 参考 New Suspense SSR Architecture in React 18 #37 https://github.com/reactwg/react-18/discussions/37 src/App.server.js(変更してみましょう) import {Suspense} from 'react'; import Hello from './Hello.server'; import World from './World.server'; import Right from "./Right.client"; export default function App({selectedId, isEditing, searchText}) { return ( <div className="main"> <Hello /> <Suspense fallback={<Right text={"This is suspense."} />}> <World /> </Suspense> </div> ); } Webブラウザで確認してみましょう。 今度は、最初に This is suspense. が表示され、3秒後に完全なページが表示されるようになったと思います。 - http://localhost:4000/ トランジション ボタンを押した時など、画面が再表示される際、一瞬だけ白い画面がチラっと見えたり、さっきまで表示されていた情報が見られなくなったりなど、画面の更新のタイミングを調整したい場合があります。 このような「見せたくない処理」をスキップして、新しい画面に切り替え (transition) する前に新しいコンテンツがロードされるのを調整(待機)できます。 参考 並列的 UI パターン(実験的機能) トランジション https://ja.reactjs.org/docs/concurrent-mode-patterns.html 実際にやってみると一目瞭然です。 再描画の処理を入れてみましょう。トランジションを使うパターンと使わないパターンを用意して、比較してみます。 src/Left.client.js(変更してみましょう) import {useTransition} from 'react'; import {useLocation} from './LocationContext.client'; export default function Left({text}) { const [location, setLocation] = useLocation(); const [, startTransition] = useTransition(); let idNext = location.selectedId + 1; return ( <div className="left"> <p>id={location.selectedId}</p> <button onClick={() => { setLocation((loc) => ({ selectedId: idNext, isEditing: false, searchText: loc.searchText, })); }}> Next id={idNext} </button> <button onClick={() => { startTransition(() => { setLocation((loc) => ({ selectedId: idNext, isEditing: false, searchText: loc.searchText, })); }); }}> Next id={idNext} (Transition) </button> <p>{text}</p> </div> ); } トランジションを使ったほうが、より自然な画面遷移になると思います。 トランジションを使用しない場合だと、Rightコンポーネントが、Nextボタンを押すたびに「This is suspense.」と表示されてしまいます。 Rightコンポーネントは意図的に3秒遅延処理を入れているため、トランジションの使用にかかわらず、新しいデータが表示されるまでに3秒待つことになります。 クライアントコンポーネントからサーバーコンポーネントに値を渡す サーバー側で値を引き継ぐ方法です。 Facebookのデモでは、Appが3つの引数( {selectedId, isEditing, searchText} )を取っています。 これは、先程のトランジションについてのクライアントコンポーネントのコード(LocationContext.clientのsetLocation関数)と関連しています。 setLocation((loc) => ({ selectedId: idNext, isEditing: false, searchText: loc.searchText, })); これにより、クライアントからサーバーへ値を引き渡すことができます。 サーバーコンポーネントの <Hello /> と <World /> に、selectedIdを引き継ぎましょう。 selectedId={selectedId} のように記述します。 src/App.server.js(変更します) import {Suspense} from 'react'; import Hello from './Hello.server'; import World from './World.server'; import Right from "./Right.client"; export default function App({selectedId, isEditing, searchText}) { return ( <div className="main"> <Hello selectedId={selectedId} /> <Suspense fallback={<Right text={"This is suspense."} />}> <World selectedId={selectedId} /> </Suspense> </div> ); } <Hello /> と <World /> もselectedIdが参照できるように変更します。せっかくselectedIdが参照できるようになったので、fetch・dbに利用しましょう。 src/Hello.server.js(変更します) import {db} from './db.server'; import Left from './Left.client'; export default function Hello({selectedId}) { const notes = db.query( `select id from notes where id=$1`, [selectedId] ).rows; let text = selectedId; notes.map((note) => { text = note.id; }); return ( <Left text={text} /> ); } src/World.server.js(変更します) import {fetch} from 'react-fetch'; import Right from './Right.client'; export default function World({selectedId}) { let _ = fetch(`http://localhost:4000/sleep/3000`); // 3秒の遅延 if (!selectedId) { return ( <Right /> ); } let note = fetch(`http://localhost:4000/notes/${selectedId}`).json(); let {title, body, updated_at} = note; let text = `${selectedId}${title}${body}${updated_at}`; return ( <Right text={text} /> ); } Webブラウザで確認してみましょう。 Nextを押すと、idに応じたデータが表示されるようになったと思います。 - http://localhost:4000/ 注:このままだと、存在しないidを指定した場合にシンタックスエラーになって落ちてしまうため、デモのAPIを修正(暫定対応)してください。 server/api.server.js(変更します) 177行目、 res.json(rows[0]); を res.json(rows[0] || "null"); に変更します。 app.get( '/notes/:id', (略) res.json(rows[0] || "null"); // ←変更 (略) ); "null" にした理由はこちらを参照ください。 この件、reactjs/server-components-demo に Pull Request を出しました。 fetchによるREST API処理 PostgreSQLにレコードを登録してみましょう。 デモで用意されているAPI( server/api.server.js に実装されています )を使います。 server/api.server.js には登録の他に、更新・削除のAPIも用意されています。 デモのコードを参考に登録処理を実装してみましょう。 新規登録(idは新しく付与)されます。Nextボタンを押して、新規作成されたデータを確認してみましょう。一番最後に追加されています。 onClickにトランジションを入れても大丈夫です。 src/Former.server.js(新規作成します) import {fetch} from 'react-fetch'; import FormerClient from './Former.client'; export default function Former({selectedId}) { const note = selectedId != null ? fetch(`http://localhost:4000/notes/${selectedId}`).json() : null; if (!note) { return <FormerClient id={null} initialTitle={""} initialBody={""} />; } let {id, title, body} = note; return <FormerClient id={id} initialTitle={title} initialBody={body} />; } src/Former.client.js(新規作成します) import {useState, useTransition} from 'react'; import {useLocation} from './LocationContext.client'; import {createFromReadableStream} from 'react-server-dom-webpack'; import {useRefresh} from './Cache.client'; export default function Former({id, initialTitle, initialBody}) { const [title, setTitle] = useState(initialTitle); const [body, setBody] = useState(initialBody); const [location, setLocation] = useLocation(); const [, startNavigating] = useTransition(); const refresh = useRefresh(); function navigate(response) { const cacheKey = response.headers.get('X-Location'); const nextLocation = JSON.parse(cacheKey); const seededResponse = createFromReadableStream(response.body); startNavigating(() => { refresh(cacheKey, seededResponse); setLocation(nextLocation); }); } // 登録処理 async function handleCreate() { const payload = {title, body}; const requestedLocation = { selectedId: "", isEditing: false, searchText: location.searchText, }; const endpoint = `http://localhost:4000/notes/`; const method = `POST`; const response = await fetch( `${endpoint}?location=${encodeURIComponent(JSON.stringify(requestedLocation))}`, { method, body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json', }, } ); console.log(response); navigate(response); } // 更新処理 async function handleUpdate() { const payload = {title, body}; const requestedLocation = { selectedId: location.selectedId, isEditing: false, searchText: location.searchText, }; const endpoint = `http://localhost:4000/notes/${location.selectedId}`; const method = `PUT`; const response = await fetch( `${endpoint}?location=${encodeURIComponent(JSON.stringify(requestedLocation))}`, { method, body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json', }, } ); console.log(response); navigate(response); } // 削除処理 async function handleDelete() { const payload = {title, body}; const requestedLocation = { selectedId: location.selectedId, isEditing: false, searchText: location.searchText, }; const endpoint = `http://localhost:4000/notes/${location.selectedId}`; const method = `DELETE`; const response = await fetch( `${endpoint}?location=${encodeURIComponent(JSON.stringify(requestedLocation))}`, { method, body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json', }, } ); console.log(response); navigate(response); } return ( <form onSubmit={(e) => e.preventDefault()}> <input type="text" value={title} onChange={(e) => { setTitle(e.target.value); }} /> <input type="text" value={body} onChange={(e) => { setBody(e.target.value); }} /> <button onClick={() => { handleCreate(); }}> Create </button> <button onClick={() => { handleUpdate(); }}> Update id={location.selectedId} </button> <button onClick={() => { handleDelete(); }}> Delete id={location.selectedId} </button> </form> ); } src/App.server.js(変更します) 作成したFormer(サーバーコンポーネント)を記述します。 <Former /> の親となる要素には、keyを与えてください。keyは、どの要素が変更・追加・削除されたのかを、Reactが識別する際に必要となります。 下記では <section></section> を使いましたが、 <div></div> でも大丈夫です。 import {Suspense} from 'react'; import Hello from './Hello.server'; import World from './World.server'; import Right from "./Right.client"; import Former from "./Former.server"; export default function App({selectedId, isEditing, searchText}) { return ( <div className="main"> <Hello selectedId={selectedId} /> <Suspense fallback={<Right text={"This is suspense."} />}> <World selectedId={selectedId} /> </Suspense> <section key={selectedId}> <Former selectedId={selectedId} isEditing={isEditing} /> </section> </div> ); } 外部のDBを利用する credentials.js を変更します。 credentials.js 例:ec2-18-181-96-11.ap-northeast-1.compute.amazonaws.com のDBを利用する。 module.exports = { host: 'ec2-18-181-96-11.ap-northeast-1.compute.amazonaws.com', database: 'notesapi', user: 'notesadmin', password: 'password', port: '5432', }; Webサーバー(express)のポートを変更する 80番にする例です。 server/api.server.js を80に変更します。 const PORT = 80; Dockerを使用している場合、docker-compose.ymlの設定も80に変更します。 ports: - '80:80' environment: PORT: 80 その他、REST APIを使用している箇所(エンドポイント)を80に変更します。 fetch(`http://localhost:80/notes/...`) ※80番なので、省略してもかまいません。 スケールアウトについて 簡単な検証を行ってみました。 結論から言うと一般的な方法でスケールアウトできます。 検証 React Server Components のデモを Amazon Linux2 (EC2) 3台にデプロイ。 DB ec2-18-181-96-11.ap-northeast-1.compute.amazonaws.com:5432 Webサーバー1 http://ec2-52-192-75-244.ap-northeast-1.compute.amazonaws.com:4000/ ※DB接続先を変更しています module.exports = { host: 'ec2-18-181-96-11.ap-northeast-1.compute.amazonaws.com', database: 'notesapi', user: 'notesadmin', password: 'password', port: '5432', }; Webサーバー2 http://ec2-54-238-209-222.ap-northeast-1.compute.amazonaws.com:4000/ ※DB接続先を変更しています module.exports = { host: 'ec2-18-181-96-11.ap-northeast-1.compute.amazonaws.com', database: 'notesapi', user: 'notesadmin', password: 'password', port: '5432', }; 次に Route 53 を使って、リクエストを振り分けるように設定します(DNSラウンドロビン)。 rsc-demo.cmsvr.live レコードタイプ A 値 52.192.75.244 54.238.209.222 これでアクセスしてみます。 例 - http://rsc-demo.cmsvr.live:4000/ 期待通りの動作をすると思います。 これは通常のSSRのように、クライアントの状態をサーバーに送信しているためです。 具体的には、Appの引数にある下記の値を、URLのqueryとHeaderのX-Locationに設定して整合性を保っています。 {selectedId, isEditing, searchText} ただし、デモにあるキャッシュの処理は工夫が必要かもしれません。 演習 React Server Components のWebアプリを作ってみましょう。 いきなり1から作ることは大変ですので、ちょっとしたWebアプリを作成しておきました。 エンジニアが気軽に React Server Components を試すことができ、それをすぐに公開できるWebアプリです。操作はとても簡単です。Fork して、デプロイ用の docker-compose.yml を上書きするだけです。 ソースコードはGitHubにあります(MIT License)。 https://github.com/rgbkids/server-components-serverless Dockerを使って、エフェメラルポート(ポート番号49152〜65535)を割り振り、個々のアプリを起動する仕組みです。チーム内のデモ用開発などのご利用にどうぞ。 const portDynamicMin = 49152; const portDynamicMax = 65535; 課題 リファクタリングをしてみましょう。 body はデモコードのままの変数名です。 body ではなく、 port にしてみましょう。 title はデモコードのままの変数名です。title ではなく、 url にしてみましょう。 vteacher.cmsvr.live をあなたが取得しているドメインにしてみましょう。 サーバー側のレンダリング処理が Former.client.js を利用しているだけです。さらに、クライアントコンポーネントからfetchをする作りになっています。SPAらしい動きではありますが、現状の作りですと、React Server Components の恩恵を受けていない形なので、改善してみましょう。 ☕️Coffee break 1 SSR経験者ならReact Server ComponentsでSPAがめちゃくちゃ簡単に作れる話 弊社が開発しているVTeacherの一部にReact Server Componentsを使用しています。YouTube等にライブ配信をするサービスなので、視聴用にとても便利です。 https://studywithme.cmsvr.live/ ぜひサインイン(Googleアカウント)して、FilterやPIN留めの操作をしてみて、SPAらしい挙動を確認してください。余計なあたまからのレンダリングによる白い画面のチラツキがなく、PCにインストールするソフトウェアや、スマホのアプリのような操作体験ができるものです。 ソースコードは コチラ です。 ※説明のため、ベースとした reactjs/server-components-demo のコードをできるだけ残し、必要に応じてコードを追加する形にしています(ファイルの先頭に //@ts-check を追加しています)。 重要なことは「SPA」感が出ていることです。 ☕️Coffee break 2 JavaScriptを巡る主導権争い 直接 React Server Componetns とは関係ありませんが、47歳さんの漫画をみて、思うことがありました。昨今の「JavaScriptはバックエンドエンジニアのものか、フロントエンドエンジニアのものか?」というような、まるでDevOpsのような問題がJavaScriptでも起きています。 (C) tome_ura さん これはCさんチーム(おそらくAPI担当?)とアプリチームの話だと思いますが、DevOpsのような争いが実際の職場でも発生していると思います。 Wikipedia https://ja.wikipedia.org/wiki/DevOps DevOps(デブオプス)は、ソフトウェア開発手法の一つ。開発 (Development) と運用 (Operations) を組み合わせたかばん語であり、開発担当者と運用担当者が連携して協力する(さらに両担当者の境目もあいまいにする)開発手法をさす。ソフトウェアを迅速にビルドおよびテストする文化と環境により、確実なリリースを、以前よりも迅速に高い頻度で可能とする組織体制の構築を目指している。 React Server Componetnsは、フロントエンドとバンクエンドで共通言語を利用できます。 フロントエンドとバックエンドのJavaScriptを巡る主導権争いも、解決できるかもしれません。 FacebookのLauren Tan(poteto)さんもRFC内でこれについて回答しています。 https://github.com/reactjs/rfcs/pull/188#issuecomment-751283849 (翻訳) アーキテクチャに関しては、チームが選択した言語でバックエンドサービスを作成し続けることも期待しています。フロントエンドチームは、React Server Componentsを採用することを選択した場合、それらのバックエンドサービスを構成するnodejsで記述されたフロントエンドAPI / BFF(フロントエンドのバックエンド)を作成できます。ここでの良い点は、これをすべて1つの言語と1つの技術スタックで実行できることです。 ☕️Coffee break 3 今までのSPA SPAについて、今までは下記のアーキテクチャが多かったのではないでしょうか。 React + (Next) + API(REST/GraphQL) MVCフレームワーク(PHPやRuby on Rails等)のViewにReactやVue 2について、 5年くらい前でしたら「ReactはMVCのViewを補うライブラリ」程度の役割でしたが、昨今の進化を見る限りでは、Viewの一部では恩恵を受け切れていない状態になっていると思います。 1について、 1が最近のモダンなアーキテクチャ・スキルセットでしょう。しかしSSGにできるサイト/サービスには限りがあり、結局のところSSRの検討が必要になることが多いと思います。 さらに、これらはWaterfall問題が発生しやすく、その対策が必要になります(Facebookの中の人いわく、React + Relay + GraphQLでクライアント側のWaterfallはあっさり解決するのだが、Facebookのエコシステムを使っていない人は、React Server Components の方法は効果的だと説明しています)。 ☕️Coffee break 4 ウォーターフォール(Waterfall)問題とは? 最初のコンポーネントのfetchが完了した後に、次のコンポーネントのfetchが開始するような コンポーネント設計上のアンチパターンです。※開発手法のWaterfallとは異なります(上から下に水が流れるイメージは同じです)。 具体的には下記のような例です。 <Hello /> がレンダリングを完了するまで、 <World /> はfetchを開始できない。 <Hello> <World /> </Hello> // <Hello /> function Hello() { const result = fetchHello(...); // Helloに関するfetch処理 return ( ... ); } // <World /> function World() { const result = fetchWorld(...); // Worldに関するfetch処理 return ( ... ); } ☕️Coffee break 5 React Server Components は どの立ち位置か そもそもReact Server Componentsとは何なのか? React Server Componentsを少し勉強した人ほど、React Server Componentsの立ち位置がよくわからなくなってくると思います。 ☕️Coffee break 6 Facebookのフロントエンドエンジニアを知る React Server Componentsの誕生の経緯を知るために、先にFacebookのことを知ってみましょう。これは米国カリフォルニア州メンローパーク(Menlo Park)の求人です。 Facebookにおける、フロントエンドエンジニアとは? (翻訳) フロントエンドエンジニアの責任 1.多くのエンジニアが関与する複雑な技術的または製品的取り組みを主導する 2.同僚に技術的なガイダンスとメンターシップを提供する 3.ニュースフィードなどのFacebook製品の機能とユーザーインターフェイスを実装する 4.複雑なWebアプリケーションを駆動する効率的で再利用可能なフロントエンドシステムを設計する 5.プロダクトデザイナー、プロダクトマネージャー、ソフトウェアエンジニアと協力して、魅力的なユーザー向け製品を提供します 6.パフォーマンスとスケーラビリティの問題を特定して解決する 「React Server Components」の提案は、これらのうち4と6に該当すると思います。 ☕️Coffee break 7 React Server ComponentsのRFC 下記がReact Server ComponentsのRFCです。 ( RFC = Request for Comments = 意見募集中 ) ここでFacebookのDan Abramov(React Core)さんとLauren Tan(React Data)さんが回答をしています。この2人はdemoの動画でも話している2人です。 重要なキーワードとして下記のことを挙げています。 バンドルサイズゼロ (クライアント側の)Waterfall問題 バンドルサイズゼロとは FacebookのLauren Tan(poteto)さんもRFC内でこれについて回答しています。 https://github.com/reactjs/rfcs/pull/188#issuecomment-751283849 (翻訳) バンドルサイズが大きいということは、ダウンロード、解析、実行するJavaScriptの数が多いということです。ローエンドのデバイスでは、この作業を迅速に行うための計算能力が低いため、これは大きな問題となります。 React Server Componentsをひとことで説明するなら「パフォーマンス改善のための技術」でしょう。 ですが、副産物的なメリットもあります。それを次に説明します。 ☕️Coffee break 8 React Server Componentsの副産物的なメリット React Server Componentsは、SSR経験者にとってSPAをつくるときに「React Server Components」ですと、とても作りやすいと思います。 ※ここでいうSSRとは、いわゆる広義のSSR(PHPやRuby on Railsなど)を指します。 Lauren Tan(poteto)さんも「状態管理」について回答しています。 (翻訳) 状態管理をほとんど行う必要がありませんでした。したがって、多くの点で、React Server Componentsは、インタラクティブなクライアントコンポーネントからデータフェッチを分離するのに実際に役立つと思います。 いままではSPAを作る際、コンポーネントに分けたあと、fetchでサーバーからデータを取得し、コンポーネント同士でデータの整合性を保ちながら、状態を管理するなど、少し敷居が高かったのです。でもReact Server Componentsなら、まるでPHPやRoRを書くように、SPAを書けるのではないでしょうか。 ☕️Coffee break 9 React Server Componentsの重要なキーワードを整理 ゼロバンドルサイズを目指す クライアント側のWaterfallを解消 コンポーネントの状態管理が楽 ☕️Coffee break 10 基本はSSR(PHPやRails等)の考えでつくることができる SSRは広義(PHPやRialsなど)の意味とします。 SSRの基本的な動作は、たとえばbuttonでclickイベントが発生したら、いったんサーバにリクエストをし、サーバーはそれに応じてhtmlを返すものです。原則、requestのたびにページ全体がレンダリング(描画)されます。 React Server Components はサーバーが名称にはいっていることもあり、このようにバックエンド(と日本では定義している)処理もできます。 React Server Componentsはフレームワークですが、CakePHPやRailsのようにフルな機能はありません。都度、機能を実装、またはライブラリを使うことになります。 重要なポイントはSSRの感覚で書いているのに、SPAらしく動作することです。 ☕️Coffee break 11 React Server Components 作り方の基本 今から作ろうとするWebアプリケーションに状態を持たせましょう。 聞きなじみのある言葉で説明するなら、オブジェクト指向で言うところのImutableに設計するということです。 Immutable Wikipedia https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%9F%E3%83%A5%E3%83%BC%E3%82%BF%E3%83%96%E3%83%AB React https://ja.reactjs.org/tutorial/tutorial.html#detecting-changes 例: userId: "u001", token: "12345abcde", lang: "en", JavaScriptのonClickイベントで次のように記述すると、状態を変更できます。 これをReact Server Components では「Locationを変更する」と言います。 setLocation((loc) => ({ userId: "", token: "", lang: "", })); onClickのイベント発生時に、アプリケーションの状態を変更(setLocation)すると、いったんサーバーにいきます(このあたりは差分だけレンダリングするなど、全部レンダリングしない仕組みになっています。イベントのシーケンスは同じイメージになります)。 これはまるでPHPやRoRなどの、SSRのようです。 各引数には{}を忘れないようにしてください。propsを指定の変数名で展開してくれます。 propsをまるごと受け取りたい、そして子供に渡したい場合は、{}なしでpropsなどの変数名で良いと思います。 function App(props) { console.log(props); console.log(`props.id=${props.id}`); console.log(`props.id=${props.name}`); return (<div></div>); } { id: a name: aaa } props.id=a props.name=aaa ☕️Coffee break 12 React Server Componentsの認証 認証について Firebaseを使った認証を考えます。 ただしあくまで認証のみの利用とします。 FirebaseのCloudストレージなどは、基本的にクライアント側での非同期処理となり(結果としてuseEffect=副作用の多様となり)、React Server Componentsのせっかくのサーバーサイド処理(RDBに対しての処理)が整合性が取りづらくなり、書きづらくなります。 なので、基本はFirebaseのAuthのみを利用することにします。ログイン後にuidとtoken(30分ごとにリフレッシュされる)を取得し、RDBに保存します。 このuidとtokenは、fetchの際、認証されます。 ちなみにFirebaseのバージョンが古い例なので、ただFirebaseの最新版にするだけだと物足りないと思うので、話題のSupabaseに挑戦してみたらいかがでしょう。 Authのコード - https://github.com/rgbkids/server-components-demo/blob/main/src/Note.server.js import Auth from "./Auth.client"; <Auth /> これによりAuth(Firebaseの認証)がはいります。 Authのコードです。Firebaseを使っているので必ずclient.js(クライアントコンポーネント)にしてください。 https://github.com/rgbkids/server-components-demo/blob/main/src/Auth.client.js 個別の情報は settings.js に記述しておきます。 https://github.com/rgbkids/server-components-demo/blob/main/settings.js.default export const firebase_config = { apiKey: "", projectId: "", authDomain: "", databaseURL: "", }; https://github.com/rgbkids/server-components-demo/blob/main/src/firebase.js import firebase from 'firebase'; export const useSignIn = () => { let provider = new firebase.auth.GoogleAuthProvider(); firebase.auth().signInWithRedirect(provider); }; export const useSignInPopup = () => { let provider = new firebase.auth.GoogleAuthProvider(); firebase.auth().signInWithPopup(provider); }; export const useSignOut = () => { firebase.auth().signOut(); }; export const useCurrentUser = () => { return firebase.auth().currentUser; }; export const useFirebase = () => { return firebase; } if (firebase.apps.length == 0) { const firebase_config = require(__dirname + '/../settings'); const firebaseConfig = firebase_config["firebase_config"]; firebase.initializeApp(firebaseConfig); } さいごに いかがでしたでしょうか? オリジナルのコンポーネントを作成し、データの登録・更新・削除までを行うことができました。 TIPSで説明したような、React Server Components のためと言われている、実験的な機能も体験してみました。 ブック以外で閲覧するには 情報の新鮮さを重視しています。投稿記事は定期的に削除しています。どうしても過去記事を読みたい場合は、VTeacherの「サブスク加入者のページ」からご閲覧ください。 https://apps.apple.com/app/vteacher/id1435002381
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

3kbのHTML to Reactパーサ「DOMParserReact」で、HTML文字列のaタグをNext.jsのLinkコンポーネントで表示する

作ったライブラリの宣伝記事です。 DOMParserReactとは DOMParserReactは軽量なHTML to Reactパーサーです。HTMLテキストをReactのコンポーネントに変換して表示します。また、表示の際にHTMLのタグを任意のコンポーネントに置き換えることができます。 主な使用想定は、個人ブログなどのコンテンツ部分です。コンテンツ部分は、MarkdownのファイルやヘッドレスCMSのリッチテキストで管理し、それをHTMLにしたものを表示していると思います。このライブラリを使用すれば、そのHTMLを表示する際、CSS in JS等のReactの技術を使って装飾したり、タイトルのようにaタグをSPA用の Link コンポーネントに置き換えたりすることができるようになります。 そして、このライブラリの一番の特徴は、その軽さです。minifyで3kb(v0.2.0時点)しかありません。類似ライブラリと比べて非常に軽量なのは、独自のパーサーを使わず、ブラウザJSのAPI DOMPerser を使用しているからです。 ライブラリ バンドルサイズ (minify) 備考 dom-parser-react 3kb 筆者作 html-react-parser 27kb rehype-react 42kb unifiedのプラグイン。計測時は、rehype-reactの他、unified、rehype-dom-parserを使用 ※上記は筆者調べ。rollup.jsでバンドルして計測。ただし、react、react-domのサイズは除外 使い方 それでは、実際の使用方法を紹介します。 NPMで公開していますので、インストールは、npm/yarnから行います。 npm i dom-parser-react # or yarn add dom-parser-react DOMParserReactを使用するには、コンポーネントをインポートして変換するHTML文字列を source で指定してください。 そのHTMLを表示するコンポーネントが作成されます。 import DOMParserReact from 'dom-parser-react' // import { renderToStaticMarkup as render } from 'react-dom/server' const htmlText = ` <h1>HTML Text</h1> ` const App = () => <DOMParserReact source={htmlText} /> render(<App />) // `<h1>HTML Text</h1>` 上記は dangerouslySetInnerHTML と結果がほぼ同じですので、今度はタグを置き換えてみましょう。 components プロパティに { 置換対象のタグ: 置換後のコンポーネント } という形式で指定します。下記は、h1 タグを Title コンポーネントに置き換える例です。 const htmlText = ` <h1>HTML Text</h1> ` const Title = (props) => <div className="title"> <h1 {...props} /> </div> const App = () => <DOMParserReact source={htmlText} components={{ h1: Title }} /> render(<App />) // `<div class="title"><h1>HTML Text</h1></div>` h1 が div.title で囲まれて表示されました。 今回は囲むだけでしたが、例えば、CSS in JSでスタイリングしたコンポーネントを指定したり、アンカーリンクのアイコン等を追加したりもできます。 また、複数種類のタグを置き換えたい場合は複数記載すれば対応可能です。 const components = { h1: Title, h2: SubTitle, p: Paragraph, } <DOMParserReact source={htmlText} components={components} /> Next.jsで使用する それでは、実際にNext.jsで使用してみましょう。 と言っても、上記のように components を指定するだけです。 import Link from 'next/link' import DOMParserReact from 'dom-parser-react' const components = { a: ({ href, ...props }) => ( <Link href={href}> <a {...props} /> </Link> ), } const Post = ({ htmlText }) => <DOMParserReact source={htmlText} components={components} /> もしかしたら、読者の中には本当に動くか疑問に思っている人がいるかもしれません。最初に説明したように DOMPerser はブラウザJSのAPIで、Next.jsのSSR/SSGが実行されるNode.jsにそのAPIはありません。 ですが、DOMParserReactは問題なく動作します。Node.jsでの実行時は、自動でJSDOMというライブラリをインポートし、DOMPerser をエミュレートします。これによってブラウザと同じように動作し、SSR/SSGが実行されます。もちろん、ブラウザではJSDOMはバンドルしませんので、軽量なサイズのままです。 最後に 上記の通りこのライブラリは、HTMLコンテンツを簡単に装飾することができ、ライブラリサイズも軽量でSSR/SSGにも対応しています。ぜひ、個人ブログなどに使用してみてください。 おまけ DOMParserReactを使用した、筆者のブログ Qiita記事を優先したため、最新の投稿はライブラリ使用前の去年のものになっています。そのうち、DOMParserReact作成時のこととかを書いていきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

3kbのHTML to Reactパーサ「DOMParserReact」で、aタグをNext.jsのLinkコンポーネントで表示する

作ったライブラリの宣伝記事です。 DOMParserReactとは DOMParserReactは軽量なHTML to Reactパーサーです。HTMLテキストをReactのコンポーネントに変換して表示します。また、表示の際にHTMLのタグを任意のコンポーネントに置き換えることができます。 主な使用想定は、個人ブログなどのコンテンツ部分です。コンテンツ部分は、MarkdownのファイルやヘッドレスCMSのリッチテキストで管理し、それをHTMLにしたものを表示していると思います。このライブラリを使用すれば、そのHTMLを表示する際、CSS in JS等のReactの技術を使って装飾したり、タイトルのようにaタグをSPA用の Link コンポーネントに置き換えたりすることができるようになります。 そして、このライブラリの一番の特徴は、その軽さです。minifyで3kb(v0.2.0時点)しかありません。類似ライブラリと比べて非常に軽量なのは、独自のパーサーを使わず、ブラウザJSのAPI DOMPerser を使用しているからです。 ライブラリ バンドルサイズ (minify) 備考 dom-parser-react 3kb 筆者作 html-react-parser 27kb rehype-react 42kb unifiedのプラグイン。計測時は、rehype-reactの他、unified、rehype-dom-parserを使用 ※上記は筆者調べ。rollup.jsでバンドルして計測。ただし、react、react-domのサイズは除外 使い方 それでは、実際の使用方法を紹介します。 NPMで公開していますので、インストールは、npm/yarnから行います。 npm i dom-parser-react # or yarn add dom-parser-react DOMParserReactを使用するには、コンポーネントをインポートして変換するHTML文字列を source で指定してください。 そのHTMLを表示するコンポーネントが作成されます。 import DOMParserReact from 'dom-parser-react' // import { renderToStaticMarkup as render } from 'react-dom/server' const htmlText = ` <h1>HTML Text</h1> ` const App = () => <DOMParserReact source={htmlText} /> render(<App />) // `<h1>HTML Text</h1>` 上記は dangerouslySetInnerHTML と結果がほぼ同じですので、今度はタグを置き換えてみましょう。 components プロパティに { 置換対象のタグ: 置換後のコンポーネント } という形式で指定します。下記は、h1 タグを Title コンポーネントに置き換える例です。 const htmlText = ` <h1>HTML Text</h1> ` const Title = (props) => <div className="title"> <h1 {...props} /> </div> const App = () => <DOMParserReact source={htmlText} components={{ h1: Title }} /> render(<App />) // `<div class="title"><h1>HTML Text</h1></div>` h1 が div.title で囲まれて表示されました。 今回は囲むだけでしたが、例えば、CSS in JSでスタイリングしたコンポーネントを指定したり、アンカーリンクのアイコン等を追加したりもできます。 また、複数種類のタグを置き換えたい場合は複数記載すれば対応可能です。 const components = { h1: Title, h2: SubTitle, p: Paragraph, } <DOMParserReact source={htmlText} components={components} /> Next.jsで使用する それでは、実際にNext.jsで使用してみましょう。 と言っても、上記のように components を指定するだけです。 import Link from 'next/link' import DOMParserReact from 'dom-parser-react' const components = { a: ({ href, ...props }) => ( <Link href={href}> <a {...props} /> </Link> ), } const Post = ({ htmlText }) => <DOMParserReact source={htmlText} components={components} /> もしかしたら、読者の中には本当に動くか疑問に思っている人がいるかもしれません。最初に説明したように DOMPerser はブラウザJSのAPIで、Next.jsのSSR/SSGが実行されるNode.jsにそのAPIはありません。 ですが、DOMParserReactは問題なく動作します。Node.jsでの実行時は、自動でJSDOMというライブラリをインポートし、DOMPerser をエミュレートします。これによってブラウザと同じように動作し、SSR/SSGが実行されます。もちろん、ブラウザではJSDOMはバンドルしませんので、軽量なサイズのままです。 最後に 上記の通りこのライブラリは、HTMLコンテンツを簡単に装飾することができ、ライブラリサイズも軽量でSSR/SSGにも対応しています。ぜひ、個人ブログなどに使用してみてください。 おまけ DOMParserReactを使用した、筆者のブログ Qiita記事を優先したため、最新の投稿はライブラリ使用前の去年のものになっています。そのうち、DOMParserReact作成時のこととかを書いていきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

バイバイ、サーバーサイドレンダリング。Prerender.ioーSEOを考慮したSPA

本記事は、掲載元で2日で23K「いいね」を獲得したSviat Kuzhelev氏による「Say goodby to Server Side Rendering. Prerender.io --- SPAs with SEO in mind.」(2021年9月21日公開)の和訳を、著者の許可を得て掲載しているものです。 バイバイ、サーバーサイドレンダリング。Prerender.ioーSEOを考慮したSPA image is taken from this resource はじめに 「完璧とは、付け加えるものがない時ではなく、取り除くものがない時に達成される。」アントワーヌ・ドゥ・サン=テグジュペリ 私たちは皆SPA❤️です。どんな形でも良いのです。React、Vue、Angular、さらにはVanillaコーディングがどのプラットフォームで使用されたかなど関係ありません。 顧客体験を損なうことなく、JavaScriptウェブサイトの静的HTMLをクローラーに提供することで、上位表示を実現したいですか?そのアイディアをここで紹介します。 今までとこれから サーバーサイドレンダリング(SSR)は、何十年にもわたりうまく機能していました。解析済みのHTMLデータをクライアントサイドに送信すると、ブラウザは追加のジョブなしで、UI上の内容を簡単に表示します。変更があった場合は、ページを完全にリロードして、バックエンドから再初期化する必要があります。 ?ちょっと待ってください。私はSPA開発が大好きです!SSRはSPAとうまく動作するので、それが問題なのではありません。next.jsのようなフレームワークは数多くありますし、すべてをゼロから作成することもできるため、SEO対策を考慮した静的ウェブサイトを常に構築できます。一件落着です? SSRの話を聞いて頭に浮かぶ唯一の懸念は、クライアントサイドレンダリング(CSR)に関することでしょう。そう、皆CSRが大好きです。でもここでは、SSRとCSRの違いをすべて説明するために寄り道はしません(さもなくば、この記事を読み始めなくなるでしょう)。 CSRの使用に関する共通の考え方(ワクワクします)は、複雑なUIシステムのアーキテクチャのバランスを保ちながら、認知リソースを大幅に削減できるというものです。UIで結果をすぐに確認でき、バックエンド関連の問題のデバッグに費やす時間を短縮します。どういうことか説明しましょう。 必要なもの ライブラリ/フレームワークのReactを私くらい好きだといいのですが。DOM APIはサーバー上でアクセスできないため、既知のReact.renderDOM()メソッドはここでは使えなくなります。(SSR好きの人は)「さあ、多くのコードベースを書き直してください」と言うでしょう!? 少なくともReact.hidate()に変更してアプリのクラッシュを解消し、(必ず必要になる)SSR関連の勉強をどんどん始めましょう。それから、状態管理について学ぶことになるでしょう…。 ?SSR対応のSPAベースアプリケーションの作成が必要な場合、状態管理はかなり大変です。 reduxなどの状態管理用ツールを追加しましょう。UIマークアップだけでなく、バックエンドから挿入された後、最初のUIレンダリングで追加された初期化データも同期している間に、面倒で嫌になるでしょう。 テクノロジーを使えば簡単にできるのに、なぜこのような多くの途方に暮れそうな処理をする必要があるのでしょうか。 もし、左手でCSRのコードを書き、右手で好きな飲み物を飲めるとしたら?(またはその逆??)もし、SSRが原因でアプリ内の何かがクラッシュし始めても、慌てることなくDOM APIにアクセスできるとしたら?もし…このリストはスパゲッティのように長くなるかもしれませんが、そこから解放されることにワクワクするなら、要点に進みましょう。 そろそろ変わり目だ この10年間、静的アプリがいかに人気だったかをGoogleが理解した時点で、それが現実になりました。より簡単に、より速く、処理するために需要が生まれたのです。これはウェブコーディングの大きな飛躍であり、SEOも進化すべきです。2015年発表のGoogleのクロールロボットは、動的クライアントサイドのウェブページ(静的SPAなど)を解析できるようになりました。 とはいえ、時々、落とし穴があるかもしれません。ロボットがタイムアウトする前に、ページに必要なリソース(画像、CSS、データ)を取得できる保証はありません:( Googleが、ダイナミックレンダリングと呼ばれるウェブページのスクリーンショット(.png/.jpg/.webp/.svgファイルではない;))を開発者が作成できると発表したすぐ後に、完全にプリレンダリングされたHTMLを保存できるようになりました。 ?ダイナミックレンダリングーMicrosoftが推奨するJavaScriptのレンダリング方法。詳細については、開発者向けドキュメントを参照してください。 ロボットがウェブページにアクセスすると、バックエンドミドルウェアがリクエストをハイジャックして、SEOに最適化された実際のスクリーンショットをクローラーエンジンに送り返します。エンドユーザーは、通常のHTML+CSS+JSパックを展開します。一般的に、サーバーは人間とロボットを区別できるようになり、人間には完全な体験を、ロボットには軽量のHTMLバージョンを提供します。 ダイナミックレンダリングの設定方法 基本的な設定はとても簡単です。クローラーエンジンのリストを作成し、リクエストヘッダをテストするだけです。 export const botUserAgents = [ 'googlebot', 'bingbot', 'linkedinbot', 'mediapartners-google', ]; 次に、ユーザーエージェントがデスクトップコンテンツとモバイルコンテンツのどちらをリクエストしているか判断します。ダイナミックサービングを使用して、適切なデスクトップ版またはモバイル版を提供します。以下は、判断の設定例です。 const isPrerenderedUA = userAgent.matches(botUserAgents) const isMobileUA = userAgent.matches(['mobile', 'android']) if (!isPrerenderedUA) { // serve regular, client-side rendered content } else { servePreRendered(isMobileUA) // serve the mobile version } これは、よくある仕組みの概念です。詳細については、Google公式ページを参照してください。 Prerender.ioの設定 もし自分ですべて作成するのが苦手なら、Prerender.ioの検討をお勧めします。かなり直感的なサービスで、作業が簡単です。 まず、パッケージを自分のリポジトリにインストールします。 // ./ npm install prerender-node --save 次に、NodeJSベースのアプリ(expressJS、nextJSなど)をセットアップし、ミドルウェアを追加します。 // ./server/index.js app.use(require('prerender-node')); …または、prerender.ioのアカウントを持っている場合は、トークンを使用します。 // ./server/index.js app.use(require('prerender-node').set('prerenderToken', 'YOUR_TOKEN')); ほら、とても簡単でしょう? スケールアップ さらに詳しく知りたい場合は、GithubのPrerender.io公式リポジトリを掘り下げると良いでしょう。スナップショットのテスト、カスタマイズ、スナップショット保存のための独自サーバー作成に関する高度なリソースまであります? まとめ この記事が役に立てば嬉しいです?私が何か見逃していたり、SSRはまだ素晴らしい仕事ができると思ったりしたら、議論を持ちかけてください。 いつか、このSEO関連の混乱がすべてなくなり、AIが地球を支配するようになるでしょう。それが実現するまで:)、開発者の作業を楽にしてくれるPrerender.ioなどの関係者に感謝しましょう! それまでの間、役に立つ記事があります。 ? React 2021でプログレッシブ画像セットを扱う方法 ?未来への道。速度を落とさずにReactでRetinaとWebP画像を統合する ? Reactで画像セット作成を自動化する方法 ❤️2021年デザイナー/開発者向けプログレッシブ画像セット作成の自動化フロー それではまた!? 翻訳協力 この記事は以下の方々のご協力により公開する事ができました。改めて感謝致します。 Original Author: Sviat Kuzhelev Original Article: Say goodby to Server Side Rendering. Prerender.io --- SPAs with SEO in mind. Thank you for letting us share your knowledge! 選定担当: @gracen 翻訳担当: @gracen 監査担当: - 公開担当: @gracen ご意見・ご感想をお待ちしております 今回の記事はいかがでしたか? ・こういう記事が読みたい ・こういうところが良かった ・こうした方が良いのではないか などなど、率直なご意見を募集しております。 頂いたお声は、今後の記事の質向上に役立たせて頂きますので、お気軽に コメント欄にてご投稿ください。Twitterでもご意見を受け付けております。 皆様のメッセージをお待ちしております。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npm install / yarn add したら tsc が通らなくなった話

React のカスタムフックをテストするために @testing-library/react-hooks をインストールしました。 そして tsc を実行すると types 周りがコケる現象が発生しました。 $ tsc --noEmit node_modules/@testing-library/react-hooks/node_modules/@types/react/index.d.ts(3092,14): error TS2300: Duplicate identifier 'LibraryManagedAttributes'. node_modules/@testing-library/react-hooks/node_modules/@types/react/index.d.ts(3103,13): error TS2717: Subsequent property declarations must have the same type. Property 'a' must be of type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>', but here has type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'. ... 本記事はこのエラーの考察と解決方法を記録したものです。 TL;DR 違うバージョンの @types/* が解決されたので npm なら npm dedupe && npm install yarn なら npx yarn-deduplicate && yarn install1 を実行して一個にまとめること。 発生経緯 説明用のサンプルを公式から拝借しました。 こんな感じにテストケースを書いています。 useCounter.spec.ts import { renderHook, act } from '@testing-library/react-hooks' import useCounter from './useCounter' test('should increment counter', () => { const { result } = renderHook(() => useCounter()) act(() => { result.current.increment() }) expect(result.current.count).toBe(1) }) このテストケースを jest から実行するときはなんの問題もありませんが、tsc を実行したときだけコケました。 問題の原因 プロジェクトの構造はこんな感じにしています。 ├── README.md ├── src │ ├── components <- React のコンポーネントを置くディレクトリ │ │ └── MyComponent.ts │ └── models <- カスタムフックとかを置くディレクトリ │ ├── useCounter.ts │ ├── useCounter.spec.ts tests フォルダを別に切り出すのではなく、テストファイルをテストしたいもののすぐ近くに置く構成にしています。 そして tsconfig.json の include フィールドに src 以下を追加すると tsconfig { "include": [ "src/**/*" ] } tsc を実行する時ファイルパスを指定しなくてもOKになります。 $ tsc --noEmit この構成にしているため、tsc の実行時は src 以下のソースファイルとテストファイルの両方を見るようになっています。 今回のエラーメッセージ $ tsc --noEmit node_modules/@testing-library/react-hooks/node_modules/@types/react/index.d.ts(3092,14): error TS2300: Duplicate identifier 'LibraryManagedAttributes'. node_modules/@testing-library/react-hooks/node_modules/@types/react/index.d.ts(3103,13): error TS2717: Subsequent property declarations must have the same type. Property 'a' must be of type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>', but here has type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'. ... はずばり @types/react/index.d.ts を読み込んだ時、型が二重定義されていたため起こったエラーです。 型の二重定義 node_modules/@testing-library/react-hooks/node_modules/@types/react/index.d.ts を見ればわかりますが、 global 以下に定義されたものがコンフリクトしています。 これが起こったのは @types/react が複数存在しているためです。 $ yarn list @types/react yarn list v1.22.11 warning Filtering by arguments is deprecated. Please use the pattern option instead. ├─ @testing-library/react-hooks@7.0.2 │ └─ @types/react@17.0.27 └─ @types/react@17.0.15 プロジェクトからは @types/react@17.0.15 を直接利用していますが、 @testing-library/react-hooks@7.0.2 からは違うバージョンの @types/react@17.0.27 が間接的に import されています。 tsc を実行するときは、プロジェクトから直接依存している types は自動的に読み込まれますが、テストファイルの import { renderHook, act } from '@testing-library/react-hooks' のこれも @testing-library/react-hooks 内の @types/react@17.0.27 を読み込んでいます。 この2つの @types/react は同時に global に型を定義しているため、同じ名前の型が二重定義されてコンフリクトしています。 複数のnode_modules npm / yarn を利用するプロジェクトはこのように、node_modules 以下にさらに node_modules を保つことができます。 ./node_modules ./node_modules/make-dir/node_modules ./node_modules/npm/node_modules/resolve/test/shadowed_core/node_modules ./node_modules/eslint-plugin-react/node_modules/resolve/test/resolver/symlinked/_/node_modules 例えば a と b から複数のライブラリのから依存されているライブラリ c はもし同じバージョンに解決できたら、共通でルート以下の ./node_modules にその共用のライブラリを置かれますが、 同じバージョンに解決されなかった場合はそれぞれのディレクトリ以下 ./node_modules/a/node_modules/c ./node_modules/b/node_modules/c に置くことになります。 今回の @testing-library/react-hooks でいうと、実は "@types/react": ">=16.9.0", を満たせば問題ないので、プロジェクトの @types/react と同じバージョンに解決することができたはずです。 解決方法 ここに解決方法が書かれています。 かいつまんで言うと、 npm なら npm dedupe && npm install yarn なら npx yarn-deduplicate && yarn install1 を実行したら、2つバージョンが違った @types/react を一個に解決できて、今回のエラーを修正できました。 $ yarn list @types/react yarn list v1.22.11 warning Filtering by arguments is deprecated. Please use the pattern option instead. └─ @types/react@17.0.27 なぜか yarn dedupe だと効かなくて npx yarn-deduplicate にしないといけないようです ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】【環境構築】Docker + Ruby on Rails + ReactでSPA化

RailsとReactを使ってSPA化したWebアプリをDocker環境で作ろうとして、結構苦労したため、自分用のメモとして残します。 各種ファイルの用意 プロジェクト用のフォルダを用意して、Rails側のapiフォルダとReact側のfrontフォルダに分ける。 プロジェクト用フォルダの直下に、docker-compose.ymlを置く。 $ mkdir -p ~/project/rails-react-app $ cd ~/project/rails-react-app $ mkdir ~/project/rails-react-app/api $ touch ~/project/rails-react-app/api/Dockerfile $ touch ~/project/rails-react-app/api/entrypoint.sh $ touch ~/project/rails-react-app/api/Gemfile $ touch ~/project/rails-react-app/api/Gemfile.lock $ mkdir ~/project/rails-react-app/front $ touch ~/project/rails-react-app/front/Dockerfile docker-composeファイル作成 $ cd ~/project/rails-react-app $ vim docker-compose.yml docker-compose.yml version: '3' services: db: image: postgres volumes: - postgres-data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: 'postgres' api: build: context: ./api/ dockerfile: Dockerfile command: /bin/sh -c "rm -f /rails-react-app/tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" image: rails:dev volumes: - ./api:/rails-react-app - ./api/vendor/bundle:/rails-react-app/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development ports: - 3001:3000 depends_on: - db front: build: context: ./front/ dockerfile: Dockerfile volumes: - ./front:/usr/src/app command: sh -c "cd rails-react-app && yarn start" ports: - "8000:3000" volumes: postgres-data: driver: local RailsとReactでDockerfileを別々に用意したので、buildの部分でDockerfileの場所を指定したのがポイント。 docker-compose.yml api: build: context: ./api/ dockerfile: Dockerfile front: build: context: ./front/ dockerfile: Dockerfile Rails用のファイルの用意 ~/project/rails-react-app/api/Dockerfile FROM ruby:2.5 RUN apt-get update -qq && apt-get install -y nodejs postgresql-client RUN mkdir /rails-react-app WORKDIR /rails-react-app COPY Gemfile /rails-react-app/Gemfile COPY Gemfile.lock /rails-react-app/Gemfile.lock RUN bundle install COPY . /rails-react-app # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 # Start the main process. CMD ["rails", "server", "-b", "0.0.0.0"] Gemfile source 'https://rubygems.org' gem 'rails', '~>5' Gemfile.lock これは空ファイルを用意。 entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /rails-react-app/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" React用のファイルの用意 ~/project/rails-react-app/front/Dockerfile FROM node:12.22-alpine3.11 WORKDIR /usr/src/app コマンドの実行 React: create-react-appを用いて環境構築(参考記事: https://blog.web.nifty.com/engineer/2714 ) $ docker-compose run api rails new . --force --no-deps --database=postgresql --api $ docker-compose build $ docker-compose run --rm front sh -c "npm install -g create-react-app && create-react-app rails-react-app" api/config/database.ymlの書き換え api/config/database.yml default: &default adapter: postgresql encoding: unicode host: db username: postgres password: postgres pool: 5 development: <<: *default database: rails-react-app_development test: <<: *default database: rails-react-app_test $ docker-compose up $ docker-compose run api rake db:create $ docker-compose run api rake db:migrate これで環境構築完了! Rails: localhost:3001 React: localhost:8000 ▼参考にさせていただいた記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

requestAnimationFrameで端末負荷を測定する

背景 最近ブラウザから端末負荷を測定したい場面に出くわしました。 弊社ではwebrtcを扱っていて、ライブ中の端末負荷の変化を測定したかったのですが、残念ながらブラウザAPIは用意されていません。 そこでrequestAnimationFrameapiを使用して画面の描画回数を測定することで、間接的に端末負荷の指標とすることにしました。 Window.requestAnimationFrame() requestAnimationFrameは本来はアニメーションを実装するために使用するAPIです。 ブラウザにアニメーションを行いたいことを知らせ、指定した関数を呼び出して次の再描画の前にアニメーションを更新することを要求します。 ブラウザが画面を描画するたびにコールされ、そのタイミングで任意の関数を実行したい場合に使用するのが一般的かと思います。 今回は、ブラウザが画面を描画するたびにコールされることを利用して、描画1回あたりに要した時間を計測しています。 1回あたりに要した時間が分かれば、画面描画のfpsも計算することができます。 つまり、端末負荷が上がって画面がカクカクする状態になればfpsも下がることを利用して、端末負荷の指標としましました。 正常時の基準とべきfpsは60ですが、注意も必要です。 このコールバックの回数は、たいてい毎秒 60 回ですが、一般的に多くのブラウザーでは W3C の勧告に従って、ディスプレイのリフレッシュレートに合わせて行われます。 特にゲーミング用のディスプレイなどはリフレッシュレートが高く設定されている場合もあり、必ずしも60を基準にできるとは限りません。 そもそももっと直接計測する方法はないのか? google meetはどうやって端末負荷を計測しているのか? googleの公式ではないので確かではないですが、chrome extensions apiを使用していると思われます。 試しにgoogle meetをchrome以外のブラウザでアクセスしてみると、cpu使用率のグラフが表示されなくなっています。 かなり有力そうです。 残念ながらextensionsのapiをfrontendのコードから呼び出すことはできないので断念しました。 googleは外部に公開していない内部apiを通じて取得しているのではなかろうかと推測されます。 実装 requestAnimationFrame()のコールバック関数の中で、再起的にrequestAnimationFrame()を呼び出しています。 60回ごとに平均を計算しています。(60はあくまで目安です) ポイントはInfinityです。理由が分かっていないのですが、時々lastCalledTimeとcurrentTimeが一致してしまうことがあり、fpsがInfinityになってしまいます。 これを避けるために、Infinityを除外する処理を加えています。 const measureFps = () => { let lastCalledTime: number | undefined; let counter = 0; let fpsArray: number[] = []; const calcFps = () => { const currentTime = Date.now(); if (lastCalledTime === undefined) { lastCalledTime = currentTime; } else { const delta = (currentTime - lastCalledTime) / 1000; lastCalledTime = currentTime; const fps = 1 / delta; if (counter >= 60) { const sum = fpsArray.reduce((a, b) => a + b); const average = sum / fpsArray.length; counter = 0; fpsArray = [fps]; console.log({ average }); } else if (fps !== Infinity) { fpsArray.push(fps); counter++; } } requestAnimationFrame(calcFps); }; requestAnimationFrame(calcFps); }; 参考: https://gist.github.com/WA9ACE/d51659371a345a9327bd それほど多くもないですが、Infinityを弾いた分は誤差になるので根本解決はしたいなと思ってます... それにしてもネイティブアプリはいいなぁ。読んで頂きありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む