20200906のReactに関する記事は14件です。

gatsby入門 チュートリアルをこなす 8. 公開するサイトの準備

gatsbyの作業履歴

gatsby入門 チュートリアルをこなす 0.開発環境をセットアップする
gatsby入門 チュートリアルをこなす 1. ギャツビービルディングブロックについて知る(1)
gatsby入門 チュートリアルをこなす 1. ギャツビービルディングブロックについて知る(2)
gatsby入門 チュートリアルをこなす 2. ギャツビーのスタイリングの概要
gatsby入門 チュートリアルをこなす 3. ネストされたレイアウトコンポーネントの作成
gatsby入門 チュートリアルをこなす 4. ギャツビーのデータ
gatsby入門 チュートリアルをこなす 5. ソースプラグインとクエリされたデータのレンダリング
gatsby入門 チュートリアルをこなす 6. 変圧器プラグイン※Transformer pluginsのgoogle翻訳
gatsby入門 チュートリアルをこなす 7. プログラムでデータからページを作成する
今回:gatsby入門 チュートリアルをこなす 8. 公開するサイトの準備
gatsby入門 ブログ作ってサーバーにアップしてみる

チュートリアルをこなす!

今回実施するgatsbyのチュートリアルはこちら
https://www.gatsbyjs.com/tutorial/part-eight/
基本はこれで最後かな?
チュートリアルの冒頭にこう書かれています。

この最後のセクションでは、Lighthouseと呼ばれる強力なサイト診断ツールを導入して、サイトを稼働させるための一般的な手順をいくつか説明します。途中で、Gatsbyサイトでよく使用するプラグインをいくつか紹介します。

※google翻訳です。
早速やっていきましょう。
ソースは前回作ったやつを使用します。

Preparing a Site to Go Live

Audit with Lighthouse

何やら監査をするそうな。
まずサイトの本番ビルドが必要のようです。

Create a production build

まず、すでに開発サーバが起動している場合は停止する。
そして、以下コマンドを実行する。
gatsby build
そして
gatsby serve
そして以下にアクセス
http://localhost:9000
2020-09-06_22h52_26.jpg
OK!

Run a Lighthouse audit

続いてLighthouseテストを実行します。
手順は以下とチュートリアルに記載

1.Chromeシークレットモードでサイトを開いて、拡張機能がテストに干渉しないようにします。次に、Chrome DevToolsを開きます。

2020-09-06_22h57_05.jpg
2020-09-06_22h58_05.jpg
2020-09-06_22h59_34.jpg
なんか出てきた。
Generate reportを押します。
2020-09-06_23h01_07.jpg
またなんか出てきた。
なんか黄色がいっぱい。。。

Add a manifest file

LighthouseのProgressive Web App不足を見直すみたい。
そしてなんだかmanifestを作れば良いらしい。
そしてGatsby’s manifest pluginを使うみたい

Using gatsby-plugin-manifest

プラグインを以下コマンドでインストール
npm install --save gatsby-plugin-manifest
※サイト直下のディレクトリで実行
とりあえずサーバ停止して実行しました。
何やらicon.pngがsrc/images配下にいるみたい。
適当にどっかから持ってくる。
gatsby-config.jsを修正

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: `Pandas Eating Lots`,
  },
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/`,
      },
    },
    `gatsby-plugin-emotion`,
    `gatsby-transformer-remark`,
    {
      resolve: `gatsby-plugin-typography`,
      options: {
        pathToConfigModule: `src/utils/typography`,
      },
    },
    ↓ここから
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `GatsbyJS`,
        short_name: `GatsbyJS`,
        start_url: `/`,
        background_color: `#6b37bf`,
        theme_color: `#6b37bf`,
        // Enables "Add to Homescreen" prompt and disables browser UI (including back button)
        // see https://developers.google.com/web/fundamentals/web-app-manifest/#display
        display: `standalone`,
        icon: `src/images/icon.png`, // This path is relative to the root of the site.
      },
    },
    ↑ここまで追記
  ],
}

※過去チュートリアルの場合です。

Add offline support

なんかpwaにservice workerがいるみたい。
Gatsby’s offline pluginをインストールします。
プラグインを以下コマンドでインストール
npm install --save gatsby-plugin-offline
gatsby-config.jsを修正

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: `Pandas Eating Lots`,
  },
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/`,
      },
    },
    `gatsby-plugin-emotion`,
    `gatsby-transformer-remark`,
    {
      resolve: `gatsby-plugin-typography`,
      options: {
        pathToConfigModule: `src/utils/typography`,
      },
    },
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `GatsbyJS`,
        short_name: `GatsbyJS`,
        start_url: `/`,
        background_color: `#6b37bf`,
        theme_color: `#6b37bf`,
        // Enables "Add to Homescreen" prompt and disables browser UI (including back button)
        // see https://developers.google.com/web/fundamentals/web-app-manifest/#display
        display: `standalone`,
        icon: `src/images/icon.png`, // This path is relative to the root of the site.
      },
    },
    `gatsby-plugin-offline`,←これ追記
  ],
}

Add page metadata

ページにメタデータを追加します。
またまたプラグインをインストール

Using React Helmet and gatsby-plugin-react-helmet

以下コマンドを実行
npm install --save gatsby-plugin-react-helmet react-helmet
gatsby-config.jsを修正

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: `Pandas Eating Lots`,
    description: `A simple description about pandas eating lots...`,←これ追記
    author: `gatsbyjs`,←これ追記
  },
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/`,
      },
    },
    `gatsby-plugin-emotion`,
    `gatsby-transformer-remark`,
    {
      resolve: `gatsby-plugin-typography`,
      options: {
        pathToConfigModule: `src/utils/typography`,
      },
    },
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `GatsbyJS`,
        short_name: `GatsbyJS`,
        start_url: `/`,
        background_color: `#6b37bf`,
        theme_color: `#6b37bf`,
        // Enables "Add to Homescreen" prompt and disables browser UI (including back button)
        // see https://developers.google.com/web/fundamentals/web-app-manifest/#display
        display: `standalone`,
        icon: `src/images/icon.png`, // This path is relative to the root of the site.
      },
    },
    `gatsby-plugin-offline`,
    `gatsby-plugin-react-helmet`,←これ追記
  ],
}

src/components/seo.jsを追加し以下を記述

src/components/seo.js
import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"

function SEO({ description, lang, meta, title }) {
  const { site } = useStaticQuery(
    graphql`
      query {
        site {
          siteMetadata {
            title
            description
            author
          }
        }
      }
    `
  )

  const metaDescription = description || site.siteMetadata.description

  return (
    <Helmet
      htmlAttributes={{
        lang,
      }}
      title={title}
      titleTemplate={`%s | ${site.siteMetadata.title}`}
      meta={[
        {
          name: `description`,
          content: metaDescription,
        },
        {
          property: `og:title`,
          content: title,
        },
        {
          property: `og:description`,
          content: metaDescription,
        },
        {
          property: `og:type`,
          content: `website`,
        },
        {
          name: `twitter:card`,
          content: `summary`,
        },
        {
          name: `twitter:creator`,
          content: site.siteMetadata.author,
        },
        {
          name: `twitter:title`,
          content: title,
        },
        {
          name: `twitter:description`,
          content: metaDescription,
        },
      ].concat(meta)}
    />
  )
}

SEO.defaultProps = {
  lang: `en`,
  meta: [],
  description: ``,
}

SEO.propTypes = {
  description: PropTypes.string,
  lang: PropTypes.string,
  meta: PropTypes.arrayOf(PropTypes.object),
  title: PropTypes.string.isRequired,
}

export default SEO

src/templates/blog-post.jsを修正

src/templates/blog-post.js
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
import SEO from "../components/seo"←これ追記

export default function BlogPost({ data }) {
  const post = data.markdownRemark
  return (
    <Layout>
      <SEO title={post.frontmatter.title} description={post.excerpt} />←これ追      <div>
        <h1>{post.frontmatter.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.html }} />
      </div>
    </Layout>
  )
}

export const query = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
      }
      excerpt←これ追記
    }
  }
`

これでもう一回Lighthouseを実行か
もう一回ビルドして動かすか。。。
2020-09-06_23h47_53.jpg
なんかあんまり向上はしてねぇな。
まぁでもちょっと向上したからいいか。HTTPSにするとか今のところ無理だし!

Keep making it better

Lighthouseを見ながら良いもの作っていってくださいねー!
だって。
基本は以上であとはまだチュートリアルがあるから見て作っていってね!
だって。

う~ん。。。どうしよう。
とりあえず今回はここまで。

ありがとうございました。

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

流行りのReactでFlexboxを利用して、検索フォーム内にアイコンを入れてみる。

はじめに

ソースコード https://github.com/TokyoProgramming/search__bar

検索バー.png

React + Flexbox + Material-UIを利用して、よくあるこのような検索フォームデザインを作りたいと思います。
このQiitaもそうですが、注意してみてみるとこのデザインの検索フォームって、本当に多いですよね。

1.React構造

Reactの構造は以下のようにとてもシンプルにしています。
コンポーネント.png

Componentsディレクトリー内にSearch.jsファイルとSearch.cssファイルを作りました。

2.Search.jsファイル

Search.js
import React from 'react';
import './Search.css';
import SearchIcon from '@material-ui/icons/Search';

function Search() {
    return (
        <div className="search">
            <div className='search__bar'>
                <SearchIcon />
                <input
                    className="search__bar__input"
                    placeholder="Search"
                />
            </div>
        </div>
      )
 }

 export default Search;

スタイリングしないと...
normal.png
きっとこれでは、だれも検索してくれませんよね。(笑)

では、cssファイルで徐々にスタイリングしていきます。

2.CSS スタイリング

いきなり結果を表示するのでなく、徐々に仕上げていきます。

(1)
Search.css
.search{
    display: flex;
    justify-content: center;
}
.search__bar{
    display: flex;
    padding: 10px;
    border: 1px solid lightgray;
    border-radius: 999px;
    background-color: #f0f2f5;
}

search-2.png

まだ、醜いですね。。。

border-radius: 999pxは、このようなフォームを作るときのテンプレートですので、覚えておくと便利です。
border-radius: 1px から、遊んでみると徐々に角が完璧な丸になっていく様子がよくわかります。

(2)inputのボーダーをなくす。

さて、こちらの付け加えると、

Search.css
.search__bar > input {
    border: none !important;
    background-color: #f0f2f5;
}

ちょっといい感じになりましたが、選択すると、右側のようにborderがくっきり出てきてしまいます。
search-3.png

(3)選択した際のborderを消す
Search.css
.search__bar > input:focus {
    outline-width: 0;
}

input::focusoutline-width: 0;設定することにより、 以下のように検索フォームを作成することができます。

search-5.png

(4)placeholderとMaterial-iconsを指定する方法。
Search.css
.search__bar > input::placeholder{
    text-align: left;
    font-size: 15px;
}
.search__bar > .MuiSvgIcon-root {
    margin-right: 5px;
}

input::placeholderで、placehoderの文字をSearchを指定して、スタイリングできます。
.MuiSvgIcon-root は、Material-iconsのアイコンを指定できます。

そして完成した、検索フォームがこちらです。
ちなみに背景色はFacebookの検索フォームと同じ色を使用しています。

search-6.png

Search.css
.search{
    display: flex;
    justify-content: center;
}
.search__bar{
    display: flex;
    padding: 10px;
    border: 1px solid lightgray;
    border-radius: 999px;
    background-color: #f0f2f5;
}
.search__bar > input {
    border: none !important;
    background-color: #f0f2f5;
}

.search__bar > input:focus {
    outline-width: 0;
}

.search__bar > input::placeholder{
    text-align: left;
    font-size: 15px;
}
.search__bar > .MuiSvgIcon-root {
    margin-right: 5px;
}

お疲れさまでした。

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

ReduxとReact (Native)Hooksとの共存

Reactは「自由にカスタマイズできる」かつ「日進月歩」というのがメリットの反面、
ネット上には新旧の情報が入り乱れていてベストプラクティスが見つけづらいというのがデメリットになっているのではないでしょうか。

その最たる例がHooksReduxをどういった構成にするかだと思います。
巷ではHooksReduxの機能を補えるから、Reduxはもはや必要ないといった意見もでているようです。

本当に不必要なのでしょうか?

初学者の方にもなるべくわかりやすいようにReduxの概要・非同期処理について根本的なところから改めて振り返って整理したのち、Hooks(useReducer・useContext)との設計概念・機能比較を行いました。
「もうそんなこと知ってるよ」という方は、Redux概要の箇所は飛ばしていただけるとありがたいです。

【Redux概要】 MVC・Flux・Reduxアーキテクチャ比較

Redux自体 MVC → Flux → Redux と既存のアーキテクチャの欠点を補うために生まれてきたアーキテクチャのため、遠回りなように思えますが、理解を深めるためにはこれらの差異について知ることが重要になってきます。

MVCアーキテクチャ

アプリケーション開発の学習を始めると一番最初に覚えるアーキテクチャではないでしょうか? 初心者にもシンプルでわかりやすいです。私自身もプラグラミングスクールでRuby on Railsを使い開発を行っていたのでお馴染みのアーキテクチャです。

単純なアーキテクチャであればシンプルに以下のようなフローになります。
従来型のフルサーバサイドレンダリングアプリケーションであれば、Model・View間のデータフローは単方向ですが、ReactなどのSPAアプリ開発においては、Model・View双方向にデータの受け渡しがあるところがポイントになります。
MVC.png
複雑なアーキテクチャになればなるほど、Model・View間のフローのパターンは指数関数的に増大してしまいメンテナンスが難しくなってしまいます。(以下図は概念を理解するために若干大げさな表記になっています。)
MVC_complex.png

Fluxアーキテクチャ

Fluxアーキテクチャは上記MVCモデルの煩雑なデータフローを解消するために生まれました。
つまり、シンプルなアプリであればMVCモデルでもなんら問題はないということになりますね。

MVCにたとえるならDispatcherControllerStoreModelといったところです。
ただし以下の点で異なっています。

  • Storeは一枚岩で複数のオブジェクトの状態を管理している(Modelのようにオブジェクトごとに個別に存在しない)
  • Storeの変更は、Actionから派生するDispatcherによってのみ行われる。Viewから直接操作することはない。
  • Storeの値が変更されると、直ちにViewに反映される。

上記のようなデータフローにより、Store・Viewはどれだけアプリケーションのボリュームが大きくなっても、単方向のデータフローに集約することができるようになります。つまり「どこのModelがどこのViewを更新して〜」、「どこのViewがどこのModelを更新して〜」、などを考えなくても単純に「Viewから『何をしたいか』のActionを発行し、その変更をStoreが受けつけ、Storeの状態が即時にViewに反映される」といったシンプルなデータフローになります。

Flux.png

Reduxアーキテクチャ

ReduxFluxアーキテクチャに更に制限を追加して、アプリケーションの状態管理をスムーズにするフレームワークです。
Fluxのデータフローに加えて以下のような制限が加わります。

  • Storeはアプリケーションに1つのみ存在する。つまり状態の更新先・参照先は常に同じStore(シングルトン)
  • Storeの状態(State)を変更できるのは、Reducerのみ
  • ReducerStore内のStateを、Viewより発行された「一意のActionType」により振り分け、新しいStateに更新する
  • Reducerは常に同じ結果を返す(非同期処理やランダムな計算を行ってはいけない)

Redux.png

【Redux概要】 Reduxで非同期処理を扱う

本格的なアプリケーション開発において、「非同期処理(外部APIとの連携)」を行わないケースは少ないと思います。
そのため非同期処理の実装は重要になってくるのですが、ReduxにおいてもMiddlewareを導入することで、非同期処理を実現することができます。
今回はMiddlewareの中でも人気のRedux-thunkRedux-sagaについて、特徴を整理してみます。

Redux Thunk

非常にシンプルなMiddlewareで、Thunkを導入するとActionCreater内部のロジックで非同期処理が実行できるようになります。
React同様、いい意味でも、悪い意味でもコード設計は自由なため、構成をうまく考えないとファットActionになってしまうリスクもあります。
ReduxThunk.png

Redux Saga

Redux-sagaについては、非同期処理はSagaに完全に切り分けてReduxとは別で処理を回すという考え方なので、ファイル数は多くなりますがredux-thunkよりこちらのほうが各機能の役割がはっきりして処理の流れが掴みやすくなります。
ただし、Sagaの内部処理ではあまり他では見かけないジェネレータ関数を利用しているため、Thunkよりも学習コストがかかるというデメリットがあります。

ReduxSaga.png

Hooksが導入されてできるようになったこと

useReducer

Reactのみでも、「Actionを発行 → Reducer内部でActionTypeによりStateの値を更新 → Viewの表示が変更」といったReduxのような外部フレームワークでしか実現できなかったFluxベースのアーキテクチャが組めるようになりました。
ただし、Storeの概念はなく、状態の管理は各オブジェクトごとに分散して管理する必要があります。(Reduxっぽく、すべてのオブジェクトのReducerCombineできなくはないようですが、それならreduxを使ったほうが効率的です。)

useContext

コンポーネントのトップレベルで定義したstateをその配下のコンポーネント(子・孫・ひ孫・・・)から、propsでのバケツリレーなしで値を取得することができます。

useReducer と useContextを組み合わせてReduxフローを実現

useReduceruseContextを組み合わせることにより、Reduxのフローを再現してみました。
下記コードは親と子の単純な例ですが、もっとコンポーネントの階層が深くなった場合でもRootコンポーネントで定義したStateにアクセス・ActionDispatchすることが可能になり、Reduxよりも簡単かつ軽量にデータフローを実現することができます。

App.tsx(root)
import React, {
  FC,
  useReducer,
  createContext,
} from 'react';
import AppScreen from './AppScreen';

const initialState = { count: 0 };

interface StateProps {
  count: number;
}

interface ActionProps {
  type: string;
}

const reducer = (state: StateProps, action: ActionProps) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
};

interface StoreContextProps {
  state: StateProps;
  dispatch: ({ type }: ActionProps) => void;
}

export const StoreContext = createContext({} as StoreContextProps);

const App: FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      <AppScreen />
    </StoreContext.Provider>
  );
};

export default App;
AppScreen.tsx
import React, { FC, useContext } from 'react';
import { StoreContext } from './App';

const AppScreen: FC = () => {
  const { state, dispatch } = useContext(StoreContext);

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
};

export default AppScreen;

useReducer.png

useReducer で非同期処理をする

useReducerを使ったデータフローではMiddlewareの導入を前提としていないようですが、Middlewareなしでも非同期処理は行うことができます。
色々と調べていたのですが、Reduxのようにアーキテクチャのベストプラクティスのようなものは定まっていないように感じました。結局ReduxのようなアーキテクチャにするならReduxを最初から導入すればいいし、違ったやり方をするのであればグローバルの状態管理項目が増えた場合にカオスになりそうな気がするし、Reactの放任主義はいつものことですが、ここも悩ましいところですね...。

App.tsx(root)
import React, {
  FC,
  useReducer,
  createContext,
} from 'react';
import axios, { AxiosError, AxiosResponse } from 'axios';
import AppScreen from './AppScreen';

axios.defaults.withCredentials = true;
axios.defaults.baseURL = 'http://xxxxxxxxxxxxxxxx.com/api';

interface StateProps {
  result: AxiosResponse;
  isLoading: boolean;
  error?: AxiosError | null;
}

interface ActionProps {
  type: string;
  payload?: any;
  error?: boolean;
}

const initialState: StateProps = {
  result: {} as AxiosResponse,
  isLoading: false,
};

const reducer = (state: StateProps = initialState, action: ActionProps) => {
  switch (action.type) {
    case 'start':
      return {
        ...state,
        isLoading: true,
      };
    case 'succeed':
      return {
        ...state,
        result: action.payload?.result,
        isLoading: false,
      };
    case 'error':
      return {
        ...state,
        result: action.payload?.error,
        isLoading: false,
      };
    default:
      throw new Error();
  }
};

interface StoreContextProps {
  state: StateProps;
  dispatch: ({ type }: ActionProps) => void;
}

export const StoreContext = createContext({} as StoreContextProps);

const App: FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      <AppScreen />
    </StoreContext.Provider>
  );
};

export default App;

AppScreen.tsx
import React, { FC, useContext } from 'react';
import axios from 'axios';
import { StoreContext } from './App';

const AppScreen: FC = () => {
  const { state, dispatch } = useContext(StoreContext);

  const clickHandler = async () => {
    dispatch({ type: 'start' });
    try {
      const result = await axios.get('/users');
      if (result.status !== 200) {
        throw new Error('The request has failed');
      }
      dispatch({ type: 'succeed', payload: { result }, error: true });
    } catch (error) {
      dispatch({ type: 'error', payload: { error }, error: true });
    }
  }

  return (
    <>
      Result: {JSON.stringify(state.result)}
      <button onClick={clickHandler}>データ取得</button>
    </>
  );
};

export default AppScreen;

Hooksの機能でもReduxと同じようなことはできる。それでもReduxを使うメリット

Reduxの設計原則は素晴らしい

ルールがありすぎるのは時には不自由に感じますが、コードが複雑になればなるほどその恩恵を受けることができます。その点、Reduxにはアーキテクチャに関するドキュメントがしっかりとまとまっており、この原則にある程度従っておけばそこまで的はずれな構成にならないというのは大きいです。
Hooksを使うから、Reduxの原則よりも素晴らしいアーキテクチャが組めるようになるというなら話は別ですが、Hooksを使っても結局Reduxのアーキテクチャを模倣してコードを組むのであれば最初からReduxを使っておけば良いという見解です、
逆に原則やルールが必要がないほど、シンプルなアプリケーションであるならばReduxを導入する意味はあまりないので、Hooks(useReducer・useContext)を使ってサクッと作るのもありかと思います。

異なるコンポーネント間でのやりとりが容易になる

原則部分とかぶる部分もありますが、グローバルの状態をStoreが一手に引き受け、かつアプリ上のすべてのReducerをコンバインしてどのコンポーネントからでも「状態の取得」、「アクションを通しての状態の変更」ができるのは便利です。
useReducerにはStoreの概念がそもそもありません。Hooksを使ってもRootコンポーネントでContextを定義すればできないことはないですが、もともとそういったアーキテクチャ用のHookではないので開発者独自の記述方法になって後々メンテナンスに苦労するということになりかねません。
Storeの概念を使いたいなら、おとなしくReduxを導入して、Reduxで管理するまでもないところのみuseReducerで処理するというのが現状のベストではないでしょうか。

Middlewareにより非同期処理と状態管理を分離できる

非同期通信を行うためのMiddlewareを自由に選べるのもメリットの一つです。Redux-thunkは自由度が高く、技術者の裁量に任されてしまいますが、Redux-sagaを使用した場合、外部APIとの非同期処理はsagaにかき分けることを強制されるので、状態管理と非同期処理を明確に分離することができます。これによりテストが書きやすく、保守性の高いプロダクトを保持することができます。

Middlewareによりアクション発行履歴(トランザクション)を管理し、巻き戻しできる

これもまたMiddlewareの話になりますが、Redux devtoolsを導入することにより、グローバルStateの可視化、アクション発行履歴の管理、タイムトラベルデバッギングができるようになり、開発効率を高めることができます。
e23206f8dbc0ff1acb9f2ff6ed01118a.gif

Redux with Hooks

Redux自体もv7.1.0以降Hooksの恩恵を受け随分とシンプルにかけるようになりました。
また、初期導入時にはRedux Tool Kitでコマンド一発でRedux & Hooks & Typescriptの開発環境が整うようです。
もっと早く欲しかった..。

useSelector & useDispatch

mapStateToPropsmapDispatchToPropsをコンポーネントにconnectせずにReduxStoreにアクセスできるようになったので、記述量が減るだけでなくかなりフレキシブルにReduxを扱うことができます。

従来までの書き方
import React, { FC, useEffect } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';

import { fetchAllItems, deleteItem } from '../actions/item';

const mapStateToProps = (state: AllState) => ({
  items: state.item.items,
  isLoading: state.item.isLoading,
});

const mapDispatchToProps = (dispatch: Dispatch) =>
  bindActionCreators(
    {
      fetchItemsStart: () => fetchAllItems.start(),
      deleteItemStart: (itemId: ItemId) => deleteItem.start(itemId),
    },
    dispatch,
  );

interface HomeScreenProps extends ScreenNavigationProp {
  items: ItemModel[];
  isLoading: boolean;
  fetchItemsStart: () => void;
  deleteItemStart: (itemId: ItemId) => { payload: { id: number } };
}

const HomeScreen: FC<HomeScreenProps> = ({
  items,
  isLoading,
  fetchItemsStart,
  deleteItemStart,
  navigation,
  route,
}) => {
  useEffect(() => {
    (async () => {
      await fetchItemsStart();
    })();
  }, []);

  useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      (async () => {
        await fetchItemsStart();
      })();
    });

    return unsubscribe;
  }, [navigation]);

  const deleteItem = async (itemId: number) => {
    alert(`deleted ${itemId}`);
    await deleteItemStart({ id: itemId });
  };

  return (
    ・・・
  );
};

export default connect(mapStateToProps, mapDispatchToProps)(HomeScreen);
HooksAPIを使った書き方
import React, { FC, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { fetchAllItems, deleteItem } from '../actions/item';

const HomeScreen: FC<ScreenNavigationProp> = ({ navigation, route }) => {
  const dispatch = useDispatch();
  const itemState = useSelector((state: AllState) => state.item);

  useEffect(() => {
    (async () => {
      await dispatch(fetchAllItems.start());
    })();
  }, []);

  useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      (async () => {
        await dispatch(fetchAllItems.start());
      })();
    });

    return unsubscribe;
  }, [navigation]);

  const deleteButtonHandler = async (itemId: number) => {
    alert(`deleted ${itemId}`);
    await dispatch(deleteItem.start({ id: itemId }));
  };

  return (
    ・・・
  );
};

export default HomeScreen;

Redux Tool Kit

私自身使ったことがないため導入方法リンクのみ共有しておきます。

https://redux-toolkit.js.org/introduction/quick-start

まとめ

  • Hooksの登場により、Reduxは唯一の選択肢ではなくなった
  • 複雑かつ大規模なプロダクトにはReduxの設計原則が最も力を発揮する
  • Hooks useReducer + useContextはReduxと競合関係にあるわけではなく共存・使い分けできる
  • Reduxの設計原則が必要ないほどシンプルなプロダクトならばHooksのみでグローバルの状態も管理できる
  • Reduxの導入ハードルは年々下がってきている
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + TypeScriptプロジェクトを手動で作成する(webpack)

はじめに

この記事では、React+TypeScriptのプロジェクトをcreate-react-appを使わずに一から手動で作成する方法の一例を紹介します。

筆者はフロントエンド開発に興味を持ち始め、これからReact+TypeScriptを学習していくうえでwebpackを利用したプロジェクトを一から作成する手順も知っておきたいと思い、自身で調べて構築した環境を備忘録も兼ねて記事にします。

注意点として、この記事では各種の設定内容に関する詳細な解説はしません。
また、記事の内容は記事執筆時点で動作が確認できたものとなります。

前提

  • macOS Catalina 10.15.4
  • Node.jsがインストール済みで、nodeコマンドとnpmコマンドが使える
node -v
v12.16.1

npm -v
6.14.8

作っていく開発環境について

今回作っていく開発環境の特徴は大まかに次の通りです。

  • React + TypeScriptのプロジェクト(create-react-appを使わずに)
  • webpack-dev-serverで開発用サーバーを立ち上げれる
  • 開発用サーバー起動中は、ソースの変更を検知してブラウザ画面が自動で反映される
    • TypeScriptの変更はreact-hot-loaderでStateを維持した反映
    • CSSの変更も自動で反映
  • webpackの設定ファイル(webpack.config)をTypeScript化する (任意)

また、動作確認ができるように簡単なサンプルアプリも用意します。
GitHubにも上げているので参考にしていただければと思います。

手順

プロジェクトのフォルダを作成する

任意の場所でReactプロジェクトのフォルダを作成します。
今回はreact-sample-appという名前にしました。

cd PATH/TO/WORKING_DIR
mkdir react-sample-app
cd react-sample-app

package.jsonを作成する

package.jsonを作成します。

npm init -y

作成後、次のように書き換えます。

package.json
{
  "name": "react-sample-app",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack  --mode=production",
    "start": "webpack-dev-server --hot --mode=development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

必要なパッケージをインストールする

必要なパッケージをnpmでインストールします。

  • インストールするパッケージ
    • webpack、webpack-cli
    • typescript、ts-loader、source-map-loader
    • react、react-dom、@types/react、@types/react-dom
    • webpack-dev-server
    • fork-ts-checker-webpack-plugin
    • html-webpack-plugin
    • react-hot-loader、@hot-loader/react-dom
    • css-loader、mini-css-extract-plugin、@types/mini-css-extract-plugin
    • optimize-css-assets-webpack-plugin、@types/optimize-css-assets-webpack-plugin
npm install --save-dev webpack webpack-cli typescript ts-loader source-map-loader react react-dom @types/react @types/react-dom webpack-dev-server fork-ts-checker-webpack-plugin html-webpack-plugin react-hot-loader @hot-loader/react-dom css-loader mini-css-extract-plugin @types/mini-css-extract-plugin optimize-css-assets-webpack-plugin @types/optimize-css-assets-webpack-plugin

tsconfig.jsonを作成する

tsconfig.jsonを作成します。

npx tsc --init

作成後、次のように書き換えます。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./dist/",
    "strict": true,
    "esModuleInterop": true,
    "sourceMap": true,
    "forceConsistentCasingInFileNames": true,
    "allowJs": true,
    "checkJs": true,
    "jsx": "react",
    "lib": [
      "ES2015",
      "DOM"
    ]
  }
}

webpack.config.jsを作成する

webpack.config.jsを作成します。

touch webpack.config.js

作成後、次のように書き換えます。

webpack.config.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")

module.exports = {
    mode: "development",
    entry: ["react-hot-loader/patch", path.resolve(__dirname, "./src/index.tsx")],
    output: {
        path: path.resolve(__dirname, "dist/"),
        filename: "bundle.js"
    },
    devtool: "source-map",
    resolve: {
        modules: ["node_modules"],
        alias: {
            "react-dom": "@hot-loader/react-dom"
        },
        extensions: [".ts", ".tsx", ".js", ".jsx", ".json"]
    },
    module: {
        rules: [
            {
                test: /\.(j|t)s(x)?$/,
                exclude: /node_modules/,
                use: {
                    loader: "ts-loader",
                }
            },
            {
                enforce: "pre",
                test: /\.ts(x?)$/,
                loader: "source-map-loader"
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            hmr: true,
                            reloadAll: true,
                        }
                    },
                    {
                        loader: "css-loader",
                    }
                ],
            }
        ]
    },
    devServer: {
        host: "localhost",
        contentBase: path.resolve(__dirname, "dist/"),
        port: 8080,
        inline: true,
        open: true,
        hot: true,
    },
    optimization: {
        minimizer: [new OptimizeCSSAssetsPlugin({})]
    },
    plugins: [
        new ForkTsCheckerWebpackPlugin(),
        new webpack.NamedModulesPlugin(),
        new HtmlWebpackPlugin({ template: "./src/index.html" }),
        new MiniCssExtractPlugin({ filename: "./css/style.css" }),
    ]
};

サンプルアプリを用意する

TypeScriptで書いたReactのアプリを用意していきます。
ここではビンゴゲームのようなサンプルアプリを作っていこうと思います。
(といっても作りは雑で、あくまで動作確認ができれば何でもよいかと思います)
サンプルはGitHubsrcフォルダも参考にしてください。

アプリの用意ができた時点でのフォルダ階層は次のようになります。
この階層図も参考にしながらhtml、css、tsxファイルを作成していってください。

.
├── package-lock.json
├── package.json
├── src
│   ├── components
│   │   ├── Bingo.tsx
│   │   ├── Card.tsx
│   │   ├── Enums.tsx
│   │   ├── Machine.tsx
│   │   └── Square.tsx
│   ├── css
│   │   └── style.css
│   ├── index.html
│   └── index.tsx
├── tsconfig.json
└── webpack.config.js

では、まずフォルダを作成していきます。

mkdir src
mkdir src/components src/css

src/index.htmlを作成します。

src/index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8" />
    <title>Sample App</title>
</head>

<body>
    <div id="app"></div>
</body>

</html>

src/css/style.cssを作成します。

src/css/style.css
body {
  background-color: #fff;
}

ul {
  width: 400px;
  margin: 20px auto;
  overflow: hidden;
}

li {
  text-align: center;
  line-height: 60px;
  display: inline-block;
}

.card ul li {
  width: 20%;
}

.square {
  box-sizing: border-box;
  background-color: #eee;
  border: solid 1px black;
  position: relative;
  font-size: 1.6rem;
}

.hit {
  background-color: #4caf50;
}

.machine {
  margin-top: 30px;
  text-align: center;
}

.btn {
  display: inline-block;
  color: #fefefe;
  background-color: #f06292;
  width: 200px;
  height: 60px;
  font-weight: bold;
  border: none;
  text-align: center;
  font-size: large;
}

.appearances {
  text-align: left;
}

tsxファイルを作成していきます。

src/index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import { hot } from "react-hot-loader/root";

import "./css/style.css";
import Bingo from "./components/Bingo";

const App: React.FC = () => {
    return (
        <div>
            <Bingo />
        </div>
    )
}
export default hot(App);
//export default App;  // react-hot-loaderを無効にする場合はこちらを使う

ReactDOM.render(<App />, document.getElementById("app"));
src/components/Bingo.tsx
import React from "react";

import Machine from "./Machine";
import Card from "./Card";

const Bingo: React.FC = () => {
    const [appearanceNumbers, setAppearanceNumbers] = React.useState<number[]>([]);

    return (
        <div>
            <Card appearanceNumbers={appearanceNumbers} />
            <Machine appearanceNumbers={appearanceNumbers} setAppearanceNumbers={setAppearanceNumbers} />
        </div>
    )
}

export default Bingo;
src/components/Card.tsx
import React from "react";

import Square from "./Square";
import BingoCard from "./Enums";

type Props = {
    appearanceNumbers: number[]
}

const squareValues = Array.from(new Array(100))
    .map((_, index) => index)
    .sort(() => Math.random() - 0.5)
    .slice(0, 25);
squareValues[12] = BingoCard.Free;

const Card: React.FC<Props> = ({ appearanceNumbers }) => {

    return (
        <div className={"card"}>
            <ul>
                {
                    squareValues.map(squareValue => (
                        <li>
                            <Square value={squareValue} appearanceNumbers={appearanceNumbers} />
                        </li>
                    ))
                }
            </ul>
        </div>
    )
}

export default Card;
src/components/Enums.tsx
enum BingoCard {
    Free = -1
}

export default BingoCard;
src/components/Machine.tsx
import React from "react";

type Props = {
    appearanceNumbers: number[]
    setAppearanceNumbers: React.Dispatch<React.SetStateAction<number[]>>
}

const remainNumbers = Array.from(new Array(100))
    .map((_, index) => index)
    .sort(() => Math.random() - 0.5);

const Machine: React.FC<Props> = ({ appearanceNumbers, setAppearanceNumbers }) => {

    const handleClick = () => {
        const appearanceNumber = remainNumbers.shift();
        if (appearanceNumber === undefined) {
            return;
        }

        setAppearanceNumbers([appearanceNumber, ...appearanceNumbers]);

    }

    return (
        <div className={"machine"}>
            <button
                className={"btn"}
                onClick={handleClick}
            >GO
            </button>
            <h2>{remainNumbers.length > 0 ? "ボタンを押してください" : "ビンゴは終了しました"}</h2>
            <div className={"appearances"}>{appearanceNumbers.join(", ")}</div>
        </div>
    )
}

export default Machine;
src/components/Square.tsx
import React from "react";

import BingoCard from "./Enums";

type Props = {
    value: number,
    appearanceNumbers: number[]
}

const Square: React.FC<Props> = ({ value, appearanceNumbers }) => {

    return (
        <div
            className={appearanceNumbers.includes(value) || value == BingoCard.Free ? "square hit" : "square"}
        >
            {value == BingoCard.Free ? "Free" : value}
        </div>
    )
}

export default Square;

これでサンプルアプリを用意できました。

webpack-dev-serverで開発用webサーバーを起動する

開発用webサーバーを起動します。

npm start

ブラウザが開き、ビンゴゲームのような画面が表示されるかと思います。

スクリーンショット 2020-09-06 14.50.13.png

ボタンを押すと番号が排出され、当たればマスも埋まっていくと思います。

スクリーンショット 2020-09-06 14.55.15.png

この状態でcssを変更してみましょう。

src/css/style.css
/* (一部抜粋) */
body {
  background-color: #333;
}

cssの変更が自動でブラウザに反映されます。
スクリーンショット 2020-09-06 14.59.01.png

次に、TypeScriptのコードを変更してみましょう。
src/components/Machine.tsxのh2タグの表示内容を適当に変更してみます。

src/components/Machine.tsx
// (省略)
const Machine: React.FC<Props> = ({ appearanceNumbers, setAppearanceNumbers }) => {

    // (省略)
    return (
        <div className={"machine"}>
            <button
                className={"btn"}
                onClick={handleClick}
            >GO
            </button>
            <h2>{remainNumbers.length > 0 ? "GOボタンを押してね" : "ビンゴは終了しました"}</h2>
            <div className={"appearances"}>{appearanceNumbers.join(", ")}</div>
        </div>
    )
}

export default Machine;

スクリーンショット 2020-09-06 15.09.09.png

TypeScriptの変更も検知して自動で反映されました。
このとき、react-hot-loaderでStateを維持した反映となるため、ビンゴカードの埋まったマスはそのままとなっています。

react-hot-loaderを使わない場合

せっかくなのでreact-hot-loaderを使わない場合の挙動も見てみましょう。
src/index.tsxを次のように変更して、開発用webサーバーを立ち上げます。
(hotを外しているだけです。また、cssやMachine.tsxは元に戻しました。)

src/index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import { hot } from "react-hot-loader/root";

import "./css/style.css";
import Bingo from "./components/Bingo";

const App: React.FC = () => {
    return (
        <div>
            <Bingo />
        </div>
    )
}
//export default hot(App);
export default App;  // react-hot-loaderを無効にする場合はこちらを使う

ReactDOM.render(<App />, document.getElementById("app"));
npm start

適当にマスを埋めておきます。

スクリーンショット 2020-09-06 15.24.11.png

1ビンゴもらいました。
この状態で、先ほどと同様にsrc/components/Machine.tsxを変更してみます。

src/components/Machine.tsx
// (省略)
const Machine: React.FC<Props> = ({ appearanceNumbers, setAppearanceNumbers }) => {

    // (省略)
    return (
        <div className={"machine"}>
            <button
                className={"btn"}
                onClick={handleClick}
            >GO
            </button>
            <h2>{remainNumbers.length > 0 ? "GOボタンを押すといいよ!" : "ビンゴは終了しました"}</h2>
            <div className={"appearances"}>{appearanceNumbers.join(", ")}</div>
        </div>
    )
}

export default Machine;

変更を保存すると、

スクリーンショット 2020-09-06 15.31.13.png

画面が「GOボタンを押すといいよ!」に変わりましたが、ブラウザのリロードによってビンゴカードは初期状態に戻されてしまいました。

react-hot-loaderを使わなかった場合はStateが維持されないことが確認できたと思います。

ビルドする

webpackでビルドしていきます。

npm run build

distフォルダが作成され、その配下に以下のファイルが出力されたかと思います。

./dist/
├── bundle.js
├── bundle.js.map
├── css
│   └── style.css
└── index.html

このとき、出力されたファイルは圧縮されていますが、圧縮したくない場合はdevelopmentモードでビルドすればよいです。

npx webpack --mode=development

以上が、React+TypeScriptのプロジェクト作成と開発用サーバーやビルドの動作確認でした。

以降はwebpackの設定をTypeScriptに書き換えます。ご興味ある方だけ次に進んでください。

webpackの設定ファイルをTypeScriptにする (任意)

webpackの設定ファイル(webpack.config.js)をTypeScriptで書き換えます。
書き換え作業の差分も参考にしてください。

まずはTypeScriptで書いたwebpackの設定を読み込むために、ts-nodeをインストールします。

npm install --save-dev ts-node

webpack.config.jswebpack.config.tsにリネームし、内容を次のように変更します。

webpack.config.ts
import * as path from "path";
import * as webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import OptimizeCSSAssetsPlugin from "optimize-css-assets-webpack-plugin";

const config: webpack.ConfigurationFactory = (env, argv) => {
    const isDevelopmentMode = argv.mode === "development";

    return {
        mode: "development",
        entry: ["react-hot-loader/patch", path.resolve(__dirname, "./src/index.tsx")],
        output: {
            path: path.resolve(__dirname, "dist/"),
            filename: "bundle.js"
        },
        devtool: "source-map",
        resolve: {
            modules: ["node_modules"],
            alias: {
                "react-dom": "@hot-loader/react-dom"
            },
            extensions: [".ts", ".tsx", ".js", ".jsx", ".json"]
        },
        module: {
            rules: [
                {
                    test: /\.(j|t)s(x)?$/,
                    exclude: /node_modules/,
                    use: {
                        loader: "ts-loader",
                    }
                },
                {
                    enforce: "pre",
                    test: /\.ts(x?)$/,
                    loader: "source-map-loader"
                },
                {
                    test: /\.css$/,
                    use: [
                        {
                            loader: MiniCssExtractPlugin.loader,
                            options: {
                                hmr: isDevelopmentMode,
                                reloadAll: true,
                            }
                        },
                        {
                            loader: "css-loader",
                        }
                    ],
                }
            ]
        },
        devServer: {
            host: "localhost",
            contentBase: path.resolve(__dirname, "dist/"),
            port: 8080,
            inline: true,
            open: true,
            hot: true,
        },
        optimization: {
            minimizer: [new OptimizeCSSAssetsPlugin({})]
        },
        plugins: [
            new ForkTsCheckerWebpackPlugin(),
            new webpack.NamedModulesPlugin(),
            new HtmlWebpackPlugin({ template: "./src/index.html" }),
            new MiniCssExtractPlugin({ filename: "./css/style.css" }),
        ]
    };
}

export default config;

最後に、package.json"main"キーの値を"webpack.config.ts"に変更します。

package.json
{
  // (省略)
  "main": "webpack.config.ts",
  // (省略)
}

これで、webpackの設定をTypeScriptに直すことができました。
先ほどと同様に、開発用サーバーの起動やビルドの実行ができることを確認します。

  • 開発用webサーバーの起動
npm start
  • ビルドの実行
npm run build

参考

https://qiita.com/humi/items/49a7472e9a10558ea5c0
https://qiita.com/humi/items/72485614151fe564dceb
https://qiita.com/SoraKumo/items/5d92b15d06778458f5e1
https://numb86-tech.hatenablog.com/entry/2018/10/24/221130
https://webpack.js.org/plugins/mini-css-extract-plugin/
https://qiita.com/sathoshi-metal/items/b9ce118cca9b75b2e0a9

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

Reactで書いたWEBサイトをGithubにコミットして、netlifyにデプロイした。

自分の自己紹介的なWEBサイトを用意したい。かつ、Reactを触ってみたい。
と思い、やってみたのでメモとして記録を残しておく。

手順

  1. githubに登録する
  2. netlifyに登録する
  3. npxでcreate-react-appでプロジェクトを作る
  4. githubにプッシュする
  5. netlifyからgithubにプッシュされたリポジトリを選択してデプロイする
  6. WEBサイトにアクセスできた!

変更を加えたら、githubにプッシュすれば自動的にビルドされて変更が反映される。

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

VSCodeのExtension「Jest」の設定ファイルjest.configのパス

Githubからクローンしてきたソフトウェアで、yarn testとかは動くけど、VSCode上のJest拡張機能のテストが失敗してしまいました。このような場合は、package.jsonのscriptsにJestの設定ファイルのパスが指定されていないか確認してみましょう。

VSCodeのJest拡張機能で見に行くconfigファイルはjest.config.jsなので、package.jsonでこれ以外のパスを設定していたりするとうまく動きません。

https://github.com/jest-community/vscode-jest

we find Jest configuration files in the workspace: jest.config.js or jest.json

私の場合は、VSCode上のJestだけ以下のエラーが出ている状態でしたが、configのパスを修正したところエラーが解消しました。

Support for the experimental syntax 'jsx' isn't currently enabled

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

型チェックツールFlowはMaterial-UIのバージョンアップに対応していない?

型チェックツールのflowを走らせたら、@material-ui/coreのimportで失敗しました。

$ yarn run flow
$ flow
> Error 
> Cannot resolve module @material-ui/core. [cannot-resolve-module]

Material-UI公式にflowの対応状況について説明が書かれていて、リンク先のプロジェクトで型定義情報がメンテナンスされているとあります。
https://material-ui.com/guides/flow/

flow-typed

flow-typed is a repository of third-party library interface definitions for use with Flow. The community is maintaining the definitions under this project.

しかし、リンク先を見るとcore_v1.x.x/flow_v0.58.x-v0.88.xと書かれています。Material-UIのバージョンアップに追従していないようです。

https://github.com/flow-typed/flow-typed/tree/master/definitions/npm/%40material-ui

少しググってみたら以下のページを発見して一応試してみましたが、@types/material-uiはTypeScriptの型情報なのでエラー解消しませんでした。
https://stackoverflow.com/questions/58941816/how-to-install-flow-types-for-material-ui-corev4-x-x

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

WindowsでReactの開発環境を整える

はじめに

WindowsでReactの開発環境を作ってみたところ、いくつかエラーが出たのでその手順と解決法を残します。
使用した環境は以下の通り

  • Windows10
  • Node.js v14.8.0
  • NPM v6.14.7
  • Windows パワーシェル(VScode経由)

手順

  1. Node.jsのインストール
  2. create-react-appのインストール
  3. プロジェクトを作成してHello worldしてみる

おまけ

  • ESlintを設定する
  • ディレクトリ構成のかんたんな説明

1.Node.jsのインストール

まずはNode.js公式サイトからダウンロードし、Node.jsをインストールします。
ターミナルで以下のコマンドを打ってVersion情報が表示されればインストール成功しています。

Node.jsのVersion情報を確認する
node -v

Node.jsがインストールされていればnpmも使うことが可能になります。

2.create-react-appのインストール

create-react-appをグローバルインストールします。

create-react-appをインストールする
npm install -g create-react-app

ここまでで準備は完了です。

3.プロジェクトを作成してHello worldしてみる

プロジェクトを作成したいディレクトリに移動し、プロジェクトを作成していきます。
多くのドキュメントではプロジェクトを作成するのに以下のコマンドを見かけると思います。

Reactのプロジェクトを作成する
create-react-app <プロジェクト名>

Windowsでは、インストールしただけでは上記のコマンドにパスが通っておらず使う事ができないことがあるようです。
解決方法は以下
1. パスを通す
2. npxコマンドを使う

今回はnpxコマンドを使う方法で作成していきます。
(以下サンプルのプロジェクト名をhelloworldとして作成します。)

Reactのプロジェクトを作成する
npx create-react-app helloworld

これで、カレントディレクトリにhelloworldというディレクトリが作られ、プロジェクトの作成が完了します。

パスの通し方については現在調査中です。

プロジェクトが作成できたら、早速動かして行きます。
helloworldディレクトリに移動し、仮想サーバー上でWebアプリの動作を確認します。

Webアプリをプレビューする
cd helloworld
npm start

以上のコマンドを打つと、規定のブラウザが起動してWebアプリの動作を確認することができます。
他のブラウザで見たい場合はlocalhost:3000にアクセスすることで見ることができます。
終わる場合はターミナルでCtrl + cを押して終了します。

以降、srcディレクトリ内のApp.jsを編集することでWebアプリを作っていきます。

おまけ

ESlintを設定する

create-react-appを使用してプロジェクトを作成すると、ESlintがモジュールに追加された状態になっています。
なので、設定を行うだけでESlintが使用可能になります。

ESlintの設定ファイルを作成する
npm eslint --init

2020-09-03_19h46_24.png

最後の質問、「Would you like to install them now with npm?」でYesにするとうまく動かなかったので、Noにしています。
ここで改めてインストールしてしまうと、競合するのかもしれません。

Compiles successfully!と表示されたら初期化が完了します。
画像では設定ファイルをJavaScriptにしているので.eslintrc.jsというファイルが作成されます。
環境に合わせて設定を追加してください。

ディレクトリ構成のかんたんな説明

以下は自分のための備忘録としての、ディレクトリ構成のかんたんな説明です。

<プロジェクト>
   L ./node_modules (インストールされているモジュールが入ってる場所。)
   L ./public (テンプレートのHTMLや画像が入っています。)
   L ./src (アプリのソース等、ここのファイルを編集してアプリを作成していきます。)
   L .eslintrc.js (ESlintの設定ファイル)
   L .gitignore (Gitで追跡対象にしないファイルを設定します。)
   L package.json (インストールされているモジュールについての設定ファイル)
   L README.md (Read meファイル)
   L yarn.lock (環境によっては無いこともある)

この他、ビルド後のデータを格納するディレクトリがあります。

最後に

create-react-appを使ってReactの開発環境を作ると、すでに必要なモジュールやファイルが用意されているため素早く開発を始められます。
最初はCDNを使ってみようと思ったのですが手こずってしまい、結局こちらのほうが早くReactの勉強を始めることができました。

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

React+Redux+Firebaseで読書管理アプリを作るまで

はじめに

初めてアプリを作ったので、その工程を記録に残したいと思い記事を書いています。
個人開発やポートフォリオの作成に役立つ内容があれば幸いです。

作ったもの

Flipper-countup.gif
「Flipper」という、シンプルな読書管理アプリです。
今まで読んだページ数がアニメーションで大きく表示されることで、自分の積み上げを視覚的に実感できることを目指しました。

私について

プログラミングを仕事にしているわけではなく、普段は英語を教えたり、翻訳の仕事をしています。
約3〜4年前に趣味でコードを軽く触ったことはありましたが実務は未経験で、1ヶ月半前にJavaScriptとReactの勉強を始めました。

Flipper

今回制作した「Flipper」の紹介です。

コンセプト

  • シンプルで美しい
  • 積み上げを実感できる

この2つがFlipperのコンセプトです。

読書管理アプリは選択肢が豊富ですが、記録をグラフにする、SNSで共有できるなど、個人的に不要だと感じる機能が搭載されているものが多いです。そこで、自分が本当に必要だと思う機能のみで構成されたシンプルなアプリを作ろうと思いました。

工夫したのは今まで自分が読んだページ数がアニメーションで表示されるところです。繰り返しになりますが、自分の積み上げを視覚的に実感できることでユーザーの読書へのモチベーションを高めることにフォーカスしました。

趣味ではなく、自己研鑽として読書をしているユーザーがターゲットです。

使用技術

  • React
  • redux
  • redux-thunk
  • react-router
  • connected-react-router
  • react-countup
  • Firebase
  • Firebase Authentication
  • Cloud Firestore
  • Firebase Hosting
  • Material-UI
  • Google Books API

機能

  • 登録
    登録.gif
    ISBNコードを入力することで読んだ本を登録することができます。

  • リスト表示
    リストページ.png
    登録した本をリストで表示します。

  • 削除
    削除.gif
    リストページから本を削除できます。

開発準備

コードを書く前の段階で行ったことです。

デザイン

flipper-note_1.jpg

かなり雑ですが、ノートに手書きすることでイメージを固めました。

タスク化

trello.png

ノートへ書き出した画面からどんな機能が必要なのかを考え、何をすべきかを明確にします。
タスク管理にはTrelloを使用しました。

開発をスムーズに進めるため、コードを書き始める前にある程度準備しておくことはとても大切だと感じました。

コードを書く

事前にタスク化を行っていたことで、ここからは「書く」「分からないことを調べる」を繰り返すだけでした。
開発期間は約2日です。

主な処理を書いたコードを記載します。

Google Books APIからデータを取得、データを保存

operations.js
export const searchAndSetBook = (isbn) => {
  return async (dispatch, getState) => {
    // 本のデータを取得
    fetch(
      `https://www.googleapis.com/books/v1/volumes?q=${isbn}&maxResults=1`,
      {}
    )
      .then((response) => {
        if (response.status === 200) {
          return response.json();
        } else {
          throw new Error("データの取得に失敗しました");
        }
      })
      .then((data) => {
        //  必要なデータのオブジェクトを作成
        return {
          title: data.items[0].volumeInfo.title,
          pages: data.items[0].volumeInfo.pageCount,
          image: data.items[0].volumeInfo.imageLinks["thumbnail"],
        };
      })
      .then((book) => {
        const uid = getState().users.uid;
        const pages = getState().users.pages;
        const books = getState().users.books;

        const newPages = pages + book["pages"];
        books.unshift(book);

        // ストアを更新
        dispatch(
          newBookAction({
            pages: newPages,
            books: books
          })
        );

        //  データベースを更新
        db.collection("users").doc(uid).update({
          pages: newPages,
          books: books
        });
      })
      .then(() => {
        alert("登録が完了しました");
      })
      .catch((error) => {
        alert(`エラー!: ${error}`);
      });
  };
};

読んだ本のリストを表示

BookCardList.jsx
const BookCardList = () => {
  const selector = useSelector(state => state)
  const books = getBooks(selector)
  return (
    <div className="section-wrapper">
        {books.length > 0 && (
          books.map((book,index) => (
            <BookCard key={index} book={book} title={book.title} pages={book.pages}/>
          ))
        )}
    </div>
  );
};

本をリストから削除

operations.js
export const deleteBook = (title, pages) => {
  return async (dispatch, getState) => {
    const uid = getState().users.uid;
    const oldPages = getState().users.pages;
    const oldBooks = getState().users.books;

    const newPages = oldPages - pages;
    const newBooks = oldBooks.filter((book) => book.title !== title);

    dispatch(
      newBookAction({
        pages: newPages,
        books: newBooks,
      })
    );

    db.collection("users")
      .doc(uid)
      .update({
        pages: newPages,
        books: newBooks,
      })
      .then(() => {
        alert("削除しました。更新してください");
      })
      .catch((error) => {
        alert(`エラー!: ${error}`);
      });
  };
};

アプリを作ってみて

思ったことは以下の2点です。

  • 何よりも楽しい

学習中にチュートリアルをいくつかやってみましたが、1から初めて10で終わる教科書的な学び方に飽きてしまうことがありました。一方で実際の開発は学んだことが即アウトプットに繋がるのでスピード感があって良いですし、自分の成長と共にアプリが完成に近づいていくので達成感があります。

  • 興味関心の幅が広がる

今回の開発を通して、これから勉強したいことがどんどん出てきました。今回使用したRedux、Firebaseの理解に不十分な点があることにも気がつけましたし、レンダリングの速度が気になるのでSSRができるNext.jsにも興味があります。
また、Google Book APIのドキュメントを読んだ際、バックエンド側の知識不足を実感しました。フロントエンド以外の領域も学んで行きたいと思います。

おわりに

ここまでお読みいただきありがとうございました。

FlipperのURLはこちらです。ゲストユーザーを用意しているので、よければ触ってみてください。
Flipper

FlipperのコードはGitHubで見られます。
GitHub

twitterもやってます。Reactなど技術的なことを呟いているので、ぜひフォローしてください。
@indigo9alpha

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

react-beautiful-dndがわかりにくすぎたので最小サンプル

React でドラッグアンドドロップ(DnD)を実現するライブラリがいくつかあります。
今回は react-beautiful-dnd を使います。

https://github.com/atlassian/react-beautiful-dnd

ですがドキュメントに、動作する最小サンプルが用意されてません。

代わりに用意しました。
Typescriptのものですが。

名称未設定.gif

Screen.tsx
import React, { FC, useState } from 'react'
import {
  DraggableProvided,
  DroppableProvided,
  DropResult,
  Draggable,
  Droppable,
  DragDropContext,
} from 'react-beautiful-dnd'

const droppableId = 'id-1'
const list = ['Hello', 'World', 'nishisuke'] // List for dnd. Each item must be identified.

const sort = <T,>(list: T[], before: number, after: number): T[] => {
  const copy = [...list]
  const [moving] = copy.splice(before, 1)
  copy.splice(after, 0, moving)

  return copy
}

export const Screen: FC = () => {
  const [uniqTexts, setUniqTexts] = useState(list)

  const dndHandler = (result: DropResult) => {
    if (
      !result.destination ||
      result.destination.index === result.source.index
    ) {
      return
    }

    const sorted = sort(
      uniqTexts,
      result.source.index,
      result.destination.index
    )
    setUniqTexts(sorted)
  }
  return (
    <DragDropContext onDragEnd={dndHandler}>
      <Droppable droppableId={droppableId}>
        {(provided: DroppableProvided) => (
          <div ref={provided.innerRef} {...provided.droppableProps}>
            {uniqTexts.map((t, i) => (
              <Draggable key={t} draggableId={t} index={i}>
                {(provided: DraggableProvided) => (
                  <div
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                  >
                    {t}
                  </div>
                )}
              </Draggable>
            ))}
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  )
}

注意点

並び替えるアイテムは id を持つ必要があります。
今回はユニークな string を並び替えたので、それ自身が id です。

placeholder はアイテム達の後ろに置く必要があります。

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

React.js Springbootを導入する

React.js Springbootの公式を少し変えたものです。

Githubのレポジトリをご覧いただきたいです。
ReactSpringbootのレポジトリ

JPA
h2
Lombok
rest repository
tymeleaf
React.jsのGET部分は少し変えています。 axiosを使っています。
cmdからmvnw.cmd spring-bot:runで起動する。
IDEで起動するとコンパイル時に生成されるJavaScriptを利用しているため動かない。

localhost:8080で確認する

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

gatsby入門 チュートリアルをこなす 7. プログラムでデータからページを作成する

gatsbyの作業履歴

gatsby入門 チュートリアルをこなす 0.開発環境をセットアップする
gatsby入門 チュートリアルをこなす 1. ギャツビービルディングブロックについて知る(1)
gatsby入門 チュートリアルをこなす 1. ギャツビービルディングブロックについて知る(2)
gatsby入門 チュートリアルをこなす 2. ギャツビーのスタイリングの概要
gatsby入門 チュートリアルをこなす 3. ネストされたレイアウトコンポーネントの作成
gatsby入門 チュートリアルをこなす 4. ギャツビーのデータ
gatsby入門 チュートリアルをこなす 5. ソースプラグインとクエリされたデータのレンダリング
gatsby入門 チュートリアルをこなす 6. 変圧器プラグイン※Transformer pluginsのgoogle翻訳
今回:gatsby入門 チュートリアルをこなす 7. プログラムでデータからページを作成する
gatsby入門 チュートリアルをこなす 8. 公開するサイトの準備

チュートリアル

今回実施するgatsbyのチュートリアルはこちら
https://www.gatsbyjs.com/tutorial/part-seven/
チュートリアルの冒頭にこう書かれています。

Reactコンポーネントをsrc / pagesに配置することで、引き続きページを作成できます。ただし、プログラムからデータからページを作成する方法を学習します。

※google翻訳です。
早速やっていきましょう。
ソースは前回作ったやつを使用します。

Programmatically create pages from data

Creating slugs for pages

マークダウンページを作成するには、onCreateNodeとcreatePagesの2つのGatsby APIを使用します。
gatsby-node.jsを作成し、以下を記述します。

gatsby-node.js
exports.onCreateNode = ({ node }) => {
  console.log(`Node created of type "${node.internal.type}"`)
}

チュートリアルはこのように記載されています。

このonCreateNode関数は、新しいノードが作成(または更新)されるたびにGatsbyによって呼び出されます。

なるほど。
gatsby開発サーバを再起動します。
起動はgatsby developでできます。
2020-09-06_02h21_53.jpg
なんかログがいっぱい出てその中にマークダウンの文字が。
gatsby-node.jsを修正します。

gatsby-node.js
exports.onCreateNode = ({ node }) => {
  ↓ここから
  if (node.internal.type === `MarkdownRemark`) {
    console.log(node.internal.type)
  }
  ↑ここまで修正
}

マークダウンだけログだしするのね。
再起動
2020-09-06_02h26_55.jpg
出てる出てる
マークダウンファイルのファイル名からURLのパスを作っていきましょう。
。。gatsbyだとパスをslugっていうのか。
gatsby-node.jsを修正

gatsby-node.js
exports.onCreateNode = ({ node, getNode }) => {←ここ修正
  if (node.internal.type === `MarkdownRemark`) {
    const fileNode = getNode(node.parent)←ここ修正
    console.log(`\n`, fileNode.relativePath)←ここ修正
  }
}

再起動
2020-09-06_02h32_44.jpg
これか!
ここからgatsby-source-filesystem pluginを使用
gatsby-node.jsを修正

gatsby-node.js
const { createFilePath } = require(`gatsby-source-filesystem`)←ここ追記

exports.onCreateNode = ({ node, getNode }) => {
  if (node.internal.type === `MarkdownRemark`) {
    console.log(createFilePath({ node, getNode, basePath: `pages` }))←ここ修正
  }
}

再起動
2020-09-06_02h36_42.jpg
出来てる!
ここからcreateNodeFieldを使用してslug(パス)を作ります。
gatsby-node.jsを修正

gatsby-node.js
const { createFilePath } = require(`gatsby-source-filesystem`)
exports.onCreateNode = ({ node, getNode, actions }) => {←ここ修正
  const { createNodeField } = actions←ここ修正
  if (node.internal.type === `MarkdownRemark`) {
    ↓ここから
    const slug = createFilePath({ node, getNode, basePath: `pages` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
    ↑ここまで追記
  }
}

再起動
GraphiQLで以下を実行

query MyQuery {
  allMarkdownRemark {
    edges {
      node {
        fields {
          slug
        }
      }
    }
  }
}

2020-09-06_02h45_04.jpg
キタコレ!

Creating pages

ページ作りだ!
gatsby-node.jsを修正

gatsby-node.js
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions
  if (node.internal.type === `MarkdownRemark`) {
    const slug = createFilePath({ node, getNode, basePath: `pages` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}

↓ここから
exports.createPages = async ({ graphql, actions }) => {
  // **Note:** The graphql function call returns a Promise
  // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise for more info
  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  console.log(JSON.stringify(result, null, 4))
}
↑ここまで追記

この時点でどうなるのかさっぱりわからんが続けよう。
src/templatesディレクトリを作成
src/templates/blog-post.jsを作成し以下を記述

blog-post.js
import React from "react"
import Layout from "../components/layout"

export default function BlogPost() {
  return (
    <Layout>
      <div>Hello blog post</div>
    </Layout>
  )
}

gatsby-node.jsを修正

gatsby-node.js
const path = require(`path`)←ここ追記
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions
  if (node.internal.type === `MarkdownRemark`) {
    const slug = createFilePath({ node, getNode, basePath: `pages` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions←ここ追記
  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  ↓ここから
  result.data.allMarkdownRemark.edges.forEach(({ node }) => {
    createPage({
      path: node.fields.slug,
      component: path.resolve(`./src/templates/blog-post.js`),
      context: {
        // Data passed to context is available
        // in page queries as GraphQL variables.
        slug: node.fields.slug,
      },
    })
  })
  ↑ここまで追記
}

再起動
以下でアクセス
http://localhost:8000/sdf
※sdfは何でもいい
2020-09-06_03h00_20.jpg
あ、パスが出来てる
アクセス
2020-09-06_03h01_14.jpg
マークダウンファイルはまだ出てこない。
なるほど、ちょっとわかったぞ。
gatsby-node.jsのonCreateNodeでマークダウンのファイル名を取得してslug(パス)を作成。
createPagesでそのslugを取得し、繰り返し処理の中でパスとsrc/templates/blog-post.jsを同期させるのね!
つづき!
src/templates/blog-post.jsの修正

src/templates/blog-post.js
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"

export default function BlogPost({ data }) {
  const post = data.markdownRemark
  return (
    <Layout>
      <div>
        <h1>{post.frontmatter.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.html }} />
      </div>
    </Layout>
  )
}

export const query = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
      }
    }
  }
`

うんうん。graphqlでマークダウンのhtml情報を取得してsrc/templates/blog-post.jsに書き込む感じか。
2020-09-06_03h15_57.jpg
出来た!
そしてsrc/pages/index.jsを修正

src/pages/index.js
import React from "react"
import { css } from "@emotion/core"
import { Link, graphql } from "gatsby"←ここ修正
import { rhythm } from "../utils/typography"
import Layout from "../components/layout"

export default function Home({ data }) {
  return (
    <Layout>
      <div>
        <h1
          css={css`
            display: inline-block;
            border-bottom: 1px solid;
          `}
        >
          Amazing Pandas Eating Things
        </h1>
        <h4>{data.allMarkdownRemark.totalCount} Posts</h4>
        {data.allMarkdownRemark.edges.map(({ node }) => (
          <div key={node.id}>
            ↓ここから
            <Link
              to={node.fields.slug}
              css={css`
                text-decoration: none;
                color: inherit;
              `}
            >
            ↑ここまで修正
              <h3
                css={css`
                  margin-bottom: ${rhythm(1 / 4)};
                `}
              >
                {node.frontmatter.title}{" "}
                <span
                  css={css`
                    color: #555;
                  `}
                >
                   {node.frontmatter.date}
                </span>
              </h3>
              <p>{node.excerpt}</p>
            </Link>←ここ修          </div>
        ))}
      </div>
    </Layout>
  )
}

export const query = graphql`
  query {
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      totalCount
      edges {
        node {
          id
          frontmatter {
            title
            date(formatString: "DD MMMM, YYYY")
          }
          ↓ここから
          fields {
            slug
          }
          ↑ここまで修正
          excerpt
        }
      }
    }
  }
`

再起動
index.jsからpostにアクセスできるようになった!
チュートリアルはここまでかな。

これ、src/pages内にmdファイルあるのが気になるから移動できないかしら?
src/mdディレクトリ作成し、mdファイルを移動
2020-09-06_03h26_25.jpg
再起動
2020-09-06_03h27_21.jpg
うん、問題ないね!
今回はここまで。

ありがとうございました。

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

①React + Firebase + Google-API を使用して、Googleクローンを作成してみる。

概要

React,Firebase, Google-APIを使って、Googleのクローンを作成します。
流行りのMarerial-UIも使用しています。
長くなるので、ReactのフロントエンドとGoogle-API,Firebaseを使用したバックエンドは分けて投稿する予定です。

デプロイ先
URL: https://clone-6bd5b.firebaseapp.com/
g-c-h-complete
Googleの検索結果と同じものが得られます。
Google-API(無料プラン)には、一日の検索数に制限があるので、スパムはやめてくださると幸いです。

目次

1.App.jsファイル
2.Maretial-UI
3.Home.js
4.Search.js 検索バー
5.まとめ
6.最後に

1. App.jsファイル

react-router-domを
npm install react-router-dom
でダウンロードしておきましょう。

App.js
import React from 'react';
import './App.css';
import Home from "./Pages/Home";
import { BrowserRouter as Router,  Switch, Route} from "react-router-dom";


function App() {
  return (
    <div className="app">
        <Router>
            <Switch>
                <Route path="/">
                    <Home />
                </Route>
            </Switch>

        </Router>

    </div>
  );
}

export default App;

2 Maretial-UI

使用するMaterial-UIのインポート

https://material-ui.com/ 

npm install @material-ui/core

これだけではiconを使用できないので、
npm install @material-ui/icons
アイコンも忘れずにインストールしておきましょう。

ここまですると、ここのMaterial Iconsから使いたいアイコンを検索して使用することができます
https://material-ui.com/components/material-icons/

material-ui

今回使用するアイコンの1つのAPPsを選択すると、
material-ui-app
以下の画面が出てくるので、
import AppsIcon from '@material-ui/icons/Apps;'
をコピーして、Home.jsファイルに貼り付けます。

3 Home.js

Home.js
import React from 'react';
import './Home.css';
import { Link } from 'react-router-dom';
import AppsIcon from '@material-ui/icons/Apps';
import {Avatar} from "@material-ui/core";

function Home() {
    return (
      <div className="home">
                <div className="home__header">
                    <div className='home__headerLeft'>
                            <Link to='/about'>About</Link>
                            <Link to='/store'>Store</Link>

                    </div>
                    <div className='home__headerRight'>
                            <Link to='/gmail'>Gmail</Link>
                            <Link to='/images'>Images</Link>
                        <AppsIcon />
                        <Avatar />

                    </div>
                </div>
            </div>

      )
 }

 export default Home

コンポーネントを以下のように、home__header その中身を左側と右側に分けました。

home__header

cssファイル

Home.css
.home {
    display: flex;
    flex-direction: column;
    height: 100vh;

}
.home__header{
    display: flex;
    justify-content: space-between;
    padding: 20px 30px;
    align-items: center;
}

.home__header a{
    margin-right: 20px;
    text-decoration: inherit;
    color: rgba(0,0,0,0.87);
    font-size: 15px;
}
.home__header a:hover{
    text-decoration: underline;
}

.home__headerRight {
    display: flex;
    align-items: center;
    min-width: 13vw;
    justify-content: space-between;

}
.home__headerRight > .MuiSvgIcon-root {
    margin-right: 20px;
}

<a href > はページがリフレッシュしてしまいますので、React では、<Link>使ったほうが良いです。

Home.css
.home__header a{
 text-decoration: inherit;
}

Home.jsファイル内で使用している <Link to=...> ... </Link>において、
Linkは aで参照してスタイルできます
text-decoration: inherit;は、でデフォルトで表示されるアンダーラインを表示させない設定です。

Home.css
.home__header a:hover{
    text-decoration: underline;
}

hover 簡単に説明するとマウスが乗ったのみに起こるイベントです。
この場合、マウスが乗った時、アンダーラインを表示させます。

Home.css
.home__headerRight > .MuiSvgIcon-root {
    ...
}

.MuiSvgIcon-rootは、Material-uiのiconをスタイリングできます。

Home.css
<div className="home">
 <div className="home__header">
           ...
 </div> 

 {/*ここに追加します*/}
  <div className='home__body'>
      <img
        src="https://www.google.com/logos/doodles/2020/aya-kodas-116th-birthday-6753651837108515-l.png"
        alt=""
      />
     <Search />
  </div>

</div>

css: home__body

Home.css
.home__body {
    flex: 1;
    display: flex;
    margin-top: 10%;
    flex-direction: column;

}
.home__body > img {
    object-fit: contain;
    height: 200px;
}

4 Search.js 検索バー

ここで、Search.jsコンポーネントを利用し、検索バーを作成していきます。

Search.js
import React from 'react';
import './Search.css';
import SearchIcon from '@material-ui/icons/Search';
import MicIcon from '@material-ui/icons/Mic';
import {Button} from "@material-ui/core";


function Search() {
    return (
         <form className="search">
            <div className='search__input'>
                <SearchIcon className="search__inputIcon"/>
                <input />
                <MicIcon />
            </div>
             <div className='search__buttons'>
                <Button type="submit" variant="outlined">Google Search</Button>
                <Button variant="outlined">I'm Feeling Lucky</Button>

            </div>
         </form>
      )

 }

 export default Search;

Search.cssファイル

Search.css
.search__input {
    display: flex;
    align-items: center;
    border: 1px solid lightgray;
    height: 30px;
    padding: 10px 20px;
    border-radius: 999px;
    width: 75vw;
    margin: 0 auto;
    margin-top: 40px;
    max-width: 560px;

}

.search__input > input {
    flex: 1;
    padding: 10px 20px;
    font-size: medium;
    border: none;
}
.search__input > input:focus{
    outline-width: 0;
}
.search__inputIcon {
    color: gray;
}
.search__buttons {
    margin-top: 30px;
    display: flex;
    justify-content: center;
}
.search__buttons button {
    margin: 5px;
    padding: 7px 15px;
    background-color: #f8f8f8;
    border: 1px solid white;
    text-transform: inherit;
    color: #5f6368;
}
.search__buttons button:hover{
    box-shadow: 0 1px 1px rgba(0,0,0,0.1);
    background-color: lightgray;
}

まとめ

ここまでやるとホームUIの完成です。

google-clone-home

最後に

ホーム画面が完成してキリが良いので、今回はここで区切らせてもらいます。
第二回は、検索した際に表示される画面と,
javascriptの機能などを実装していきたいと考えています。

第三回は(最終回を予定)、Google-APIとFirebaseを実装させていきます。

お疲れさまでした。

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

gatsby入門 チュートリアルをこなす 6. 変圧器プラグイン※Transformer pluginsのgoogle翻訳

gatsbyの作業履歴

gatsby入門 チュートリアルをこなす 0.開発環境をセットアップする
gatsby入門 チュートリアルをこなす 1. ギャツビービルディングブロックについて知る(1)
gatsby入門 チュートリアルをこなす 1. ギャツビービルディングブロックについて知る(2)
gatsby入門 チュートリアルをこなす 2. ギャツビーのスタイリングの概要
gatsby入門 チュートリアルをこなす 3. ネストされたレイアウトコンポーネントの作成
gatsby入門 チュートリアルをこなす 4. ギャツビーのデータ
gatsby入門 チュートリアルをこなす 5. ソースプラグインとクエリされたデータのレンダリング
今回:gatsby入門 チュートリアルをこなす 6. 変圧器プラグイン※Transformer pluginsのgoogle翻訳
gatsby入門 チュートリアルをこなす 7. プログラムでデータからページを作成する
gatsby入門 チュートリアルをこなす 8. 公開するサイトの準備

チュートリアル

今回実施するgatsbyのチュートリアルはこちら
https://www.gatsbyjs.com/tutorial/part-six/
チュートリアルの冒頭にこう書かれています。これ重要

前のチュートリアルでは、ソースプラグインがどのようにデータをギャツビーのデータシステムに取り込むかを示しました。このチュートリアルでは、変換プラグインがソースプラグインによってもたらされる生のコンテンツを変換する方法を学びます。ソースプラグインとトランスフォーマープラグインの組み合わせは、Gatsbyサイトの構築時に必要になる可能性のあるすべてのデータソースとデータ変換を処理できます。

※google翻訳
チュートリアルをざっと見た感じ、ブログの投稿機能が実装できるようになるのかしら。
早速やっていきましょう。
ソースは前回作ったやつを使用します。

Transformer plugins

Transformer plugins

マークダウンファイルをサイトに追加し、変換プラグインとGraphQLを使用してHTMLに変換する方法を理解する。
マークダウンファイル(src/pages/sweet-pandas-eating-sweets.md)を作成します。

src/pages/sweet-pandas-eating-sweets.md
---
title: "Sweet Pandas Eating Sweets"
date: "2017-08-10"
---

Pandas are really sweet.

Here's a video of a panda eating sweets.

<iframe width="560" height="315" src="https://www.youtube.com/embed/4n0xNbfJLR8" frameborder="0" allowfullscreen></iframe>

http://localhost:8000/my-files
2020-09-05_21h58_22.jpg
出てきた!
これはgatsby-source-filesystemを常に、追加する新しいファイルをスキャンしているようです。
いいね。もうサーバ処理いらない。
続けてtransformer pluginをインストール
サーバ停止して以下を実行
npm install --save gatsby-transformer-remark
※tutorial-part-fourディレクトリ直下で実行
インストール後gatsby-config.jsを修正

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: `Pandas Eating Lots`,
  },
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/`,
      },
    },
    `gatsby-transformer-remark`,←これ追記
    `gatsby-plugin-emotion`,
    {
      resolve: `gatsby-plugin-typography`,
      options: {
        pathToConfigModule: `src/utils/typography`,
      },
    },
  ],
}

再起動
2020-09-05_23h49_38.jpg
お、追加されてる。
2020-09-05_23h53_53.jpg
チュートリアル通りにクエリを実行してチュートリアル通りの結果。よし。

Create a list of your site’s markdown files in

投稿リストをindexに表示するよう以下を記述

src/pages/index.js
import React from "react"
import { graphql } from "gatsby"
import { css } from "@emotion/core"
import { rhythm } from "../utils/typography"
import Layout from "../components/layout"

export default function Home({ data }) {
  console.log(data)
  return (
    <Layout>
      <div>
        <h1
          css={css`
            display: inline-block;
            border-bottom: 1px solid;
          `}
        >
          Amazing Pandas Eating Things
        </h1>
        <h4>{data.allMarkdownRemark.totalCount} Posts</h4>
        {data.allMarkdownRemark.edges.map(({ node }) => (
          <div key={node.id}>
            <h3
              css={css`
                margin-bottom: ${rhythm(1 / 4)};
              `}
            >
              {node.frontmatter.title}{" "}
              <span
                css={css`
                  color: #bbb;
                `}
              >
                 {node.frontmatter.date}
              </span>
            </h3>
            <p>{node.excerpt}</p>
          </div>
        ))}
      </div>
    </Layout>
  )
}

export const query = graphql`
  query {
    allMarkdownRemark {
      totalCount
      edges {
        node {
          id
          frontmatter {
            title
            date(formatString: "DD MMMM, YYYY")
          }
          excerpt
        }
      }
    }
  }
`

queryでtitleと日付取得して表示する感じね。
2020-09-06_00h01_30.jpg
いいね。
もう1件mdファイルを追加しちゃおう。

src/pages/pandas-and-bananas.md
---
title: "Pandas and Bananas"
date: "2017-08-21"
---

Do Pandas eat bananas? Check out this short video that shows that yes! pandas do seem to really enjoy bananas!

<iframe width="560" height="315" src="https://www.youtube.com/embed/4SZl1r2O_bY" frameborder="0" allowfullscreen></iframe>

2020-09-06_00h04_27.jpg
いいね!再起動だなんだが不要でソッコーで反映されるのが素晴らしい!
queryを以下にすると、日付の降順で取得できる

src/pages/index.js(query部分)
export const query = graphql`
  query {
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {←ここ修正
      totalCount
      edges {
        node {
          id
          frontmatter {
            title
            date(formatString: "DD MMMM, YYYY")
          }
          excerpt
        }
      }
    }
  }
`

2020-09-06_00h13_46.jpg
ソートが変わったぜ!

チュートリアルはここまでか。
たぶん、今後mdファイルを表示する処理があるのかな。

今回はここまで。

ありがとうございました。

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