20200317のReactに関する記事は11件です。

Next.js 9.3で追加されたgetStaticPropsでSSRからSSGにする

はじめに

先日、Next.js9.3がリリースされました。

待望のSSGサポートがされ、コミュニティがだいぶ賑わっていましたね。
(個人的にはSassのサポートがかなり嬉しいです)

SSGを使いたいときに、GatsbyではなくNext.jsが選択されることも増えてくるのではないでしょうか?
私は、プライベートでGatsbyも触っていたのですが、業務ではNext.jsを使っており、GraphQLの学習コストも相まってNext.jsを選択する機会が増えていきそうなので嬉しいです。

今回は、SSRで使っていたブログを、今回追加された、getStaticPropsというSSG用のAPIを使ってSSGにしたので、簡単にですがビフォーアフターを載せておきます。

実装

ビフォー(SSR)

Home.getInitialProps = async () => {
  const key = {
    headers: { 'X-API-KEY': process.env.api_key }
  }
  const res = await axios.get('https://self-site.microcms.io/api/v1/skills', key)
  const skills = await res.data

  return { skills }
}
Page                                                           Size     First Load
┌ λ /                                                          17 kB       75.4 kB
└   /_app                                                      289 B       57.2 kB

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

アフター(SSG)

export const getStaticProps: GetStaticProps = async () => {
  const key = {
    headers: { 'X-API-KEY': process.env.api_key }
  }
  const res = await axios.get('https://self-site.microcms.io/api/v1/skills', key)
  const skills = await res.data

  return { props: { skills } }
}
Page                                                           Size     First Load
┌ ● /                                                          11.8 kB     69.8 kB
└   /_app                                                      287 B       56.8 kB

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

ビフォー(SSR)では元々あった、getInitialPropsを使用しています。
今は、getStaticPropsgetServerSidePropsに別れ、これらを使うことが推奨されています。
少しの変更で切り替えれるので、簡単にできました。

参考

公式 getInitialProps
公式 Data fetching

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

[react-native]AsyncStorageの使い方

AsyncStorageとは?

AsyncStorageはreactnativeにおいてデータを永続的に保存を可能とするライブラリの一つです。
reactnative公式では非推奨ですが、expoではまだ推奨しているので勉強してみました。
DBに保存したりいろんな方法でデータの永続化は可能ですが、今回はスマホのストレージに保存するやり方です。

基本的な使い方

・AsyncStorage.setItem(key, value);
・AsyncStorage.getItem(key);
setItemでキーを指定して保存
getItemでキーを指定して取得
といった流れです。実際に試してみます。

App.js
import React from 'react';
import { StyleSheet, View, Button, Text, AsyncStorage } from 'react-native';

export default class App extends React.Component {
  state = {
    num1: 1,
    num2: 2,
  }
  render() {
    const setData = async (no, value) => {
      try {
        await AsyncStorage.setItem(no, value);
      } catch (error) {
        console.log(error);
      }

    }
    const getData = async (no) => {
      try {
        const value = await AsyncStorage.getItem(no);
        switch (no) {
          case "1":
            this.setState({ num1: value })
            break;
          case "2":
            this.setState({ num2: value })
            break;
          default:
        }
      } catch (error) {
        console.log(error);
      }
    }
    return (
      <View style={styles.container}>
        <View style={styles.row}>
          <Text style={styles.textStyle}>No1: {this.state.num1}</Text>
          <Button title="getNo1" onPress={() => { getData('1') }} />
          <Button title="setNo1" onPress={() => { setData('1', '11') }} />
        </View>
        <View style={styles.row}>
          <Text style={styles.textStyle}>No2: {this.state.num2}</Text>
          <Button title="getNo2" onPress={() => { getData('2') }} />
          <Button title="setNo2" onPress={() => { setData('2', '22') }} />
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  row: {
    flexDirection: "row",
    alignItems: "center"
  },
  textStyle: {
    fontSize: 20,
  }

});

Mar-14-2020 10-08-03.gif

配列での使い方

App.js
import React from 'react';
import { StyleSheet, View, Button, Text, AsyncStorage } from 'react-native';

export default class App extends React.Component {
  state = {
    array: [{
      no: "-1"
    }],
  }
  render() {
    const setData = async (data) => {
      try {
        for (var i in data) {
          await AsyncStorage.setItem(i, JSON.stringify(data[i]));
        }
      } catch (error) {
        console.log(error);
      }
    }
    const getData = async (data) => {
      try {
        for (var i in data) {
          var value = await AsyncStorage.getItem(i);
          console.log(JSON.parse(value).no);
        }
      } catch (error) {
        console.log(error);
      }
    }
    const arrayPush = () => {
      this.state.array.push({ no: this.state.array.length })
    }
    return (
      <View style={styles.container}>
        <View style={styles.row}>
          <Button title="arrayPush" onPress={() => { arrayPush() }} />
          <Button title="getarray" onPress={() => { getData(this.state.array) }} />
          <Button title="setarray" onPress={() => { setData(this.state.array) }} />
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  row: {
    flexDirection: "row",
    alignItems: "center"
  },
  textStyle: {
    fontSize: 20,
  }
});

setするときは0~の数字でkeyを設定
取得するときも0~の数字で取得
といった流れです。
また、JSONを使うことで要素の値も簡単に取り出せます!

まとめ

大規模なシステム向けではないと思いますが、
簡単なアプリであれば簡単に組むことができます。
知っておくといいかも?

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

#React でマテリアルUIをインストールして「ローディング中のぐるぐる」を使う

package.json

dependencies に @material-ui/core を追加する

{
  "name": "example",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "react": "^16.13.0",
    "react-dom": "^16.13.0",
+    "@material-ui/core": "latest",
    "react-scripts": "3.4.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

App.js

ぐるぐるを三個置いてみる

import React from 'react';

import CircularProgress from '@material-ui/core/CircularProgress'

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

export default App;

画面例

yarn install
yarn start

して動作確認する

image

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/3039

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

#React で Component を複数個配置した時のエラー と return を囲む空タグの意味 / Parsing error: Adjacent JSX elements must be wrapped in an enclosing tag.

コード

import React from 'react';

import CircularProgress from '@material-ui/core/CircularProgress'

function App() {
  return (
    <CircularProgress />
    <CircularProgress />
    <CircularProgress />
  );
}

export default App;

エラー

image

解決

タグで囲めば良いらしい。

import React from 'react';

import CircularProgress from '@material-ui/core/CircularProgress'

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

export default App;

Fragment

より正しそうな書き方

import React from 'react';

import CircularProgress from '@material-ui/core/CircularProgress'

function App() {
  return (
    <React.Fragment>
      <CircularProgress />
      <CircularProgress />
      <CircularProgress />
  </React.Fragment>
  );
}

export default App;

空のタグ

エラー回避のハックだろうか? 空のタグでも良いみたいだ。

import React from 'react';

import CircularProgress from '@material-ui/core/CircularProgress'

function App() {
  return (
    <>
      <CircularProgress />
      <CircularProgress />
      <CircularProgress />
    </>
  );
}

export default App;

と思ったら Fragment の短縮記法らしい。

https://reactjs.org/docs/fragments.html#short-syntax

image

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/3038

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

ログインなしでコードを共有できるサイトつくた

成果物

https://code.itsumen.com/

リポジトリ

https://github.com/yuzuru2/code_site

開発環境

  • ubuntu 18.04
  • docker
  • docker-compose

使用ライブラリ周り

フロントエンド

  • parcel
  • bootstrap
  • highlight.js
  • react
  • nginx(静的ファイルを配信)

バックエンド

  • typescript
  • nodejs
  • pm2

データベース

  • mongodb

成果物を使うケース

  • ちょろっと書いたコードを誰かに見せたい時

UI

ホーム画面

無題.png

コード閲覧画面

無題.png

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

コード共有サイト作成 docker react node.js mongodb

成果物

https://code.itsumen.com/

リポジトリ

https://github.com/yuzuru2/code_site

開発環境

  • ubuntu 18.04
  • docker
  • docker-compose

使用ライブラリ周り

フロントエンド

  • parcel
  • bootstrap
  • highlight.js
  • react
  • nginx(静的ファイルを配信)

バックエンド

  • typescript
  • nodejs
  • pm2

データベース

  • mongodb

成果物を使うケース

  • ちょろっと書いたコードを誰かに見せたい時

UI

ホーム画面

無題.png

コード閲覧画面

無題.png

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

コード共有サイトつくた

成果物

https://code.itsumen.com/

リポジトリ

https://github.com/yuzuru2/code_site

開発環境

  • ubuntu 18.04
  • docker
  • docker-compose

使用ライブラリ周り

フロントエンド

  • parcel
  • bootstrap
  • highlight.js
  • react
  • nginx(静的ファイルを配信)

バックエンド

  • typescript
  • nodejs
  • pm2

データベース

  • mongodb

成果物を使うケース

  • ちょろっと書いたコードを誰かに見せたい時

UI

ホーム画面

無題.png

コード閲覧画面

無題.png

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

同じWebアプリをjQueryとReactで作った感想

概要

JavaScriptとCSSの取り扱いの勉強用に、
同じWebアプリをjQueryベースをReactベースで兼ねて制作してみました。

Webアプリの機能

  • ヘッダー、フッター、メイン部分があります。
  • メイン部分には登録・削除機能付きのテーブルUIを配置しています。

yha-1228.github.io_ajax-jquery_public_.png

なおデータはmockAPIを使わせて頂いております。

比較したかったこと

  • Ajax処理はどうなるか?
  • CSSによるレイアウト・ビジュアルの構成はどうなるか?

jQuery版

制作手順

スタイリング

基本となるブロックをこのように決めました。
mock.png

横幅調整用のコンテナを決めた後に、HTMLとSassでこれを再現しました。

Sassのフォルダはこのようになりました。

.
├── app 基本的にコンポーネントはここに配置
│   ├── _footer.scss
│   ├── _header.scss
│   ├── _main.scss
│   └── _table-app.scss
├── global
│   ├── _base.scss タイプセレクタへのCSS (SMACSSのBase相当)
│   ├── _configs.scss 色の定数
│   └── _reset.scss リセットCSS
├── shared 共通として切り出すコンポーネント
│   ├── _button.scss
│   └── _container.scss
└── index.scss ここで全て読み込む

ちなみにBEMを採用しています。

src/sass/app/_header.scss
.header {
  padding: 24px 0;
  background-color: $colorPrimary;

  &__logo {
    font-size: 32px;
    font-weight: bold;
    color: $colorBackground;
    &:hover {
      text-decoration: underline;
    }
  }
}

Ajax処理

jQuery Ajaxでクラスを作り、その中にメソッドを配置してテーブルUIを完成させました。
テーブルUIのブロック分けは以下のようになっています。

  • テーブルの新規登録フォーム
  • テーブル本体

ソースコードはこのようになりました。Reactを知った後だと画面がレンダリングする流れを手動で意識することがやや大変に感じます。

index.js
class TableApp {
  constructor($el) {
    this.update($el);
  }

  update($el) {
    this.$el = $el;
    this.$inputs = this.$el.find('input');
    this.$add = this.$el.find('.js-add');
    this.$tbody = this.$el.find('tbody');
    this.baseUrl = 'https://5e6736691937020016fed762.mockapi.io/users';
    this.getJson(this.baseUrl).then((result) => {
      this.users = result;
      this.render(this.users);
      this.$delete = this.$el.find('.js-delete');
      this.bindEvents();
    });
  }

  getJson(url) {
    return $.ajax({
      type: 'GET',
      url: url,
      dataType: 'json'
    });
  }

  postJson(url, data) {
    return $.ajax({
      type: 'POST',
      url: url,
      dataType: 'json',
      data: data
    });
  }

  deleteJson(url) {
    return $.ajax({
      type: 'DELETE',
      url: url,
      dataType: 'json'
    });
  }

  bindEvents() {
    this.handleAddClick = this.handleAddClick.bind(this);
    this.handleDeleteClick = this.handleDeleteClick.bind(this);
    this.$add.on('click', this.handleAddClick);
    this.$delete.on('click', this.handleDeleteClick);
  }

  unBindEvents() {
    this.$add.off('click');
    this.$delete.off('click');
  }

  handleAddClick(e) {
    e.preventDefault();

    const postData = {
      id: '',
      username: $('#username').val(),
      email: $('#email').val()
    };

    this.postJson(this.baseUrl, postData).then((result) => {
      const addedUser = result;
      alert(`Added ${addedUser.username}'s data.`);
      this.unBindEvents();
      this.update(this.$el);
    });
  }

  handleDeleteClick(e) {
    const clickdUserId = $(e.currentTarget).data('id');
    this.deleteJson(`${this.baseUrl}/${clickdUserId}`).then((result) => {
      const deletedUser = result;
      alert(`Deleted ${deletedUser.username}'s data.`);
      this.unBindEvents();
      this.update(this.$el);
    });
  }

  render(users) {
    this.$inputs.val('');

    this.$tbody.html(
      users.map(
        (user) => `
        <tr class="table-app__tbody-row js-row">
          <td class="table-app__table-data align-left">
            <button class="button js-delete" data-id=${user.id}>
              <i class="fas fa-trash-alt"></i>
            </button>
          </td> 
          <td class="table-app__table-data align-left">${user.username}</td>
          <td class="table-app__table-data align-left">${user.email}</td>
        </tr>`
      )
    );
  }
}

React版

制作手順

jQuery版を素に、
- SassからStyled-components
- jQueryからReact
のように移植しました。

スタイリング

コンポーネントの階層に従いフォルダ分けしました。
リセットCSSやタグに設定するスタイル(SMACSSでいうとBase)はcreateGlobalStyleを使うスタイルとして纏め、その他のスタイルは共通用とそうでないもの用でフォルダ分けしました。

src以下のフォルダ構成は以下のようになりました。
Sassバージョンとほぼ変わりありません。

.
├── App 基本的にコンポーネントはここに配置
│   ├── Footer
│   │   ├── Copyright.js
│   │   └── index.js
│   ├── Header
│   │   └── index.js
│   ├── Main
│   │   └── index.js
│   └── TableApp
│       └── index.js
├── GlobalStyles 
│   ├── Base.js タイプセレクタへのCSS (SMACSSのBase相当)
│   ├── Config.js 色の定数
│   └── Reset.js リセットCSS
├── Shared 共通として切り出すコンポーネント
│   ├── Button.js
│   └── Container.js
└── index.js ここで全て読み込む

Ajax処理

Ajax自体にハマった点は特に無かったですが、
フォーム送信時に使う"制御されたコンポーネント"の概念が新鮮でした。

ソースコードはこのようになりました。
テーブルUIは面倒で一纏めにしたので多少スクロール量が増えますが、デザイン用のコンポーネントをロジック用の親コンポーネントが操作する、という流れが見やすくて快適です。

src/App/TableApp/index.jsx
import React, { Component } from 'react';
import styled from 'styled-components/macro';
import { color } from '../../GlobalStyles/Config';
import Button from '../../Shared/Button';

export default class TableApp extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: '',
      email: '',
      error: null,
      loaded: false,
      users: []
    };
    this.handleUsernameChange = this.handleUsernameChange.bind(this);
    this.handleEmailChange = this.handleEmailChange.bind(this);
    this.baseUrl = 'https://5e6736691937020016fed762.mockapi.io/users';
    this.handleAddClick = this.handleAddClick.bind(this);
    this.handleDeleteClick = this.handleDeleteClick.bind(this);
  }

  getJson(url) {
    return fetch(url).then((res) => res.json());
  }

  postJson(url, data) {
    return fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    }).then((res) => res.json());
  }

  deleteJson(url) {
    return fetch(url, { method: 'DELETE' }).then((res) => res.json());
  }

  handleUsernameChange(e) {
    this.setState({ username: e.target.value });
  }

  handleEmailChange(e) {
    this.setState({ email: e.target.value });
  }

  handleAddClick(e) {
    e.preventDefault();

    const data = {
      id: '',
      username: this.state.username,
      email: this.state.email
    };

    this.postJson(this.baseUrl, data).then((result) => {
      const addedUser = result;
      alert(`Added ${addedUser.username}'s data.`);
      this.componentDidMount();
      this.setState({ username: '', email: '' });
    });
  }

  handleDeleteClick(e) {
    const clickedId = e.currentTarget.dataset.id;

    this.deleteJson(`${this.baseUrl}/${clickedId}`).then((result) => {
      const deletedUser = result;
      alert(`Deleted ${deletedUser.username}'s data.`);
      this.componentDidMount();
    });
  }

  componentDidMount() {
    this.getJson(this.baseUrl).then(
      (result) => {
        this.setState({
          error: null,
          loaded: true,
          users: result
        });
      },
      (error) => {
        this.setState({
          error: error,
          loaded: true,
          users: []
        });
      }
    );
  }

  render() {
    const users = this.state.users;

    return (
      <>
        <TableBar>
          <form method="post">
            <InputsWrapper>
              <Label htmlFor="username">username</Label>
              <InputText
                type="text"
                value={this.state.username}
                onChange={this.handleUsernameChange}
                name="username"
                id="username"
              />
              <Label htmlFor="email">email</Label>
              <InputText
                type="text"
                value={this.state.email}
                onChange={this.handleEmailChange}
                name="email"
                id="email"
              />
            </InputsWrapper>
            <Button wide primary onClick={this.handleAddClick}>
              <i className="fas fa-plus"></i>&nbsp;&nbsp;Add
            </Button>
          </form>
        </TableBar>

        <Table>
          <thead>
            <TheadRow>
              <TableHeader align="left">Delete</TableHeader>
              <TableHeader align="left">username</TableHeader>
              <TableHeader align="left">email</TableHeader>
            </TheadRow>
          </thead>
          <tbody>
            {users.map((user) => (
              <TbodyRow key={user.id}>
                <TableData align="left">
                  <Button onClick={this.handleDeleteClick} data-id={user.id}>
                    <i className="fas fa-trash-alt"></i>
                  </Button>
                </TableData>
                <TableData align="left">{user.username}</TableData>
                <TableData align="left">{user.email}</TableData>
              </TbodyRow>
            ))}
          </tbody>
        </Table>
      </>
    );
  }
}

const TableBar = styled.div`
  margin-bottom: 32px;
`;

const InputsWrapper = styled.div`
  margin-bottom: 16px;
`;

const Label = styled.label`
  display: block;
  margin-bottom: 16px;
  font-size: inherit;
  color: ${color.gray01};
`;

const InputText = styled.input`
  width: 100%;
  line-height: 24px;
  margin-bottom: 16px;
  background-color: white;
  font-size: inherit;
`;

const Table = styled.table`
  width: 100%;
`;

const TheadRow = styled.tr`
  line-height: 56px;
  border-bottom: 2px solid ${color.primary};
  color: ${color.primary};
  font-weight: bold;
`;

const TbodyRow = styled.tr`
  line-height: 56px;
  border-bottom: 1px solid lightgray;
`;

const TableHeader = styled.th`
  ${(props) => props.align === 'left' && 'text-align: left;'}
  padding: 0 10px;
  white-space: nowrap;
`;

const TableData = styled.td`
  ${(props) => props.align === 'left' && 'text-align: left;'}
  padding: 0 10px;
  white-space: nowrap;
`;

感想

jQueryとReactは思想こそ違うものの、コンポーネント設計をきちんと行うことが大切だと思いました。
またReactのrenderメソッド等リアクティブプログラミングでよしなにしてくれる有難さやstate管理する仕組みに改めてすごい!と感動しました。

styled-componentsの気に入ったところ

  • Sassの定数管理やmixinの概念をそのまま持ち越せる
  • マークアップとCSSを結び付けるのに、クラスが入らずスムーズに繋がること
  • デザイン用コンポーネントとロジック用コンポーネントが分かれることを強制するような仕組みになるので、HTMLやIDやクラスから推測する疲れがなくなる
  • コンポーネントの階層関係をフォルダに反映できて見通しが良くなる(これはsyled-componentsに限らずReactにおけるCSSなら全般的にそうなりやすいと思いますが)

styled-componentsの気を付けておきたい点

  • テンプレートリテラルを組み立てていることを意識すること(この点を考慮すると、やはりCSSを手軽に書くというよりはSass等を学んだ次のステップに相応しいのかなと思いました)
  • CRAでbabel-plugin-styled-componentsを使う場合は、'styled-components'でなく'styled-components/macro'をimportすること

styled-componentsのちょっとイマイチ?なところ

babel-plugin-styled-componentsによりクラス名にコンポーネント名が付き識別しやすくなりますが、同じファイルに子コンポーネントを書くか、別ファイルに子コンポーネントを書いてimportするかで挙動が変わります。

ヘッダー部分に外枠用のコンポーネントを付けるが、名前をHeaderWrapper等ではなく汎用的なWrapperという名前にする例を考えます。

(1) 同じファイルから読み込む例

Header/index.jsx
import React, { Component } from 'react';

export default class Header extends Component {
  render() {
    return (
      <Wrapper>Header!</Wrapper>
    );
  }
}


const Wrapper = styled.header`
  /* hogehoge */
`

この場合、Wrapperのクラス名にはHeader接頭辞が付きます。

(2) 違うファイルから読み込む例

Header/index.jsx
import React, { Component } from 'react';
import Wrapper from './Wrapper';

export default class Header extends Component {
  render() {
    return (
      <Wrapper>Header!</Wrapper>
    );
  }
}
Header/Wrapper.jsx
import styled from 'styled-components/macro';

const Wrapper = styled.footer`
  /* hogehoge */
`;

export default Wrapper;

この場合、Wrapperのクラス名にはHeader接頭辞が付きません。
(一応フォルダ分けした時点でハッシュ値で区別できるので実用上は問題ないのですが...)

贅沢を望むなら、常に(1)になって欲しいところです。

おわりに

最後にGitHubリポジトリのリンクを掲載しておきます。

jQuery版
https://github.com/yha-1228/ajax-jquery

React版
https://github.com/yha-1228/ajax-react

デザインセンス以外のマサカリは大歓迎です。

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

簡素なWebアプリをjQueryベースとReactベースで制作してみた

概要

JavaScriptとCSSの取り扱いの勉強用に、
同じWebアプリをjQueryベースをReactベースで兼ねて制作してみました。

Webアプリの機能

  • ヘッダー、フッター、メイン部分があります。
  • メイン部分には登録・削除機能付きのテーブルUIを配置しています。

比較したかったこと

  • Ajax処理
  • CSSによるレイアウト・ビジュアルの構成

jQuery版

制作手順

スタイリング

基本となるブロックをこのように決めました。
mock.png

横幅調整用のコンテナを決めた後に、HTMLとSassでこれを再現しました。

Ajax処理

jQuery Ajaxでクラスを作り、その中にメソッドを配置してテーブルUIを完成させました。
テーブルUIのブロック分けは以下のようになっています。

  • テーブルの新規登録フォーム
  • テーブル本体

React版

制作手順

jQuery版を素に、
- SassからStyled-components
- jQueryからReact
のように移植しました。

スタイリング

コンポーネントの階層に従いフォルダ分けしました。
リセットCSSやタグに設定するスタイル(SMACSSでいうとBase)はcreateGlobalStyleを使うスタイルとして纏め、その他のスタイルは共通用とそうでないもの用でフォルダ分けしました。

src以下のフォルダ構成は以下のようになりました。

.
├── App 基本的にコンポーネントはここに配置。大まかなブロック毎にフォルダを分ける
│   ├── Footer
│   │   ├── Copyright.js
│   │   └── index.js
│   ├── Header
│   │   └── index.js
│   ├── Main
│   │   └── index.js
│   └── TableApp
│       └── index.js
├── GlobalStyles 
│   ├── Base.js タイプセレクタへのCSS (SMACSSのBase相当)
│   ├── Config.js 色の定数
│   └── Reset.js リセットCSS
├── Shared 共通として切り出すコンポーネント
│   ├── Button.js
│   └── Container.js
└── index.js Reset, Baseを順に読み込んでから、App/.../index.jsのコンポーネントを適宜配置。

Ajax処理

Ajax自体にハマった点は特に無かったですが、
フォーム送信時に使う"制御されたコンポーネント"の概念が新鮮でした。

感想

jQueryとReactは思想こそ違うものの、コンポーネント設計をきちんと行うことが大切だと思いました。
またReactのrenderメソッド等リアクティブプログラミングでよしなにしてくれる有難さやstate管理する仕組みに改めてすごい!と感動しました。

styled-componentsの気に入ったところ

  • Sassの定数管理やmixinの概念をそのまま持ち越せる
  • マークアップとCSSを結び付けるのに、クラスが入らずスムーズに繋がること
  • デザイン用コンポーネントとロジック用コンポーネントが分かれることを強制するような仕組みになるので、HTMLやIDやクラスから推測する疲れがなくなる
  • コンポーネントの階層関係をフォルダに反映できて見通しが良くなる(これはsyled-componentsに限らずReactにおけるCSSなら全般的にそうなりやすいと思いますが)

styled-componentsの気を付けておきたい点

  • テンプレートリテラルを組み立てていることを意識すること(この点を考慮すると、やはりCSSを手軽に書くというよりはSass等を学んだ次のステップに相応しいのかなと思いました)
  • CRAでbabel-plugin-styled-componentsを使う場合は、'styled-components'でなく'styled-components/macro'をimportすること

styled-componentsのちょっとイマイチ?なところ

babel-plugin-styled-componentsによりハッシュ値にファイル名が付き識別しやすくなりますが、同じファイルに子コンポーネントを書くか、別ファイルに子コンポーネントを書いてimportするかで挙動が変わります。

ヘッダー部分に外枠用のコンポーネントを付けるが、名前をHeaderWrapper等ではなく汎用的なWrapperという名前にする例を考えます。

(1) 同じファイルから読み込む例

Header/index.js
import React, { Component } from 'react';

export default class Header extends Component {
  render() {
    return (
      <Wrapper>Header!</Wrapper>
    );
  }
}


const Wrapper = styled.header`
  /* hogehoge */
`

この場合、Wrapperのクラス名にはHeader接頭辞が付きます。

(2) 違うファイルから読み込む例

Header/index.js
import React, { Component } from 'react';
import Wrapper from './Wrapper';

export default class Header extends Component {
  render() {
    return (
      <Wrapper>Header!</Wrapper>
    );
  }
}
Header/Wrapper.js
import styled from 'styled-components/macro';

const Wrapper = styled.footer`
  /* hogehoge */
`;

export default Wrapper;

この場合、Wrapperのクラス名にはHeader接頭辞が付きません。
(一応フォルダ分けした時点でハッシュ値で区別できるので実用上は問題ないのですが...)

贅沢を望むなら、常に(1)になって欲しいところです。

おわりに

最後にGitHubリポジトリのリンクを掲載しておきます。

jQuery版
https://github.com/yha-1228/ajax-jquery

React版
https://github.com/yha-1228/ajax-react

デザインセンス以外のマサカリは大歓迎です。

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

同じWebアプリをjQueryとReactで作ったらどうなったか

概要

JavaScriptとCSSの取り扱いの勉強用に、
同じWebアプリをjQueryベースをReactベースで兼ねて制作してみました。

Webアプリの機能

  • ヘッダー、フッター、メイン部分があります。
  • メイン部分には登録・削除機能付きのテーブルUIを配置しています。

yha-1228.github.io_ajax-jquery_public_.png

なおデータはmockAPIを使わせて頂いております。

比較したかったこと

  • Ajax処理はどうなるか?
  • CSSによるレイアウト・ビジュアルの構成はどうなるか?

jQuery版

制作手順

スタイリング

基本となるブロックをこのように決めました。
mock.png

横幅調整用のコンテナを決めた後に、HTMLとSassでこれを再現しました。

Sassのフォルダはこのようになりました。

.
├── app 基本的にコンポーネントはここに配置
│   ├── _footer.scss
│   ├── _header.scss
│   ├── _main.scss
│   └── _table-app.scss
├── global
│   ├── _base.scss タイプセレクタへのCSS (SMACSSのBase相当)
│   ├── _configs.scss 色の定数
│   └── _reset.scss リセットCSS
├── shared 共通として切り出すコンポーネント
│   ├── _button.scss
│   └── _container.scss
└── index.scss ここで全て読み込む

ちなみにBEMを採用しています。

src/sass/app/_header.scss
.header {
  padding: 24px 0;
  background-color: $colorPrimary;

  &__logo {
    font-size: 32px;
    font-weight: bold;
    color: $colorBackground;
    &:hover {
      text-decoration: underline;
    }
  }
}

Ajax処理

jQuery Ajaxでクラスを作り、その中にメソッドを配置してテーブルUIを完成させました。
テーブルUIのブロック分けは以下のようになっています。

  • テーブルの新規登録フォーム
  • テーブル本体

ソースコードはこのようになりました。Reactを知った後だと画面がレンダリングする流れを手動で意識することがやや大変に感じます。

index.js
class TableApp {
  constructor($el) {
    this.update($el);
  }

  update($el) {
    this.$el = $el;
    this.$inputs = this.$el.find('input');
    this.$add = this.$el.find('.js-add');
    this.$tbody = this.$el.find('tbody');
    this.baseUrl = 'https://5e6736691937020016fed762.mockapi.io/users';
    this.getJson(this.baseUrl).then((result) => {
      this.users = result;
      this.render(this.users);
      this.$delete = this.$el.find('.js-delete');
      this.bindEvents();
    });
  }

  getJson(url) {
    return $.ajax({
      type: 'GET',
      url: url,
      dataType: 'json'
    });
  }

  postJson(url, data) {
    return $.ajax({
      type: 'POST',
      url: url,
      dataType: 'json',
      data: data
    });
  }

  deleteJson(url) {
    return $.ajax({
      type: 'DELETE',
      url: url,
      dataType: 'json'
    });
  }

  bindEvents() {
    this.handleAddClick = this.handleAddClick.bind(this);
    this.handleDeleteClick = this.handleDeleteClick.bind(this);
    this.$add.on('click', this.handleAddClick);
    this.$delete.on('click', this.handleDeleteClick);
  }

  unBindEvents() {
    this.$add.off('click');
    this.$delete.off('click');
  }

  handleAddClick(e) {
    e.preventDefault();

    const postData = {
      id: '',
      username: $('#username').val(),
      email: $('#email').val()
    };

    this.postJson(this.baseUrl, postData).then((result) => {
      const addedUser = result;
      alert(`Added ${addedUser.username}'s data.`);
      this.unBindEvents();
      this.update(this.$el);
    });
  }

  handleDeleteClick(e) {
    const clickdUserId = $(e.currentTarget).data('id');
    this.deleteJson(`${this.baseUrl}/${clickdUserId}`).then((result) => {
      const deletedUser = result;
      alert(`Deleted ${deletedUser.username}'s data.`);
      this.unBindEvents();
      this.update(this.$el);
    });
  }

  render(users) {
    this.$inputs.val('');

    this.$tbody.html(
      users.map(
        (user) => `
        <tr class="table-app__tbody-row js-row">
          <td class="table-app__table-data align-left">
            <button class="button js-delete" data-id=${user.id}>
              <i class="fas fa-trash-alt"></i>
            </button>
          </td> 
          <td class="table-app__table-data align-left">${user.username}</td>
          <td class="table-app__table-data align-left">${user.email}</td>
        </tr>`
      )
    );
  }
}

React版

制作手順

jQuery版を素に、
- SassからStyled-components
- jQueryからReact
のように移植しました。

スタイリング

コンポーネントの階層に従いフォルダ分けしました。
リセットCSSやタグに設定するスタイル(SMACSSでいうとBase)はcreateGlobalStyleを使うスタイルとして纏め、その他のスタイルは共通用とそうでないもの用でフォルダ分けしました。

src以下のフォルダ構成は以下のようになりました。
Sassバージョンとほぼ変わりありません。

.
├── App 基本的にコンポーネントはここに配置
│   ├── Footer
│   │   ├── Copyright.js
│   │   └── index.js
│   ├── Header
│   │   └── index.js
│   ├── Main
│   │   └── index.js
│   └── TableApp
│       └── index.js
├── GlobalStyles 
│   ├── Base.js タイプセレクタへのCSS (SMACSSのBase相当)
│   ├── Config.js 色の定数
│   └── Reset.js リセットCSS
├── Shared 共通として切り出すコンポーネント
│   ├── Button.js
│   └── Container.js
└── index.js ここで全て読み込む

Ajax処理

Ajax自体にハマった点は特に無かったですが、
フォーム送信時に使う"制御されたコンポーネント"の概念が新鮮でした。

ソースコードはこのようになりました。
テーブルUIは面倒で一纏めにしたので多少スクロール量が増えますが、デザイン用のコンポーネントをロジック用の親コンポーネントが操作する、という流れが見やすくて快適です。

src/App/TableApp/index.jsx
import React, { Component } from 'react';
import styled from 'styled-components/macro';
import { color } from '../../GlobalStyles/Config';
import Button from '../../Shared/Button';

export default class TableApp extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: '',
      email: '',
      error: null,
      loaded: false,
      users: []
    };
    this.handleUsernameChange = this.handleUsernameChange.bind(this);
    this.handleEmailChange = this.handleEmailChange.bind(this);
    this.baseUrl = 'https://5e6736691937020016fed762.mockapi.io/users';
    this.handleAddClick = this.handleAddClick.bind(this);
    this.handleDeleteClick = this.handleDeleteClick.bind(this);
  }

  getJson(url) {
    return fetch(url).then((res) => res.json());
  }

  postJson(url, data) {
    return fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    }).then((res) => res.json());
  }

  deleteJson(url) {
    return fetch(url, { method: 'DELETE' }).then((res) => res.json());
  }

  handleUsernameChange(e) {
    this.setState({ username: e.target.value });
  }

  handleEmailChange(e) {
    this.setState({ email: e.target.value });
  }

  handleAddClick(e) {
    e.preventDefault();

    const data = {
      id: '',
      username: this.state.username,
      email: this.state.email
    };

    this.postJson(this.baseUrl, data).then((result) => {
      const addedUser = result;
      alert(`Added ${addedUser.username}'s data.`);
      this.componentDidMount();
      this.setState({ username: '', email: '' });
    });
  }

  handleDeleteClick(e) {
    const clickedId = e.currentTarget.dataset.id;

    this.deleteJson(`${this.baseUrl}/${clickedId}`).then((result) => {
      const deletedUser = result;
      alert(`Deleted ${deletedUser.username}'s data.`);
      this.componentDidMount();
    });
  }

  componentDidMount() {
    this.getJson(this.baseUrl).then(
      (result) => {
        this.setState({
          error: null,
          loaded: true,
          users: result
        });
      },
      (error) => {
        this.setState({
          error: error,
          loaded: true,
          users: []
        });
      }
    );
  }

  render() {
    const users = this.state.users;

    return (
      <>
        <TableBar>
          <form method="post">
            <InputsWrapper>
              <Label htmlFor="username">username</Label>
              <InputText
                type="text"
                value={this.state.username}
                onChange={this.handleUsernameChange}
                name="username"
                id="username"
              />
              <Label htmlFor="email">email</Label>
              <InputText
                type="text"
                value={this.state.email}
                onChange={this.handleEmailChange}
                name="email"
                id="email"
              />
            </InputsWrapper>
            <Button wide primary onClick={this.handleAddClick}>
              <i className="fas fa-plus"></i>&nbsp;&nbsp;Add
            </Button>
          </form>
        </TableBar>

        <Table>
          <thead>
            <TheadRow>
              <TableHeader align="left">Delete</TableHeader>
              <TableHeader align="left">username</TableHeader>
              <TableHeader align="left">email</TableHeader>
            </TheadRow>
          </thead>
          <tbody>
            {users.map((user) => (
              <TbodyRow key={user.id}>
                <TableData align="left">
                  <Button onClick={this.handleDeleteClick} data-id={user.id}>
                    <i className="fas fa-trash-alt"></i>
                  </Button>
                </TableData>
                <TableData align="left">{user.username}</TableData>
                <TableData align="left">{user.email}</TableData>
              </TbodyRow>
            ))}
          </tbody>
        </Table>
      </>
    );
  }
}

const TableBar = styled.div`
  margin-bottom: 32px;
`;

const InputsWrapper = styled.div`
  margin-bottom: 16px;
`;

const Label = styled.label`
  display: block;
  margin-bottom: 16px;
  font-size: inherit;
  color: ${color.gray01};
`;

const InputText = styled.input`
  width: 100%;
  line-height: 24px;
  margin-bottom: 16px;
  background-color: white;
  font-size: inherit;
`;

const Table = styled.table`
  width: 100%;
`;

const TheadRow = styled.tr`
  line-height: 56px;
  border-bottom: 2px solid ${color.primary};
  color: ${color.primary};
  font-weight: bold;
`;

const TbodyRow = styled.tr`
  line-height: 56px;
  border-bottom: 1px solid lightgray;
`;

const TableHeader = styled.th`
  ${(props) => props.align === 'left' && 'text-align: left;'}
  padding: 0 10px;
  white-space: nowrap;
`;

const TableData = styled.td`
  ${(props) => props.align === 'left' && 'text-align: left;'}
  padding: 0 10px;
  white-space: nowrap;
`;

感想

jQueryとReactは思想こそ違うものの、コンポーネント設計をきちんと行うことが大切だと思いました。
またReactのrenderメソッド等リアクティブプログラミングでよしなにしてくれる有難さやstate管理する仕組みに改めてすごい!と感動しました。

styled-componentsの気に入ったところ

  • Sassの定数管理やmixinの概念をそのまま持ち越せる
  • マークアップとCSSを結び付けるのに、クラスが入らずスムーズに繋がること
  • デザイン用コンポーネントとロジック用コンポーネントが分かれることを強制するような仕組みになるので、HTMLやIDやクラスから推測する疲れがなくなる
  • コンポーネントの階層関係をフォルダに反映できて見通しが良くなる(これはsyled-componentsに限らずReactにおけるCSSなら全般的にそうなりやすいと思いますが)

styled-componentsの気を付けておきたい点

  • テンプレートリテラルを組み立てていることを意識すること(この点を考慮すると、やはりCSSを手軽に書くというよりはSass等を学んだ次のステップに相応しいのかなと思いました)
  • CRAでbabel-plugin-styled-componentsを使う場合は、'styled-components'でなく'styled-components/macro'をimportすること

styled-componentsのちょっとイマイチ?なところ

クラス名の挙動が読み込み方法によって変わる

babel-plugin-styled-componentsによりクラス名にコンポーネント名が付き識別しやすくなりますが、同じファイルに子コンポーネントを書くか、別ファイルに子コンポーネントを書いてimportするかで挙動が変わります。

ヘッダー部分に外枠用のコンポーネントを付けるが、名前をHeaderWrapper等ではなく汎用的なWrapperという名前にする例を考えます。

(1) 同じファイルから読み込む例

Header/index.jsx
import React, { Component } from 'react';

export default class Header extends Component {
  render() {
    return (
      <Wrapper>Header!</Wrapper>
    );
  }
}


const Wrapper = styled.header`
  /* hogehoge */
`

この場合、Wrapperのクラス名にはHeader接頭辞が付きます。

(2) 違うファイルから読み込む例

Header/index.jsx
import React, { Component } from 'react';
import Wrapper from './Wrapper';

export default class Header extends Component {
  render() {
    return (
      <Wrapper>Header!</Wrapper>
    );
  }
}
Header/Wrapper.jsx
import styled from 'styled-components/macro';

const Wrapper = styled.footer`
  /* hogehoge */
`;

export default Wrapper;

この場合、Wrapperのクラス名にはHeader接頭辞が付きません。
(一応フォルダ分けした時点でハッシュ値で区別できるので実用上は問題ないのですが...)

贅沢を望むなら、常に(1)になって欲しいところです。

タイポに気付けない場合がある

色などの定数を呼び出す際にタイプミスしてもエラーになりません。
ここはちょっと詰まりました。

実際にあった例

src/GlobalStyles/Config.js
export const color = {
  foreground: 'rgb(30, 30, 30)',
  gray01: 'rgb(100, 100, 100)',
  background: 'rgb(252, 252, 252)',
  primary: '#3e5ce0'
};
Hoge.js
import { color } from '../../GlobalStyles/Config.js';

// 間違い
const Hoge = styled.div`
  color: ${color.foreGround};
`

// 正しい
const Hoge = styled.div`
  color: ${color.foreground};
`

export default Hoge;

存在しない変数名を呼び出しているのになぜか無反応です。

おわりに

最後にGitHubリポジトリのリンクを掲載しておきます。

jQuery版
https://github.com/yha-1228/ajax-jquery

React版
https://github.com/yha-1228/ajax-react

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

React開発環境を作る1

reactを勉強するときに開発環境を作るところでいろいろ躓いたのでまとめていきます。

使用ツール

  • webpack
  • babel
  • css-loader
  • style-loader

ツールインストール

npm install

まずはこれを叩いてpackage.jsonを作る。

npm install --save react react-dom

reactとreact-domのインストール

npm install --save-dev webpack webpack-dev-server
npm install --save-dev babel-cli babel-loader babel-preset-env babel-preset-react
npm install --save-dev eslint eslint-loader eslint-plugin-react 
npm install --save-dev css-loader style-loader babel-loader

開発をサポートするツールのインストール
開発で依存するパッケージを使うときは--saveで、開発をサポートするパッケージを使うときは--save-devを使うらしい。

package.json

package.json
{
  "name": "react_",
  "version": "1.0.0",
  "description": "react_",
  "private": true,
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack-dev-server",
    "webpack": "webpack -d"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^16.13.0",
    "react-dom": "^16.13.0"
  },
  "devDependencies": {
    "@babel/preset-env": "^7.8.7",
    "@babel/preset-react": "^7.8.3",
    "@babel/register": "^7.8.6",
    "babel-cli": "^6.26.0",
    "babel-loader": "^8.0.6",
    "babel-preset-env": "^1.7.0",
    "babel-preset-react": "^6.24.1",
    "css-loader": "^3.4.2",
    "eslint": "^6.8.0",
    "eslint-loader": "^3.0.3",
    "eslint-plugin-react": "^7.19.0",
    "style-loader": "^1.1.3",
    "webpack": "^4.42.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.10.3"
  }
}

できたpackage.jsonにいろいろ付け足したのがこれ
scriptsはnpm startnpm run buildコマンドを叩いたときにwebpack-dev-serverを起動してくれっていうことを書いている。
npm run webpackを叩いたときはwebpack -dが起動する。
--saveでインストールしたものはdependenciesに--save-devでインストールしたものはdevDependenciesに書かれている

とりあえず、今日はここまで、
次はそれぞれのツールの解説をしていきます。

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