- 投稿日:2020-03-17T23:10:19+09:00
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
を使用しています。
今は、getStaticProps
とgetServerSideProps
に別れ、これらを使うことが推奨されています。
少しの変更で切り替えれるので、簡単にできました。参考
- 投稿日:2020-03-17T20:24:47+09:00
[react-native]AsyncStorageの使い方
AsyncStorageとは?
AsyncStorageはreactnativeにおいてデータを永続的に保存を可能とするライブラリの一つです。
reactnative公式では非推奨ですが、expoではまだ推奨しているので勉強してみました。
DBに保存したりいろんな方法でデータの永続化は可能ですが、今回はスマホのストレージに保存するやり方です。基本的な使い方
・AsyncStorage.setItem(key, value);
・AsyncStorage.getItem(key);
setItemでキーを指定して保存
getItemでキーを指定して取得
といった流れです。実際に試してみます。App.jsimport 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, } });配列での使い方
App.jsimport 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を使うことで要素の値も簡単に取り出せます!まとめ
大規模なシステム向けではないと思いますが、
簡単なアプリであれば簡単に組むことができます。
知っておくといいかも?
- 投稿日:2020-03-17T15:34:28+09:00
#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して動作確認する
Original by Github issue
- 投稿日:2020-03-17T15:21:08+09:00
#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;エラー
解決
タグで囲めば良いらしい。
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
Original by Github issue
- 投稿日:2020-03-17T14:45:50+09:00
ログインなしでコードを共有できるサイトつくた
成果物
リポジトリ
https://github.com/yuzuru2/code_site
開発環境
- ubuntu 18.04
- docker
- docker-compose
使用ライブラリ周り
フロントエンド
- parcel
- bootstrap
- highlight.js
- react
- nginx(静的ファイルを配信)
バックエンド
- typescript
- nodejs
- pm2
データベース
- mongodb
成果物を使うケース
- ちょろっと書いたコードを誰かに見せたい時
UI
ホーム画面
コード閲覧画面
- 投稿日:2020-03-17T14:45:50+09:00
コード共有サイト作成 docker react node.js mongodb
成果物
リポジトリ
https://github.com/yuzuru2/code_site
開発環境
- ubuntu 18.04
- docker
- docker-compose
使用ライブラリ周り
フロントエンド
- parcel
- bootstrap
- highlight.js
- react
- nginx(静的ファイルを配信)
バックエンド
- typescript
- nodejs
- pm2
データベース
- mongodb
成果物を使うケース
- ちょろっと書いたコードを誰かに見せたい時
UI
ホーム画面
コード閲覧画面
- 投稿日:2020-03-17T14:45:50+09:00
コード共有サイトつくた
成果物
リポジトリ
https://github.com/yuzuru2/code_site
開発環境
- ubuntu 18.04
- docker
- docker-compose
使用ライブラリ周り
フロントエンド
- parcel
- bootstrap
- highlight.js
- react
- nginx(静的ファイルを配信)
バックエンド
- typescript
- nodejs
- pm2
データベース
- mongodb
成果物を使うケース
- ちょろっと書いたコードを誰かに見せたい時
UI
ホーム画面
コード閲覧画面
- 投稿日:2020-03-17T12:51:14+09:00
同じWebアプリをjQueryとReactで作った感想
概要
JavaScriptとCSSの取り扱いの勉強用に、
同じWebアプリをjQueryベースをReactベースで兼ねて制作してみました。Webアプリの機能
- ヘッダー、フッター、メイン部分があります。
- メイン部分には登録・削除機能付きのテーブルUIを配置しています。
なおデータはmockAPIを使わせて頂いております。
比較したかったこと
- Ajax処理はどうなるか?
- CSSによるレイアウト・ビジュアルの構成はどうなるか?
jQuery版
制作手順
スタイリング
横幅調整用のコンテナを決めた後に、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.jsclass 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.jsximport 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> 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.jsximport 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.jsximport React, { Component } from 'react'; import Wrapper from './Wrapper'; export default class Header extends Component { render() { return ( <Wrapper>Header!</Wrapper> ); } }Header/Wrapper.jsximport styled from 'styled-components/macro'; const Wrapper = styled.footer` /* hogehoge */ `; export default Wrapper;この場合、Wrapperのクラス名にはHeader接頭辞が付きません。
(一応フォルダ分けした時点でハッシュ値で区別できるので実用上は問題ないのですが...)贅沢を望むなら、常に(1)になって欲しいところです。
おわりに
最後にGitHubリポジトリのリンクを掲載しておきます。
jQuery版
https://github.com/yha-1228/ajax-jqueryReact版
https://github.com/yha-1228/ajax-reactデザインセンス以外のマサカリは大歓迎です。
- 投稿日:2020-03-17T12:51:14+09:00
簡素なWebアプリをjQueryベースとReactベースで制作してみた
概要
JavaScriptとCSSの取り扱いの勉強用に、
同じWebアプリをjQueryベースをReactベースで兼ねて制作してみました。Webアプリの機能
- ヘッダー、フッター、メイン部分があります。
- メイン部分には登録・削除機能付きのテーブルUIを配置しています。
比較したかったこと
- Ajax処理
- CSSによるレイアウト・ビジュアルの構成
jQuery版
制作手順
スタイリング
横幅調整用のコンテナを決めた後に、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.jsimport 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.jsimport React, { Component } from 'react'; import Wrapper from './Wrapper'; export default class Header extends Component { render() { return ( <Wrapper>Header!</Wrapper> ); } }Header/Wrapper.jsimport styled from 'styled-components/macro'; const Wrapper = styled.footer` /* hogehoge */ `; export default Wrapper;この場合、Wrapperのクラス名にはHeader接頭辞が付きません。
(一応フォルダ分けした時点でハッシュ値で区別できるので実用上は問題ないのですが...)贅沢を望むなら、常に(1)になって欲しいところです。
おわりに
最後にGitHubリポジトリのリンクを掲載しておきます。
jQuery版
https://github.com/yha-1228/ajax-jqueryReact版
https://github.com/yha-1228/ajax-reactデザインセンス以外のマサカリは大歓迎です。
- 投稿日:2020-03-17T12:51:14+09:00
同じWebアプリをjQueryとReactで作ったらどうなったか
概要
JavaScriptとCSSの取り扱いの勉強用に、
同じWebアプリをjQueryベースをReactベースで兼ねて制作してみました。Webアプリの機能
- ヘッダー、フッター、メイン部分があります。
- メイン部分には登録・削除機能付きのテーブルUIを配置しています。
なおデータはmockAPIを使わせて頂いております。
比較したかったこと
- Ajax処理はどうなるか?
- CSSによるレイアウト・ビジュアルの構成はどうなるか?
jQuery版
制作手順
スタイリング
横幅調整用のコンテナを決めた後に、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.jsclass 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.jsximport 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> 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.jsximport 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.jsximport React, { Component } from 'react'; import Wrapper from './Wrapper'; export default class Header extends Component { render() { return ( <Wrapper>Header!</Wrapper> ); } }Header/Wrapper.jsximport styled from 'styled-components/macro'; const Wrapper = styled.footer` /* hogehoge */ `; export default Wrapper;この場合、Wrapperのクラス名にはHeader接頭辞が付きません。
(一応フォルダ分けした時点でハッシュ値で区別できるので実用上は問題ないのですが...)贅沢を望むなら、常に(1)になって欲しいところです。
タイポに気付けない場合がある
色などの定数を呼び出す際にタイプミスしてもエラーになりません。
ここはちょっと詰まりました。実際にあった例
src/GlobalStyles/Config.jsexport const color = { foreground: 'rgb(30, 30, 30)', gray01: 'rgb(100, 100, 100)', background: 'rgb(252, 252, 252)', primary: '#3e5ce0' };Hoge.jsimport { color } from '../../GlobalStyles/Config.js'; // 間違い const Hoge = styled.div` color: ${color.foreGround}; ` // 正しい const Hoge = styled.div` color: ${color.foreground}; ` export default Hoge;存在しない変数名を呼び出しているのになぜか無反応です。
おわりに
最後にGitHubリポジトリのリンクを掲載しておきます。
- 投稿日:2020-03-17T00:34:20+09:00
React開発環境を作る1
reactを勉強するときに開発環境を作るところでいろいろ躓いたのでまとめていきます。
使用ツール
- webpack
- babel
- css-loader
- style-loader
ツールインストール
npm installまずはこれを叩いてpackage.jsonを作る。
npm install --save react react-domreactと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 start
とnpm run build
コマンドを叩いたときにwebpack-dev-serverを起動してくれっていうことを書いている。
npm run webpack
を叩いたときはwebpack -dが起動する。
--saveでインストールしたものはdependenciesに--save-devでインストールしたものはdevDependenciesに書かれているとりあえず、今日はここまで、
次はそれぞれのツールの解説をしていきます。