20190127のReactに関する記事は3件です。

これから始めたNext.js

Next.jsを触り始めたのでその過程を残しておこうと思います。書いたコードはgithubに上げてあります(natsuhikok/next-react)。Next.jsのexampleが充実してて迷わずに進めるところが好きでした。

インストールして動かしてみます

インストールとpages/index.jsだけで動くのでcreate-react-appするより、らくちんかもしれません。

$ npm install --save next react react-dom

import React from 'react'が必要ないのは少しきもいです。

pages/index.js
export default () => <div>これから始めたNext.js</div>;

起動スクリプトは以下としました。

package.json
"scripts": {
   "start": "next"
 },

$ npm run startでlocalhost:3000にnext.jsアプリが起動します。この時点でホットリロードも効いています。

後述にある_document.jsなどを新しく追加した場合にはアプリのリロードが必要です。変更したけど反映されないなどの場合は、一度アプリを起動し直してみると変なところでつまずかずに済むと思います。

ビルドとproductionサーバー

ビルドしての本番運用がつらいとNext.jsでSSRする意味が薄いので先に試してみます。

package.json
"scripts": {
  "start": "next",
  "build": "next build",
  "production": "next start"
},

next buildでビルドします。next startでbuildされた.nextからサーバーを起動してくれます。pm2による永続起動は以下です。

package.json
"scripts": {
    "start": "next",
    "build": "next build",
    "production": "next start",
    "pm2": "next build && pm2 start npm --name 'next' -- run production"
}

これで安心して他の部分を試していけます。

styled-components

NextJSでstyled-componentsを試したところ特定の条件下で以下のエラーがでてしまいました。

Expected server HTML to contain a matching <tag> in <tag>...

ExpressなどでReactをSSRしたときにも見かけるあれです。これはbabel-plugin-styled-componentsをbabelに追加することで解決できます。

 .babelrc
{
  "presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true }]]
}

これでエラーは解決されますが、初回のロード時にスタイルが遅れてでるよ。状態になるので_document.jsを追加します。公式のexample 通りです。

_document.js
import Document from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
  static async getInitialProps (ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;
    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
        });
      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: [...initialProps.styles, ...sheet.getStyleElement()],
      };
    } finally {
      sheet.seal();
    }
  }
}

createGlobalStyleはどこでするべきか

Metaデータを納めるコンポーネントを作って各ページで読み込むのがよさそうです。_app.jsで読み込むこともできそうですが_document.js, _app.jsはできる限り手を入れない方向で進めています。

components/Meta.js
import Head from 'next/head';

export default ({ title }) => (
  <>
    <Head>
      <title>{title}</title>
      <meta name="viewport" content="initial-scale=1.0, width=device-width" />
      <meta charSet="utf-8" />
    </Head>
    <GlobalStyle />
  </>
);

import { createGlobalStyle } from 'styled-components';
import normalize from 'styled-normalize';

const GlobalStyle = createGlobalStyle`
  ${normalize}
  * { box-sizing: border-box; }
  html {
    font-size: 62.5%;
    font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Yu Gothic", YuGothic, Verdana, Meiryo, "M+ 1p", sans-serif;
  }
  input { border-radius: 0; }
  body {
    font-size: 1.4rem;
  }
  p, h1, h2, h3, h4, h5, h6 { margin: 0; }
  ul {
    margin: 0;
    padding: 0;
    list-style-type: none;
  }
`

GlobalStyleはタイミングで別コンポーネントにすることが多いです。

redux

NextJSでReduxを使ってみます。生React+reduxとの違いは_app.jsくらいかと思います。公式のexamleを参考にしました。

_app.js
import { Provider } from 'react-redux';
import App, { Container } from 'next/app';
import withRedux from 'next-redux-wrapper';
import makeStore from '../store/makeStore';

class ExtendedApp extends App {
  static getInitialProps = async ({ Component, ctx }) => ({
    pageProps: (Component.getInitialProps ? await Component.getInitialProps(ctx) : {})
  })
  render () {
    const { Component, pageProps, store } = this.props;
    return (
      <Container>
        <Provider store={store}>
          <Component {...pageProps} />
        </Provider>
      </Container>
    );
  }
}

export default withRedux(makeStore)(ExtendedApp);

store/makeStore.js
import { combineReducers, createStore } from 'redux';
import { message } from './message';

const rootReducer = combineReducers({ message });

export default initialState => createStore(rootReducer, initialState);

recompose

NextJSの拡張であるgetInitialPropsrecomposeで扱います。

pages/index.js
import { connect } from 'react-redux';
import { compose, withState, setStatic, pure, withHandlers } from 'recompose';

import Navigation from '../components/Navigation';
import Meta from '../components/Meta';
import { updateMessage } from '../store/message';

const Index = ({ message, handleClick, inputValue, handleInputChange }) => (
  <>
    <Meta title="これから始めたNext.js" />
    <Navigation />
    <Headline>これから始めたNext.js</Headline>
    <p>{message}</p>
    <input value={inputValue} onChange={handleInputChange} />
    <button onClick={handleClick}>submit</button>
  </>
);

export default compose(
  setStatic('getInitialProps', async props => {
    const { isServer } = props;
    if(!isServer) {
      console.log(props);
    }
  }),
  withState('inputValue', 'setInputValue', ''),
  connect(
    ({ message }) => ({ message: message.message }),
    dispatch => ({
      updateMessage: value => dispatch(updateMessage(value)),
    }),
  ),
  withHandlers({
    handleClick: ({ setInputValue, updateMessage, inputValue }) => () => {
      updateMessage(inputValue);
      setInputValue('');
    },
    handleInputChange: ({ setInputValue }) => e => setInputValue(e.target.value),
  }),
  pure,
)(Index);

import styled from 'styled-components';
const Headline = styled.h1`
  font-size: 32px;
  line-height: 1.8;
  border-bottom: 4px dotted blue;
`;

Dynamic URL

user/:idのような動的なルーティングが必要です。残念ながらNextJSでこれを実現するためにはserver.jsを追加するなど自前でカスタムサーバー・ルーティングする必要があります。ここでは最小限で実装できるuser?id=idの方式を試してみます。気持ちダサいですがSEO的には問題のない方法です。

queryはgetInitialPropsから取得できるのでこれをViewに渡します。

pages/users.js
import { compose, setStatic, pure } from 'recompose';
import Navigation from '../components/Navigation';
import Meta from '../components/Meta';

const Posts = ({ id }) => (
  <div>
    <Meta title="これから始めたNext.js" />
    <Navigation />
    <h1>これは{id}番目のユーザーページです。</h1>
  </div>
);

export default compose(
  setStatic('getInitialProps', async props => {
    const { query: { id } } = props;
    return { id };
  }),
  pure,
)(Posts);

queryを指定したリンクはnext/Linkで作ることができます。

components/Navigation.js
import Link from 'next/link';

export default () => (
  <ul>
    <li><Link href="/"><a>home</a></Link></li>
    <li><Link href="/about"><a>About</a></Link></li>
    <li>
      <Link href={{ pathname: '/users', query: { id: 3 } }}>
        <a>User</a>
      </Link>
    </li>
  </ul>
);

参考

zeit/next.js
Example app with styled-components
Redux example
how can I use next in pm2?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.jsで、余分なdivを作らない方法 ~HOCを使ってみる~

HOCとは ドキュメントに載っていますが、最初のうちは目を通しても何を言っているのか分かりませんよね。自分もここを見落として、SPAを作っていましたが、この概念結構大切だなと思うので、今回まとめてみました。

環境情報

  • npm 5.6.0
  • node 8.11.1
  • react.js 16.4.2

HOCとは

Hocコンポーネントとは、Higher Order Componentの略。他のコンポーネントをラップする高次元のコンポーネントを指します。

簡単な例

import React, { Component } from 'react';
import header from './header';
import footer from './footer';

class Layout extends Component {
  render () { 
            return (
            <div>
                <header />
                <footer />
            </div>
        )
  }

export default Layout;

Reactではコンポーネントはdivで囲んであげなくてはあげません。しかし、クラスを付与する訳でもないのに、余分にdivタグを作るのは気持ち悪いなーと思うことはないでしょうか?
そんな時に、以下のようなHOCを作ってあげれば余分なdivタグが生まれなくなります。

src/hoc/Aux/Aux.js
const aux = (props) => props.children;

export default aux;
src/hoc/Layout/Layout.jsx
import React, { Component } from 'react';
import Aux from '../Aux/Aux';
import header from './header';
import footer from './footer';

class Layout extends Component {
  render () { 
    return (
      <Aux>
        <header />
        <footer />
      </Aux>
    )
  }

export default Layout;

このAuxコンポーネントを作り、Auxの子供にコンポーネントを渡すことによって、余分なdivタグを作られなくなります。

まとめ

すごく簡単な例ですが、他にもエラーのハンドリングなんかはこのHOCという概念を使ってあげて、コンポーネントをラップして実現させます。なかなか難しい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

typescript-eslintのversion1.x系がリリースされたので使ってみる

typescript-eslintのVersion 1.X系がリリースされましたので、サンプルアプリを使って試してみたいと思います。

typescript-eslintについて

typescript-eslint は、 @mysticatea さんの記事でもあったように、ESLintチームがTypeScriptのサポートする宣言があり、 1月20日にVersion 1.0.0がリリースされました。さらに3日後にはVersion1.1.0がリリースされています。

現在、TypeScriptが利用されているプロジェクトは、TypeScriptの普及により多くなってきたこともあり、後々TSLintからESLintへの移行が必須になるかと思います(しばらくは大丈夫だと思いますが...)。

そこで、今回は、TypeScript + React のサンプルアプリでtypescript-eslintを試してみたいと思います。また、既存のtslint.jsonを利用したサンプルも試してみたいと思います。

ESLintの導入

create-react-app を利用して、任意のサンプルを作ります。

typescript-eslint $ create-react-app typescript-eslint-react-sample --scripts-version=react-scripts-ts

サンプルアプリに移動して、以下のパッケージをインストールします。

  • eslint
  • @typescript-eslint/parser
  • @typescript-eslint/typescript-estree
  • @typescript-eslint/eslint-plugin
パッケージインストール
typescript-eslint $ yarn add eslint @typescript-eslint/parser @typescript-eslint/typescript-estree @typescript-eslint/eslint-plugin -D

ESLintの設定

eslintの必要最低限の設定をしていきます。

.eslintrc.json
{
  "root": true,
  "plugins": ["@typescript-eslint"],
  "parser": "@typescript-eslint/parser",
  "env": {
    "es6": true,
    "browser": true
  },
  "rules": {
    "no-console": "error" // console.logなどのコンソール出力に対してエラーを出す設定
  },
  "parserOptions": {
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  }
}
package.json
{
  // ...
  "scripts": {
    "eslint": "eslint -c ./.eslintrc.json 'src/**/*.{ts,tsx}'"
  },
  // ...
}

上記、設定が完了したら、コンソールに対するエラーが出るか確認してみます。

1____w_1_0_q_t_typescript-eslint-react-sample__fish_.png

servicework用のtsが引っかかっているのが確認できましたので、最低限の設定に問題がないことを確認できました。

tslintの設定を利用する

既存のプロジェクトでTSLintを利用している場合、その設定をそのまま利用したくなる場合があるかと思います。今回は、tslint.jsonがある場合を想定して試してみたいと思います。

まず、以下のパッケージを追加します。

  • @typescript-eslint/eslint-plugin-tslin

次に、 .eslintrc.json の設定を変更します。

.eslintrc.json
{
  "root": true,
  "plugins": ["@typescript-eslint", "@typescript-eslint/tslint"], // <- 追加
  // ..
  "rules": { // <- 追加(先ほどのconsoleのルールは削除して、このルールだけにします)
    "@typescript-eslint/tslint/config": [
      "warn",
      {
        "lintFile": "./tslint.json"
      }
    ]
  },
  "parserOptions": {
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    },
    "project": "./tsconfig.json"// <- 追加
  }
}

次に、 tslint.json を変更します。今回は、 var キーワードの使用に対するチェックを追加します。

tslint.json
{
  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
  "linterOptions": {
    "exclude": [
      "config/**/*.js",
      "node_modules/**/*.ts",
      "coverage/lcov-report/*.js"
    ]
  },
  "rules": { // <-追加
    "no-var-keyword": true
  }
}

次に、適当なファイルを修正します。今回は適当な関数を追加して、その中で var を宣言しています。

App.tsx
// ..
class App extends React.Component {
  public fn = () => { // <- 追加
    var arg = "aaa";
    return arg.toUpperCase();
  };
  public render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.tsx</code> and save to reload.
        </p>
        {this.fn()} // <- 追加
      </div>
    );
  }
}
// ..

最後に、 yarn eslint で追加したルールが機能しているか確認します。

1____w_1_0_q_t_typescript-eslint-react-sample__fish_.png

上がESLintの実行結果で、下が、TSLintの実行結果です。
同じエラーが出ていることが確認できたので、正常にtslint.jsonが機能していることが確認できました。

最後に

今回は必要最低限の設定のみで実践しましたが、ルールを整備したり、オリジナルのルールを追加する必要があります。が、導入自体のハードルはそんなに高くないように感じました。

今回作成したリポジトリは、こちらです。

ESLint、TSLintに対するご指摘などがありましたら、ご連絡ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む