20200925のReactに関する記事は6件です。

React-Redux 私的まとめ

こちらの記事は以下の書籍を参考にアウトプットとして執筆しました。
React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで

ルーティング

実行パターン

URL遷移なし

Store内にpageプロパティを実装するなどで判断

URLハッシュ

取得はlocation.hash
ハッシュが変更をきっかけにしている

history API

ブラウザの履歴情報を操作することができるAPIを使う手法

ルーティングライブラリ

react-router v4

API

history APIを使う

import { render } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux'
import { BrowserRouter as Router, Route } from 'react-router-dom'

// 略

render(
  <Provider store={store} >
    <Router>
      <Route path="/" component={App} />
    </Router>
  </Provider>,
  document.getElementByid('root')
)

引用:React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで

BrowserRouterより下からRouteが使える

react-router-redux

react-routerを更にreduxに最適化できる
ルーティング情報をStoreのStateで管理

Reduxの非同期処理

redux-thunkによる非同期処理

ReduxではAPIからのレスポンスはActionとして扱う

thunkミドルウェア

ミドルウェア適応

import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import reducers from './reducers'

const middlewares = [logger, thunk]

const store = createStore(reducers, applyMiddleware(...middlewares))
//createStoreの第2引数はStoreの初期オプションを与える

export default store

参考:React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで

import shortid from 'shortid'
import * as types from '../types/todo'

// 同期Actionクリエータ
export function addTodo(title) {
  return {
    type: types.ADD_TODO,
    payload: {
      id: shortid.generate(),
      title,
    }
  }
}
// 非同期Actionクリエータ
// 関数をリターン
export function asyncAddTodo(title) {
  return (dispatch, getState) => {
    setTimeout(() => {
      dispatch(addTodo(title))
    }, 1000);
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VSCodeでwebpack-dev-server上のReactをデバックする

目標

webpack-dev-server上で実行したReactをVSCodeでデバックできるようにする。

環境

  • OS
    • macOS Catalina 10.15.6
    • Windows 10
  • VSCode: 1.49.2
    • Debugger for Chrome: 4.12.11
  • React: 16.13.1
  • webpack: 4.43.0
  • webpack-dev-server: 3.11.0

Webpackの設定

ソースマップの設定が必要になります。
ソースマップはバンドルする前と後のコードを関連付けてくれるものです。

webpackのconfigにdevtoolsを設定するとソースマップが利用できるようになります。
例として自分が設定した値を載せます。

webpack.config.js
module.exports = {
  ...
  devtools: 'inline-source-map',
  devServer: {
    ...

他のパラメータに関しては以下を参考にしてください。
https://webpack.js.org/configuration/devtool/

上記config設定後、実行時の引数として -dを付与すると、VSCodeでデバック可能なwebpack-dev-serverが立ち上がります。

webpack-dev-server -d

VSCodeの設定

以下デバックに必要なExtensionsのインストールと、launch.jsonの編集が必要になります。

Debugger for Chrome

https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome

launch.json
{
  "configurations": [
    {
      "name": "Launch Chrome",
      "request": "launch",
      "type": "pwa-chrome",
      "url": "http://localhost:8080",
      "webRoot": "${workspaceFolder}"
    }
  ]
}

デバック方法

  1. webpack-dev-serverを起動
  2. デバック画面からlaunchを起動
  3. 起動したChromeで画面操作
  4. ブレークで止まった際にデバック画面を操作

おわりに

以上でVSCodeでwebpack-dev-server上で動く、Reactがデバックできるようになります。

バンドル化する前のコードで追いかけることが可能になりますので、かなり楽になるのではないでしょうか。

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

React + TypeScript + vte.cxで簡単なWebアプリを作ってみた④CSV出力編

はじめに

この記事はvte.cx(ブイテックス)というBaaSを使って登録した情報をCSV出力機能を実装してみた記事です。

今回実装した機能

image.png

今回実装したのは特定のURLにアクセスすると上記のようなCSVが出力される機能です。

今回出てくる難しいIT用語録(分かる人は読み飛ばして大丈夫です)

CSV is 何

名前と見た目はなんとなく知っていたのですがそれがどういうものかうまく説明できなかったので調べてみました。

comma-separated values(略称:CSV)は、いくつかのフィールド(項目)をカンマ「,」で区切ったテキストデータおよびテキストファイル。拡張子は .csv、MIMEタイプは text/csv。
「comma-separated variables」とも言う。広く普及した訳語はないが、「カンマ区切り」などとも呼ばれる。Microsoft Excelでは「CSV (カンマ区切り)」としている。
(中略)
引用:Wikipedia|Comma-Separated Values

カンマ区切りされたファイルらしいですね。

ちなみにExcelで開けるそうなのですが、

CSVファイルはExcelでも参照・編集が可能です。しかし、CSVファイルをExcelで編集する時には注意が必要です。
CSVファイルには、ExcelやDBなどにあるデータ型の情報が入っていません。抽出元のシステムのデータ型が文字列型、数値型、日付型などに関係なく全て文字情報として出力されています。この様なCSVファイルをExcelで普通に開いた場合、Excelの解釈でデータ型が設定され、結果、下記のような元のCSVファイルの情報と違う形で表示されてしまいます。(例は上記のCSVファイルをExcelで開いた場合の例です)

型は全て文字列なのでExcelに合わせる際に不具合がでる可能性があるそうです。

api is 何

「API」とは、「Application Programming Interface」の頭文字です。

大雑把に言うと「アプリケーションをプログラミングするためのインターフェース」という意味です。

インターフェイスとは、コンピュータ用語でいうと、「何か」と「何か」をつなぐものという意味を持ちます。例えば、USBも「パソコン」と「周辺機器」をつなぐものですので、インターフェイスの一つです。

つまり、APIとは、この「何か」と「何か」が「アプリケーション、ソフトウェア」と「プログラム」をつなぐもの、という意味になります。

アプリとアプリを繋げることによって、機能性を拡張させ、さらに便利に使えるようにし、欲を言えば両方のアプリにとってウィン・ウィンの状態を生み出すのがAPIの狙いです。

出典:データのじかん

簡単にいうと外部の人が作ったプログラムをとってきて自分のコードのなかで使うといったものです。

サーバーサイドレンダリング is 何

本来JavaScriptで行う画面の書き換え処理などをサーバー側で実行させて、ユーザーの待機時間を短くすること。

PHPなどを使ってwebアプリケーションを作成した場合もSSRではあるが、
あえて「SSR」と呼ぶときは、上記のようにJavaScriptと対比させて表現することが多い。

出典:SSR(サーバーサイドレンダリング)とは

画面表示など、ブラウザでやると時間のかかる処理を見えないところでやっちゃおうとったものですね。

手順

ではCSV出力の仕方を説明していきます。

vtecxを使ったcsv出力ではサーバーサイドJavascriptを使用する必要があります。

vte.cxによるバックエンドを不要にする開発(7.サーバサイドJavaScript)

こちらの記事を参考に進めていきます。

大まかな流れとしては

①プロジェクト直下のserverフォルダの中に{ファイル名}.csv.tsxという名前のファイルを作る。

②以下のコマンドでサーバにデプロイする。

npm run watch:server -- --env.entry=/server/ファイル名.tsx

③ブラウザからhttp://{サービス名}/s/ファイル名.csvを開く

では実際作ったコードを使って一連の流れを進めていきます。

get-csv-users-information.csv.tsx(csv出力をするファイル)

server/get-csv-users-information.csv.tsx
import * as vtecxapi from 'vtecxapi'

const feed_data = vtecxapi.getFeed('/users')
// 配列で帰ってくる

const users_info_body = feed_data.map((entry: any) => {
    console.log(JSON.stringify(entry.users))
    // 名前
    const name = entry.users.name || '未登録'
    // 性別
    const gender = entry.users.gender || '未登録'
    // 年齢
    const age = entry.users.name || '未登録'
    // 住所
    const address = entry.users.address || '未登録'
    // パスワード
    const password = entry.users.password || '未登録'
    // メールアドレス
    const email = entry.users.email || '未登録'
    // 郵便番号
    const post_number = entry.users.post_number || '未登録'
    // 好きな住居形態
    const like_residence_type = entry.users.like_residence_type || '未登録'
    // 役職
    const position = entry.users.position || '未登録'
    // 使用言語
    const language = entry.users.language ? `"${entry.users.language}"` : '未登録'

    const array: string[] = []
    array.push(name)
    array.push(gender)
    array.push(age)
    array.push(address)
    array.push(password)
    array.push(email)
    array.push(post_number)
    array.push(like_residence_type)
    array.push(position)
    array.push(language)

    return array
})

//ヘッダー
const user_info_header = ['名前', '性別', '年齢', '住所', 'パスワード', 'メールアドレス', '郵便番号', '好きな住居形態', '役職', '使用言語']

let body: any = [user_info_header, ...users_info_body]

// CSV出力
vtecxapi.doResponseCsv(body, 'summary_index_' + '.csv')

まず最初に

server/get-csv-users-information.csv.tsx
import * as vtecxapi from 'vtecxapi'

const feed_data = vtecxapi.getFeed('/users'

としてサーバーサイドレンダリングするためのapiをimportし、vtecxapi.getFeed()というメソッドを使い、エンドポイントに登録されているデータを取得します。

今回サーバーサイドJSということでブラウザの開発者ツールでconsole.logできなく、「どうすれば登録しているデータを確認することができるんだ...」と思っていましたが、

image.png

vtecxの管理画面に入り、ログを見ることで確認できました。

server/get-csv-users-information.csv.tsx
const users_info_body = feed_data.map((entry: any) => {

    // 名前
    const name = entry.users.name || '未登録'
    // 性別
    const gender = entry.users.gender || '未登録'
    // 年齢
    const age = entry.users.name || '未登録'
    // 住所
    const address = entry.users.address || '未登録'
    // パスワード
    const password = entry.users.password || '未登録'
    // メールアドレス
    const email = entry.users.email || '未登録'
    // 郵便番号
    const post_number = entry.users.post_number || '未登録'
    // 好きな住居形態
    const like_residence_type = entry.users.like_residence_type || '未登録'
    // 役職
    const position = entry.users.position || '未登録'
    // 使用言語
    const language = entry.users.language ? `"${entry.users.language}"` : '未登録'

    const array: string[] = []
    array.push(name)
    array.push(gender)
    array.push(age)
    array.push(address)
    array.push(password)
    array.push(email)
    array.push(post_number)
    array.push(like_residence_type)
    array.push(position)
    array.push(language)

    return array
})

//ヘッダー
const user_info_header = ['名前', '性別', '年齢', '住所', 'パスワード', 'メールアドレス', '郵便番号', '好きな住居形態', '役職', '使用言語']

let body: any = [user_info_header, ...users_info_body]

// CSV出力
vtecxapi.doResponseCsv(body, 'summary_index_' + '.csv')

次に先ほど取得したデータをmapメソッドを使って必要なデータを配列に格納していきます。

mapメソッド Javascript公式サイト

(mapメソッドは取得したデータを自分の意図した通りにすることができるフロントエンド開発に必須のものなのでしっかりマスターしたいものです。)

取得したデータはmapメソッドによって

users_info_body = [['name','gender','age',.....'language'],['名前','未登録',.....'日本語']]

といった構造になります。

それでここからがCSV出力の本題です。

vtecxapi.doResponseCsv(出力したい配列,CSVのファイル名)

というメソッドを使うことでurlアクセス時にCSVを出力することができます。

出力したい配列なのですが、例えば

const date_array: any = ['集計期間']
let body: any = [date_array, ['a', 'b', 'c'], ['aa', 'bb', 'cc']]
// CSV出力
vtecxapi.doResponseCsv(body, 'summary_index_' + '.csv')

とすると、

image.png

このようなCSVファイルを出力することができます

配列一つが1行になって左づめで入力されていきます。
また第二引数のファイルネームはsummary_index.csvという名前で指定しているのでちゃんとその名前の通り出力されました。

感想

登録された情報を取得してCSVに出力する機能はfirebaseにはない機能で、vte.cxというサービス内で用意されているのでかなり使い勝手が良いなと思いました。
集計処理をするアプリなどとても作りやすそうです。

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

Reactコンポーネントの状態を確認するにはReact Developer Toolsを使う

目的

WEB上で動作しているページでReactコンポーネントの中身がどのようになっているかを確認する。
→入力やページ遷移での変化を目視確認する

導入方法

  • GoogleChromeの拡張機能「React Developer Tools」を利用する。

  • 導入後にコンポーネントを確認したいページでF12を押すと、
    以下画像のように「Components」というタグが追加されている。
    ここでそのページに関わるコンポーネントとその内容が全て確認できる。
    デベロッパーツールのように直接編集することも可能。
    React Developer Tools

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

【Gatsby.js】jsonファイルのデータをGraphQL経由で取得する

この記事ではjsonファイルからGraphQL経由でデータを取得し、ページ内で利用する方法を紹介します。

jsonファイルを元に、複数ページを生成する方法を知りたい場合は昨日書いた以下の記事を参照。
【Gatsby.js】単一のjsonファイルを元に複数のページを生成する - Qiita

jsonファイルを配置

$ mkdir src/data
$ touch src/data/events.json
src/data/events.json
[
  {
    "title": "Gatsby勉強会",
    "date": "2020/10/01"
  },
  {
    "title": "React勉強会",
    "date": "2020/11/01"
  },
  {
    "title": "JavaScript勉強会",
    "date": "2020/12/01"
  }
]

プラグインをインストール

$ yarn add gatsby-transformer-json gatsby-source-filesystem
gatsby-config.js
module.exports = {
  plugins: [
    `other-plugin-hoge`,
    //追記↓
    `gatsby-transformer-json`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `data`,
        path: `${__dirname}/src/data/`,
      },
    },
  ],
}

クエリを作成

gatsby developで開発サーバーを起動し、http://localhost:8000/___graphql?query にアクセスしGraphiQLを開く。
まずはroot?にallEventsJsonという項目が追加されていることを確認。

allEventJson > edges > nodeを開き、titledateにチェックを入れてクエリを発行。

データを使いたいページ内で、データを取得

ここではsrc/pages/events.jsというページでデータを利用します。

$ touch src/pages/events.js
src/pages/events.js
import React from "react"
import { graphql, useStaticQuery } from "gatsby"

export default () => {
  const data = useStaticQuery(graphql`
    query {
      allEventsJson {
        edges {
          node {
            date
            title
          }
        }
      }
    }
  `)
  const events = data.allEventsJson.edges
  return (
    <div>
      {events.map(e => (
        <div>
          <h2>{e.node.title}</h2>
          <p>{e.node.date}</p>
        </div>
      ))}
    </div>
  )
}

結果

 2020-09-25 12.10.14.png

参考

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

Styled-componentsでシンプルにグリッドレイアウトを実装する

はじめに

ReactとStyled-compomentsを使って、シンプルに12カラムのグリッドレイアウトを実装してみました。

カラムレイアウトにはFlexboxを使っています。
Reactの環境構築はcreate-react-appで行います。

なお、Styled-compomentsの概要については省略します。

グリッドレイアウトの概要

  • 12カラムのグリッドに沿って、横並びやレスポンシブのアイテムを配置していきます。
  • 各カラムの左右にはガター(溝)のpaddingがあります。12カラム全体では左右に(ガター/2)分のpaddingとなります。

グリッドレイアウトについてあまり聞きなれない場合は下記リンク等が参考になると思います。

Bootstrapのグリッドシステムの使い方を初心者に向けておさらいする
Grid system - Bootstrap 4.5 - 日本語リファレンス

1. Reactプロジェクトの準備

CRAをTypeScript付きでプロジェクト作成し、Styled-compoments関連のパッケージもインストールします。

$ npx create-react-app grid-layout-styled-components --typescript
$ cd grid-layout-styled-components
$ npm install --save styled-components
$ npm install @types/styled-components
$ npm install --save-dev babel-plugin-styled-components

次にbabel-plugin-styled-componentsを使うための準備をします。

$ touch .babelrc

.babelrc

{
  "plugins": [
    [ "babel-plugin-styled-components" ]
  ]
}

最後にindex.cssとApp.cssの中身を一旦空にしておきます。

2. グローバルスタイルの作成

グローバルに適用するスタイルを作成します。

$ touch src/GlobalStyle.ts

はじめにリセットCSSを追加します。好みによりNormalizeCSSでも可能だと思います。
リセットCSSの中では個人的にEric Meyer氏のものがシンプルで好きです。

GlobalStyle.ts
import { createGlobalStyle } from 'styled-components/macro';

export const GlobalStyle = createGlobalStyle`
  /* Reset CSS */
  /* ===================================== */

  /* http://meyerweb.com/eric/tools/css/reset/ 
    v2.0 | 20110126
    License: none (public domain)
  */

  html, body, div, span, applet, object, iframe,
  h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  a, abbr, acronym, address, big, cite, code,
  del, dfn, em, img, ins, kbd, q, s, samp,
  small, strike, strong, sub, sup, tt, var,
  b, u, i, center,
  dl, dt, dd, ol, ul, li,
  fieldset, form, label, legend,
  table, caption, tbody, tfoot, thead, tr, th, td,
  article, aside, canvas, details, embed, 
  figure, figcaption, footer, header, hgroup, 
  menu, nav, output, ruby, section, summary,
  time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
  }
  /* HTML5 display-role reset for older browsers */
  article, aside, details, figcaption, figure, 
  footer, header, hgroup, menu, nav, section {
    display: block;
  }
  body {
    line-height: 1;
  }
  ol, ul {
    list-style: none;
  }
  blockquote, q {
    quotes: none;
  }
  blockquote:before, blockquote:after,
  q:before, q:after {
    content: '';
    content: none;
  }
  table {
    border-collapse: collapse;
    border-spacing: 0;
  }
}
`;

次にタイプセレクタへのCSSを追加します。
ここで必須なのは*, *::before, *::after{box-sizing: border-box;}になります。

ついでに必要ないとは思いますが、フォントはいつでも綺麗にしておきたい性分なのでbodyへのfont-family設定も癖で追加しました。

GlobalStyle.ts
import { createGlobalStyle } from 'styled-components/macro';

export const GlobalStyle = createGlobalStyle`
  /* Reset CSS */
  /* ===================================== */

  /* http://meyerweb.com/eric/tools/css/reset/ 
    v2.0 | 20110126
    License: none (public domain)
  */

  html, body, div, span, applet, object, iframe,
  h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  a, abbr, acronym, address, big, cite, code,
  del, dfn, em, img, ins, kbd, q, s, samp,
  small, strike, strong, sub, sup, tt, var,
  b, u, i, center,
  dl, dt, dd, ol, ul, li,
  fieldset, form, label, legend,
  table, caption, tbody, tfoot, thead, tr, th, td,
  article, aside, canvas, details, embed, 
  figure, figcaption, footer, header, hgroup, 
  menu, nav, output, ruby, section, summary,
  time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
  }
  /* HTML5 display-role reset for older browsers */
  article, aside, details, figcaption, figure, 
  footer, header, hgroup, menu, nav, section {
    display: block;
  }
  body {
    line-height: 1;
  }
  ol, ul {
    list-style: none;
  }
  blockquote, q {
    quotes: none;
  }
  blockquote:before, blockquote:after,
  q:before, q:after {
    content: '';
    content: none;
  }
  table {
    border-collapse: collapse;
    border-spacing: 0;
  }

  /* Add Global CSS */
  /* ===================================== */

  *, *::before, *::after {
    box-sizing: border-box;
  }

  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  }
`;

3. 定数の作成

App.tsxに書いていきます。以下を用意します。

  • ブレークポイント
  • 各ブレークポイントに対応するコンテナの最大幅
  • グリッドの溝
  • カラム数
App.tsx
import React from 'react';
import styled from 'styled-components/macro';
import { GlobalStyle } from './GlobalStyle';
import './App.css';

// configs

const breakpoints: { sm: number; md: number; lg: number; xl: number } = {
  sm: 576,
  md: 768,
  lg: 992,
  xl: 1280,
};

const containerMaxWidths: { sm: number; md: number; lg: number; xl: number } = {
  sm: 540,
  md: 720,
  lg: 960,
  xl: 1250,
};

const gridColumns: number = 12;

const gridGutterWidth: number = 32;

// components

function App() {
  return <div><GlobalStyle /></div>;
}

export default App;

4. 汎用コンポーネント作成

汎用的に使うコンテナのコンポーネントと、グリッド行・グリッド列のコンポーネントを用意します。

実装にあたりBootstrapのscssやcssのソースを参考にしました。
全て理解するのは難解ですが、一つ一つは(Bootstrapを使うかに関係なく)CSSの設計に勉強になることが多いと感じます。

App.tsx
import React from 'react';
import styled from 'styled-components/macro';
import { GlobalStyle } from './GlobalStyle';
import './App.css';

// configs

const breakpoints: { sm: number; md: number; lg: number; xl: number } = {
  sm: 576,
  md: 768,
  lg: 992,
  xl: 1280,
};

const containerMaxWidths: { sm: number; md: number; lg: number; xl: number } = {
  sm: 540,
  md: 720,
  lg: 960,
  xl: 1250,
};

const gridColumns: number = 12;

const gridGutterWidth: number = 32;

// components

const Container = styled.div`
  max-width: 100%;
  @media (min-width: ${breakpoints.sm}px) {
    max-width: ${containerMaxWidths.sm}px;
  }
  @media (min-width: ${breakpoints.md}px) {
    max-width: ${containerMaxWidths.md}px;
  }
  @media (min-width: ${breakpoints.lg}px) {
    max-width: ${containerMaxWidths.lg}px;
  }
  @media (min-width: ${breakpoints.xl}px) {
    max-width: ${containerMaxWidths.xl}px;
  }
  padding-right: ${gridGutterWidth / 2}px;
  padding-left: ${gridGutterWidth / 2}px;
  margin-right: auto;
  margin-left: auto;
`;

const Row = styled.div`
  display: flex;
  flex-wrap: wrap;
  margin-right: ${-gridGutterWidth / 2}px;
  margin-left: ${-gridGutterWidth / 2}px;
`;

type ColProps = {
  sizeDefault: number;
  sizeSm?: number;
  sizeMd?: number;
  sizeLg?: number;
  sizeXl?: number;
};

const Col = styled.div<ColProps>`
  flex: 0 0 ${(props) => (props.sizeDefault / gridColumns) * 100}%;
  max-width: ${(props) => (props.sizeDefault / gridColumns) * 100}%;
  @media (min-width: ${breakpoints.sm}px) {
    flex: 0 0 ${(props) => ((props.sizeSm || props.sizeDefault) / gridColumns) * 100}%;
    max-width: ${(props) => ((props.sizeSm || props.sizeDefault) / gridColumns) * 100}%;
  }
  @media (min-width: ${breakpoints.md}px) {
    flex: 0 0 ${(props) => ((props.sizeMd || props.sizeDefault) / gridColumns) * 100}%;
    max-width: ${(props) => ((props.sizeMd || props.sizeDefault) / gridColumns) * 100}%;
  }
  @media (min-width: ${breakpoints.lg}px) {
    flex: 0 0 ${(props) => ((props.sizeLg || props.sizeDefault) / gridColumns) * 100}%;
    max-width: ${(props) => ((props.sizeLg || props.sizeDefault) / gridColumns) * 100}%;
  }
  @media (min-width: ${breakpoints.xl}px) {
    flex: 0 0 ${(props) => ((props.sizeXl || props.sizeDefault) / gridColumns) * 100}%;
    max-width: ${(props) => ((props.sizeXl || props.sizeDefault) / gridColumns) * 100}%;
  }
  padding-right: ${gridGutterWidth / 2}px;
  padding-left: ${gridGutterWidth / 2}px;
`;


function App() {
  return <div><GlobalStyle /></div>;
}

export default App;

ここで各コンポーネントの解説をします。
なお前提として、モバイルファーストで作っています。

Containerコンポーネント

幅はブレークポイントに対応する最大幅を設定します。
中央寄せした上で、グリッド溝/2を左右のpaddingに与えます。

Rowコンポーネント

はじめにdisplayflexにセットして、
グリッド溝/2のネガティブマージンを左右のmarginに与えます。

Colコンポーネント

幅は12グリッドのいくつ分を占めるかをpropsとして受け取れるようにします。
デフォルトの幅は必須で、sm/md/lg/xl用の幅は任意とします。

※sizeDefaultの名前は最初sizeとする予定だったのですが、元々HTMLにsizeという属性があるようなので止めました。

flexの他にmax-widthにおいても幅を設定していますが、前者だけだと特定ブラウザで動かないようなので後者も設定する必要があるらしいです。(Bootstrapのソースコメントによると)
この辺りは申し訳無いのですが調べずにスルーしています。

余談

なるべくシンプルにという方針で、多少1つのブロックにつき繰り返しは多くなりそうですが
css propを使わない(Sassでいうmixinのような用法)方針で実装しました。

5. 実際に使ってみる

前章のコンポーネントを使ってみます。
グリッドを適用する場合はRowを記述し、その子にColをサイズのpropsと共に指定します。

App.css
.side-border {
  border-right: 1px solid #000;
  border-left: 1px solid #000;
}
App.tsx
import React from 'react';
import styled from 'styled-components/macro';
import { GlobalStyle } from './GlobalStyle';
import './App.css';

// configs

const breakpoints: { sm: number; md: number; lg: number; xl: number } = {
  sm: 576,
  md: 768,
  lg: 992,
  xl: 1280,
};

const containerMaxWidths: { sm: number; md: number; lg: number; xl: number } = {
  sm: 540,
  md: 720,
  lg: 960,
  xl: 1250,
};

const gridColumns: number = 12;

const gridGutterWidth: number = 32;

// components

const Container = styled.div`
  max-width: 100%;
  @media (min-width: ${breakpoints.sm}px) {
    max-width: ${containerMaxWidths.sm}px;
  }
  @media (min-width: ${breakpoints.md}px) {
    max-width: ${containerMaxWidths.md}px;
  }
  @media (min-width: ${breakpoints.lg}px) {
    max-width: ${containerMaxWidths.lg}px;
  }
  @media (min-width: ${breakpoints.xl}px) {
    max-width: ${containerMaxWidths.xl}px;
  }
  padding-right: ${gridGutterWidth / 2}px;
  padding-left: ${gridGutterWidth / 2}px;
  margin-right: auto;
  margin-left: auto;
`;

const Row = styled.div`
  display: flex;
  flex-wrap: wrap;
  margin-right: ${-gridGutterWidth / 2}px;
  margin-left: ${-gridGutterWidth / 2}px;
`;

type ColProps = {
  sizeDefault: number;
  sizeSm?: number;
  sizeMd?: number;
  sizeLg?: number;
  sizeXl?: number;
};

const Col = styled.div<ColProps>`
  flex: 0 0 ${(props) => (props.sizeDefault / gridColumns) * 100}%;
  max-width: ${(props) => (props.sizeDefault / gridColumns) * 100}%;
  @media (min-width: ${breakpoints.sm}px) {
    flex: 0 0 ${(props) => ((props.sizeSm || props.sizeDefault) / gridColumns) * 100}%;
    max-width: ${(props) => ((props.sizeSm || props.sizeDefault) / gridColumns) * 100}%;
  }
  @media (min-width: ${breakpoints.md}px) {
    flex: 0 0 ${(props) => ((props.sizeMd || props.sizeDefault) / gridColumns) * 100}%;
    max-width: ${(props) => ((props.sizeMd || props.sizeDefault) / gridColumns) * 100}%;
  }
  @media (min-width: ${breakpoints.lg}px) {
    flex: 0 0 ${(props) => ((props.sizeLg || props.sizeDefault) / gridColumns) * 100}%;
    max-width: ${(props) => ((props.sizeLg || props.sizeDefault) / gridColumns) * 100}%;
  }
  @media (min-width: ${breakpoints.xl}px) {
    flex: 0 0 ${(props) => ((props.sizeXl || props.sizeDefault) / gridColumns) * 100}%;
    max-width: ${(props) => ((props.sizeXl || props.sizeDefault) / gridColumns) * 100}%;
  }
  padding-right: ${gridGutterWidth / 2}px;
  padding-left: ${gridGutterWidth / 2}px;
`;

// Usage

const Heading = styled.h1`
  font-size: 32px;
  font-weight: bold;
  padding: 24px 0;
`;

type InnerContentProps = { height: number; backgroundColor: string };

const InnerContent = styled.div<InnerContentProps>`
  height: ${(props) => props.height}px;
  background-color: ${(props) => props.backgroundColor};
`;

function App() {
  return (
    <div>
      <GlobalStyle />
      <Container className="side-border">
        <Heading>12 Grid system</Heading>
        <Row>
          {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((v, index) => (
            <Col key={index} sizeDefault={1}>
              <InnerContent height={600} backgroundColor="deepskyblue"></InnerContent>
            </Col>
          ))}
        </Row>

        <Heading>Responsive 1</Heading>
        <Row>
          <Col sizeDefault={12} sizeMd={4} sizeLg={4} sizeXl={4}>
            <InnerContent height={200} backgroundColor="lightgray"></InnerContent>
          </Col>
          <Col sizeDefault={12} sizeMd={4} sizeLg={4} sizeXl={4}>
            <InnerContent height={200} backgroundColor="darkgray"></InnerContent>
          </Col>
          <Col sizeDefault={12} sizeMd={4} sizeLg={4} sizeXl={4}>
            <InnerContent height={200} backgroundColor="gray"></InnerContent>
          </Col>
        </Row>

        <Heading>Responsive 2</Heading>
        <Row>
          <Col sizeDefault={12} sizeMd={12} sizeLg={4} sizeXl={4}>
            <InnerContent height={300} backgroundColor="gold"></InnerContent>
          </Col>
          <Col sizeDefault={12} sizeMd={12} sizeLg={8} sizeXl={8}>
            <InnerContent height={300} backgroundColor="goldenrod"></InnerContent>
          </Col>
        </Row>

        <Heading>Responsive 3</Heading>
        <Row>
          <Col sizeDefault={12} sizeLg={8} sizeXl={8}>
            <Row>
              <Col sizeDefault={6}>
                <InnerContent height={150} backgroundColor="blue"></InnerContent>
              </Col>
              <Col sizeDefault={6}>
                <InnerContent height={150} backgroundColor="darkblue"></InnerContent>
              </Col>
              <Col sizeDefault={6}>
                <InnerContent height={150} backgroundColor="dodgerblue"></InnerContent>
              </Col>
              <Col sizeDefault={6}>
                <InnerContent height={150} backgroundColor="royalblue"></InnerContent>
              </Col>
            </Row>
          </Col>
          <Col sizeDefault={12} sizeLg={4} sizeXl={4}>
            <InnerContent height={300} backgroundColor="crimson"></InnerContent>
          </Col>
        </Row>
      </Container>
    </div>
  );
}

export default App;

上記を出力するとこのようになります。

スクリーンショット 2020-09-25 7.20.16.png

実際の動きは下記GitHubからリンクを参照してください。

https://github.com/yha-1228/grid-layout-styled-components

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