3
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

冬空の中でReactの世界観をちょっとだけ広げたかった

それはただ美しかった。

コンポーネントという呪縛がある一方で、開発者の自由さを守りながら宣言的UIによる美しい構成が可能となった。

数千行に渡るDOMイベント管理の苦行から開放された。

見上げればそこにはReactがあった。

まるでオリオン座のように。

だがしかしBUT

仮想DOMというバーチャルな世界を実現するために、「Hello World」を表示するには現実的にWebpackやRollup等のバンドルが必要になった。

これを私は「コンポーネントの呪縛」と勝手に思っている。(悪気はない。)

初学者だった頃のjQuery使いの私にはしばらく難しかった。

とりあえずこれのようなminimalなスターターを書いて、そっと公式ドキュメントを閉じた。

数年の月日を経て

複雑なステート管理にはReduxが有名に、事実上のデファクトとなった。

やがてHooksが誕生した。

Hooksの登場により再度公式ドキュメントを開き、そしてそれをそっと閉じた。

どうしてもReduxのような管理方法が必要になった

JSerとしての経験値が上がるにつれて、jQuery思想の自分にはやがて限界が訪れた。

コンポーネントである以上基本は親が子を教育するのである。

したがってグローバルなstoreへどのコンポーネントからもアクセスできる機構が必要になったのだ。

dispatchめ。(悪気はない。)

おや...

結局グローバルstoreは親から子へ移譲しているだけではないか。

親から子へデータを渡している?

それは究極の発想ではview template engineに近しいのではなかろうか。

JSならejsやhandlebars、PHPあたりではbladeとかそういった、あの{{ data }}ではなかろうか。

あの古き良き、サーバーから受け取ったデータをビューへ渡す方法ではなかろうか。

Reactを view template engine にできないだろうか

調べたらあった。

サーバーからデータを渡す:

// views/index.jsxをレンダリング
res.render('index', { message: 'Hello World' });

するとviews/index.jsxのpropsにデータが伝搬される:

var React = require('react');

function IndexPage(props) {
  return <div>{props.message}</div>;
}

module.exports = IndexPage;

素晴らしいではないか。

と思ったら

It renders static markup and does not support mounting those views on the client.

静的マークアップをレンダリングするだけでReactはマウントされないだと。。。

Reactにする意味はあるのか?(ビューの数千行が数百行くらいにはなるかもしれないが。。。)

JSのイベントはどうする?

CSS-in-JSはどうなる?

どうしてもReactをマウントしたかった

$ npm install --save @react-ssr/express
package.json
{
  "scripts": {
    "start": "node server.js"
  }
}
server.js
const express = require('express');
const register = require('@react-ssr/express/register');

const app = express();

(async () => {
  // Reactビューエンジンとして`.jsx`を登録
  await register(app);

  app.get('/', (req, res) => {
    const message = 'Hello World!';
    res.render('index', { message });
  });

  app.listen(3000, () => {
    console.log('> Ready on http://localhost:3000');
  });
})();
views/index.jsx
export default function Index({ message }) {
  return <p>{message}</p>;
}

そう、Reactをそのまま利用できるビューエンジンにしたかった

views/index.jsx
import React from 'react';

const IndexPage = (props) => {
  const [message, setMessage] = React.useState('waiting...');

  const onClick = () => setMessage('This is a react-ssr!');

  return (
    <React.Fragment>
      <button onClick={onClick}>Click Me</button>
      <p>Message from state: {message}</p>
    </React.Fragment>
  );
};

export default IndexPage;

スクリーンショット 2019-11-30 22.12.58.png

↓↓↓

スクリーンショット 2019-11-30 22.13.27.png

想像してみてほしい

server.js
const posts = [
  { id: 1, body: 'This is a first post.' },
  { id: 2, body: 'This is a second post.' },
  { id: 3, body: 'This is a last post.' },
];

app.get('/', (req, res) => {
  res.render('index', { posts });
});

app.get('/posts/:postId', (req, res) => {
  const { postId } = req.params;
  const post = findById(postId);
  res.render('post', { post });
});

この古き良きデータ流しを、

views/index.jsx
import React from 'react';

const IndexPage = ({ posts }) => {
  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

export default IndexPage;

↓↓↓

http://localhost:3000

スクリーンショット 2019-11-30 22.23.13.png

views/post.jsx
import React from 'react';

const PostPage = ({ post }) => {
  return (
    <React.Fragment>
      <p>{post.body}</p>
    </React.Fragment>
  );
};

export default PostPage;

↓↓↓

http://localhost:3000/posts/1

スクリーンショット 2019-11-30 22.23.55.png

Reactで扱える喜びを...!!!

NEXT.jsとの比較

データの非同期取得

クライアント側のデータ非同期取得は、どちらもReact.useEffect()で非同期関数を処理する必要がある。

NEXT.jsの場合

pages/index.jsx
import React, { useState, useEffect } from 'react';

const HomePage = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    // componentDidMount like
    fn() {
      const res = await fetch('http://localhost:4000/api/posts');
      const posts = await res.json();
      setPosts(posts);
    }
    fn();

    return () => {
      // componentWillUnmount like
    };
  }, []);

  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

export default HomePage;

しかしこれではSSRされないのでSEO対策にならない。

仕方なく以下のようなNEXT.js wayに従って書き直す。

pages/index.jsx
import React from 'react';

const HomePage = ({ posts }) => {
  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

HomePage.getInitialProps = async () => {
  const res = await fetch('http://localhost:4000/api/posts');
  const posts = await res.json();
  return { posts };
};

export default HomePage;

だいぶすっきりしたものの、非同期データをSSRさせたい場合、このgetInitialPropsという魔法の呪文を唱える必要がある。

react-ssrの場合

すでに紹介したように、view template engineとして機能するため、APIサーバーから取得したデータをそのままビューへ渡せば良い。

そもそも同一サーバーならAPIサーバーを建てる必要もなく、直接DBから取得したデータをビューへ渡せばpropsから取得データへアクセスできる。

server.js
(async () => {
  const posts = await fetchFromDbOrAPIServer();

  app.get('/posts', (req, res) => {
    res.render('posts', { posts });
  });
})();
views/index.jsx
import React from 'react';

const HomePage = ({ posts }) => {
  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

export default HomePage;

ユーザー認証

NEXT.jsの場合

iaincollins/next-authというライブラリがあるが、これもクライアント側ではgetInitialPropsを利用する。

しかしサーバー側の使い方が、NEXT.js wayなサーバーの書き方をする必要があったり、そもそもドキュメントが充分とは思えないのであまり使う気にはなれなかった。

server.js
const next = require('next')
const nextAuth = require('next-auth')
const nextAuthConfig = require('./next-auth.config')

require('dotenv').load()

const nextApp = next({
  dir: '.',
  dev: (process.env.NODE_ENV === 'development')
})

nextApp.prepare()
.then(async () => {
  const nextAuthOptions = await nextAuthConfig()
  const nextAuthApp = await nextAuth(nextApp, nextAuthOptions)  
  console.log(`Ready on http://localhost:${process.env.PORT || 3000}`)
})
.catch(err => {
  console.log('An error occurred, unable to start the server')
  console.log(err)
})

↑を読んでこれから何が起こるのかわかるエスパーはいるのだろうか。

react-ssrの場合

react-ssrはExpressのview template engineとして振る舞うので、古き良きExpress wayなユーザー認証を利用できる。

例えばメールアドレス・パスワードによる認証であればpassportpassport-localを利用する。

ソースコードは長くなるため割愛するが、その分サーバー側で柔軟な調整ができる。

  • ユーザーデータのシリアライズ・デシリアライズ方法の調整
  • sessionの保存方法の調整(たとえばDBにsession情報を保存する等)
  • リダイレクトURLの柔軟な調整(条件分岐等)

サーバー側のバグはクリティカルであることが多い。
「なんでも屋」すぎるライブラリには頼らず、小さくて優秀なライブラリを複数利用しつつ細かい調整は自前で実装するのがベストプラクティスだと筆者は考える。

react-ssrの実装サンプル

examples/with-jsx-emotion

スクリーンショット 2019-12-01 5.25.35.png

examples/with-jsx-material-ui

スクリーンショット 2019-12-01 5.27.23.png

examples/with-jsx-semantic-ui

スクリーンショット 2019-12-01 5.29.42.png

examples/styled-components

スクリーンショット 2019-12-01 5.31.16.png

さいごに

最終的にreact-ssrの宣伝っぽくなってしまってすみません。

ですが古き良きビューエンジンの形でReactを利用したいと思ったことはありませんか?

小規模開発でもわざわざAPIサーバーとクライアントを分けなければならない煩雑さを感じたことはありませんか?

そういった疑問を感じたことがある方にとって、少しでも可能性が広がる記事になれば幸いです。

筆者の稚拙な技術記事であるため、間違い等はコメントにてご指摘ください。

ここまで読んでくださりありがとうございました。

ちなみに私はベテルギウスが好きです。(一番高いところにあるから。)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
3
Help us understand the problem. What are the problem?