- 投稿日:2020-08-17T23:12:24+09:00
Next.jsのルーティング使ったみた
はじめに
next.jsのLinkコンポーネントを使い、動的なルーティング(ページ外遷移)や静的なルーティング(ページ内遷移)を実装する機会があったので復習としてこの記事を残します。今回はNext.js 9から追加されたDynamic Routingを簡易的に使ってみました。
完成形
実装
遷移させたい要素にidを指定
//遷移先 import { RoutingTest } from '~/components/molecules/routingtest'; <RoutingTest /> <div className={'py-64'}> <img src={ 'https://1.bp.blogspot.com/-NQ4JWCsibbg/U400_zN20KI/AAAAAAAAg70/4N7ulVzxrLE/s800/family_kyoudai.png' } /> <img src={ 'https://1.bp.blogspot.com/-NQ4JWCsibbg/U400_zN20KI/AAAAAAAAg70/4N7ulVzxrLE/s800/family_kyoudai.png' } /> </div> <div id={'RoutingTEst'} className={'py-64 text-5xl'}> 遷移成功 </div>Next.jsはLinkというコンポーネントを用意してくれています。シンプルなルーティングでは、以下のようにhrefパラメータに遷移先のidを渡せば遷移できます
//遷移元 import { Link } from '~/components/ions/link'; export const RoutingTest: React.FC = () => ( <div> <Link href={'#RoutingTest'} > <div className={'text-5xl'}>ここをクリック</div> </Link> </div> );まとめ
他にもNext.jsのルーティング方法はたくさんあります
今回はNext.js 9から実装されたDynamic Routingを簡易的に使ってみました詳しくはこちらをチェックしてみてください↓
https://nextjs.org/docs/routing/dynamic-routes
- 投稿日:2020-08-17T22:40:23+09:00
React FormikでFormの各部品のcomponentを制作
先日、「React Formikの入門編」の記事を投稿しました。
その記事をもとにformの各部品(input、textarea、select)などをcomponentとして分けたので共有します。
※実際にアプリケーションを開発する際はこのようにcomponent分けをするのではないでしょうか。FormContainer
- form画面の親コンポーネント
initialValues
、validationSchema
、onSubmit
を定義- 呼び出し先に送るpropsを設定
FormContainer.jsimport { Form, Formik } from 'formik'; import { checkboxOptions, dropdownOptions, radioOptions, } from '../constants/formOptions'; import FormControl from './FormControl'; import React from 'react'; import SubmitButton from './SubmitButton'; import { initialValues } from '../validation/initialValues'; import { validationSchema } from '../validation/validationSchema'; function FormContainer() { const onSubmit = (values) => { console.log('form data', values); console.log('Saved data', JSON.parse(JSON.stringify(values))); }; return ( <Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={onSubmit} > {(formik) => ( <Form> <FormControl control="input" name="email" label="Email" type="email" /> <FormControl control="textarea" name="description" label="Description" /> <FormControl control="select" label="Select a topic" name="selectOption" options={dropdownOptions} /> <FormControl control="radio" label="Radio topic" name="radioOption" options={radioOptions} /> <FormControl control="checkbox" label="Checkbox topics" name="checkboxOption" options={checkboxOptions} /> <FormControl control="fieldArrayInput" label="multi topic" name="fieldArrayInput" /> <FormControl control="fileInput" label="file upload" name="fileInput" /> <FormControl control="date" label="Pick a date" name="birthDate" /> <SubmitButton formik={formik} /> </Form> )} </Formik> ); } export default FormContainer;FormControl
- formの部品によって各コンポーネントに振り分ける
FormControl.jsimport CheckboxGroup from './CheckboxGroup'; import DatePicker from './DatePicker'; import FieldArrayInput from './FieldArrayInput'; import FileInput from './FileInput'; import Input from './Input'; import RadioButtons from './RadioButtons'; import React from 'react'; import Select from './Select'; import Textarea from './Textarea'; function FormikControl(props) { const { control, ...rest } = props; switch (control) { case 'input': return <Input {...rest} />; case 'textarea': return <Textarea {...rest} />; case 'select': return <Select {...rest} />; case 'radio': return <RadioButtons {...rest} />; case 'checkbox': return <CheckboxGroup {...rest} />; case 'fieldArrayInput': return <FieldArrayInput {...rest} />; case 'fileInput': return <FileInput {...rest} />; case 'date': return <DatePicker {...rest} />; default: return null; } } export default FormControl;Input
Input.jsimport { ErrorMessage, Field } from 'formik'; import React from 'react'; function Input(props) { const { label, name, ...rest } = props; return ( <div className="form-control"> <label htmlFor={name}>{label}</label> <Field id={name} name={name} {...rest} /> <ErrorMessage name={name} /> </div> ); } export default Input;Textarea
Filed
コンポーネントのpropsas
で「textarea」を指定Textarea.jsimport { ErrorMessage, Field } from 'formik'; import React from 'react'; function Textarea(props) { const { label, name, ...rest } = props; return ( <div className="form-control"> <label htmlFor={name}>{label}</label> <Field as="textarea" id={name} name={name} {...rest} /> <ErrorMessage name={name} /> </div> ); } export default Textarea;Select
Filed
コンポーネントのpropsas
で「select」を指定Select.jsimport { ErrorMessage, Field } from 'formik'; import React from 'react'; function Select(props) { const { label, name, options, ...rest } = props; return ( <div className="form-control"> <label htmlFor={name}>{label}</label> <Field as="select" id={name} name={name} {...rest}> {options.map((option) => ( <option key={option.value} value={option.value}> {option.key} </option> ))} </Field> <ErrorMessage name={name} /> </div> ); } export default Select;Radio Button
RadioButtons.jsimport { ErrorMessage, Field } from 'formik'; import React from 'react'; function RadioButtons(props) { const { label, name, options, ...rest } = props; return ( <div className="form-control"> <label htmlFor={name}>{label}</label> <Field id={name} name={name} {...rest}> {({ field }) => { return options.map((option) => ( <React.Fragment key={option.key}> <input id={option.value} type="radio" {...field} value={option.value} checked={field.value === option.value} /> <label htmlFor={option.value}>{option.key}</label> </React.Fragment> )); }} </Field> <ErrorMessage name={name} /> </div> ); } export default RadioButtons;Checkbox
CheckboxGroup.jsimport { ErrorMessage, Field } from 'formik'; import React from 'react'; function CheckboxGroup(props) { const { label, name, options, ...rest } = props; return ( <div className="form-control"> <label htmlFor={name}>{label}</label> <Field id={name} name={name} {...rest}> {({ field }) => { return options.map((option) => ( <React.Fragment key={option.key}> <input id={option.value} type="checkbox" {...field} value={option.value} checked={field.value.includes(option.value)} /> <label htmlFor={option.value}>{option.key}</label> </React.Fragment> )); }} </Field> <ErrorMessage name={name} /> </div> ); } export default CheckboxGroup;FieldArray
- 「+」「-」ボタンを押下すると動的に
input
が増減するコンポーネント- 1つ目の
input
で「-」ボタンを押下できない- 1つ目の
input
に値が入ってない場合はバリデーションエラーとなるFieldArrayInput.jsimport { ErrorMessage, Field, FieldArray } from 'formik'; import React from 'react'; function FieldArrayInput(props) { const { label, name } = props; const validateArrayInput = (value) => { let error; if (!value) { error = 'FieldArrayInput is Required'; } return error; }; return ( <div className="form-control"> <label htmlFor={name}>{label}</label> <FieldArray name={name}> {(fieldArrayProps) => { const { push, remove, form } = fieldArrayProps; const { values } = form; return ( <div> {values[name].map((value, index) => ( <div key={index}> {/* 1つ目のInputは入力必須にする */} {index === 0 ? ( <Field name={`${name}[${index}]`} validate={validateArrayInput} /> ) : ( <> <Field name={`${name}[${index}]`} /> <button type="button" onClick={() => remove(index)}> - </button> </> )} </div> ))} <button type="button" onClick={() => push('')}> + </button> </div> ); }} </FieldArray> <ErrorMessage name={name} /> </div> ); } export default FieldArrayInput;File
- validationチェック(拡張子、ファイルサイズ)は未着手
- サムネイル画像表示は未着手
FileInput.jsimport { ErrorMessage, Field } from 'formik'; import React from 'react'; function FileInput(props) { const { label, name, ...rest } = props; return ( <div className="form-control"> <label htmlFor={name}>{name}</label> <Field name={name} {...rest}> {({ form }) => { const { setFieldValue } = form; return ( <input id={name} name={name} type="file" onChange={(event) => { setFieldValue(name, event.currentTarget.files[0]); }} /> ); }} </Field> <ErrorMessage name={name} /> </div> ); } export default FileInput;DatePicker
react-datepicker
を使用DatePicler.jsimport 'react-datepicker/dist/react-datepicker.css'; import { ErrorMessage, Field } from 'formik'; import DateView from 'react-datepicker'; import React from 'react'; function DatePicker(props) { const { label, name, ...rest } = props; return ( <div className="form-control"> <label htmlFor={name}>{label}</label> <Field name={name}> {({ field, form }) => { const { setFieldValue } = form; const { value } = field; return ( <DateView id={name} {...field} {...rest} selected={value} onChange={(val) => setFieldValue(name, val)} /> ); }} </Field> <ErrorMessage name={name} /> </div> ); } export default DatePicker;Submit
Submit.jsimport React from 'react'; function SubmitButton(props) { const { formik } = props; return ( <button type="submit" disabled={!formik.isValid}> Submit </button> ); } export default SubmitButton;以上です。
修正した方が良い箇所がありましたら教えていただけますと幸いです。
- 投稿日:2020-08-17T22:05:55+09:00
【備忘録】日本一わかりやすいReact-Redux講座 実践編 #13 「注文履歴を確認しよう〜コンポーネントの再利用〜」
はじめに
概要
この記事は、トラハック氏(@torahack_)が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。
前回の講座で、トランザクション処理を含めた購入処理を実装しました。
今回は、これまでに作成してきたコンポーネントを再利用しながら、注文履歴画面を作成します。
※ 前回記事: 【備忘録】日本一わかりやすいReact-Redux講座 実践編 #12後半 「トランザクションを使って商品を注文しよう(後半)」
動画URL
注文履歴を確認しよう〜コンポーネントの再利用〜【日本一わかりやすいReact-Redux講座 実践編#13】
要点
- React コンポーネントを再利用することで、新規コンポーネントの作成が効率化される。
完成系イメージ
http://localhost:3000/order/confirm
カート内の商品を入れた状態で、「注文を確定する」をクリックすると、
http://localhost:3000/order/completes
ここは未定義です。Drawerメニューの「商品履歴」をクリックすると、
カートに入れた商品が、購入履歴として表示されています。
再度、商品を購入すると、別の注文IDとして購入履歴が追加されていきます。
メイン
実装概要
今回は、購入履歴を確認する画面を作ります。これまでの講座で学習してきたことの復習になります。
前回の講座で、DBの
users
コレクションのサブコレクションとして、orders
サブコレクションを定義しました。Redux Storeについても、同様のデータ構造で購入履歴を設計します。具体的には、
initialState.js
内で、users
の配列要素としてorders
配列を定義orders
を Store から取得して React で使用するための operations, actions, reducers., selectors を定義を実装します。
また、購入履歴用のテンプレートを新たに作ります。
- templates:
OrderHistory.jsx
- route:
/order/history
とします。
reducksファイル実装
実装ファイル(reducks)1.src/reducks/store/initialState.js 2.src/reducks/users/operations.js 3.src/reducks/users/actions.js 4.src/reducks/users/reducers.js 5.src/reducks/users/selectors.js1.src/reducks/store/initialState.jsconst initialState = { products: { list: [] }, users: { cart: [], isSignedIn: false, orders: [], //追記 role: "", uid: "", username: "" } }; export default initialState
users
の配列要素として、orders
配列を定義します。2.src/reducks/users/operations.jsimport { fetchOrdersHistoryAction, fetchProductsInCartAction,signInAction,signOutAction } from "./actions"; //追記 import { push } from "connected-react-router"; import {auth, db, FirebaseTimestamp} from "../../firebase/index" . . . //追記 export const fetchOrdersHistory = () => { return async (dispatch, getState) => { const uid = getState().users.uid; const list = [] db.collection("users").doc(uid).collection('orders') .orderBy('updated_at', "desc") .get() .then(snapshots => { snapshots.forEach(snapshot => { const data = snapshot.data(); list.push(data) }); dispatch(fetchOrdersHistoryAction(list)) }) } } //追記ここまで . . .DB上の
orders
を取得し、オブジェクト配列としてアクションに渡すfetchOrdersHistory()
を定義します。3.src/reducks/users/actions.jsexport const FETCH_ORDERS_HISTORY = "FETCH_ORDERS_HISTORY"; export const fetchOrdersHistoryAction = (orders) => { return { type: "FETCH_ORDERS_HISTORY", payload: orders } } . . .4.src/reducks/users/reducers.jsimport * as Actions from './actions' import initialState from '../store/initialState' export const UsersReducer = (state = initialState.users, action) => { switch (action.type) { //追記 case Actions.FETCH_ORDERS_HISTORY: return { ...state, orders: [...action.payload] }; //追記ここまで case Actions.FETCH_PRODUCTS_IN_CART: return { ...state, cart: [...action.payload] }; case Actions.SIGN_IN: return { ...state, ...action.payload }; case Actions.SIGN_OUT: return { ...action.payload }; default: return state } }5.src/reducks/users/selectors.jsimport { createSelector } from "reselect"; const usersSelector = (state) => state.users; . . . export const getOrdersHistory = createSelector( [usersSelector], state => state.orders ) . . .actions, reducers,selectorsを定義します。
getOrdersHistory()
から、Store内のorders
を参照できるようになりました。コンポーネントファイル実装
コンポーネント構想は以下のイメージ。
ここに書いてあるコンポーネントだけでなく、過去の講座で作成したコンポーネントをどんどん再利用していきます。
実装ファイル(コンポーネント)1.src/templates/OrderHistory.jsx 2.src/templates/index.js 3.src/components/Products/OrderHistoryItem.jsx 4.src/components/Products/OrderedProducts.jsx 5.src/components/Products/index.js 6.src/Router.jsx1.src/templates/OrderHistory.jsximport React, {useEffect} from 'react'; import {useDispatch, useSelector} from "react-redux"; import List from "@material-ui/core/List"; import {getOrdersHistory} from "../reducks/users/selectors"; import {OrderHistoryItem} from "../components/Products"; import {fetchOrdersHistory} from "../reducks/users/operations"; import {makeStyles} from "@material-ui/styles"; const useStyles = makeStyles((theme) => ({ orderList: { background: theme.palette.grey["100"], margin: '0 auto', padding: 32, [theme.breakpoints.down('sm')]: { width: '100%' }, [theme.breakpoints.up('md')]: { width: 768 } }, })) const OrderHistory = () => { const classes = useStyles() const dispatch = useDispatch() const selector = useSelector(state => state) const orders = getOrdersHistory(selector); useEffect(() => { dispatch(fetchOrdersHistory()) },[]) return ( <section className="c-section-wrapin"> <List className={classes.orderList}> {orders.length > 0 && ( orders.map(order => <OrderHistoryItem order={order} key={order.id} />) )} </List> </section> ); }; export default OrderHistory;2.src/templates/index.jsexport {default as CartList} from './CartList' export {default as Home} from './Home' export {default as OrderConfirm} from './OrderConfirm' export {default as OrderHistory} from './OrderHistory' //追記 export {default as ProductDetail} from './ProductDetail' export {default as ProductEdit} from './ProductEdit' export {default as ProductList} from './ProductList' export {default as Reset} from './Reset' export {default as SignIn} from './SignIn' export {default as SignUp} from './SignUp'
useEffect()
内でfetchOrdersHistory()
を実行し、DB上のorders
サブコレクションを取得してStoreに保存します。const orders = getOrdersHistory(selector);
で、Storeに保存したorders
をstateとして取得します。orders
をmap
を用いてイテレートし、<OrderHistoryItem>
へ展開します。3.src/components/Products/OrderHistoryItem.jsximport React from 'react'; import Divider from "@material-ui/core/Divider"; import {TextDetail} from "../UIkit"; import {OrderedProducts} from "./index"; const datetimeToString = (date) => { return date.getFullYear() + "-" + ("00" + (date.getMonth()+1)).slice(-2) + "-" + ("00" + date.getDate()).slice(-2) + " " + ("00" + date.getHours()).slice(-2) + ":" + ("00" + date.getMinutes()).slice(-2) + ":" + ("00" + date.getSeconds()).slice(-2) } const dateToString = (date) => { return date.getFullYear() + "-" + ("00" + (date.getMonth()+1)).slice(-2) + "-" + ("00" + date.getDate()).slice(-2) } const OrderHistoryItem = (props) => { const order = props.order; const orderedDatetime = datetimeToString(props.order.updated_at.toDate()) const price = "¥" + order.amount.toLocaleString() const shippingDate = dateToString(props.order.shipping_date.toDate()) const products = props.order.products return ( <div> <div className="module-spacer--small" /> <TextDetail label={"注文ID"} value={order.id} /> <TextDetail label={"注文日時"} value={orderedDatetime} /> <TextDetail label={"発送予定日"} value={shippingDate} /> <TextDetail label={"注文金額"} value={price}/> {products.length > 0 && ( <OrderedProducts products={products} /> )} <div className="module-spacer--extra-extra-small" /> <Divider /> </div> ); }; export default OrderHistoryItem;
<TextDetail>
コンポーネントを再利用しています。props.order
が持っているproducts
を<OrderedProducts>
に渡します。4.src/components/Products/OrderedProducts.jsximport React, {useCallback} from 'react'; import List from "@material-ui/core/List"; import ListItem from "@material-ui/core/ListItem"; import ListItemAvatar from "@material-ui/core/ListItemAvatar"; import ListItemText from "@material-ui/core/ListItemText"; import Divider from "@material-ui/core/Divider"; import {makeStyles} from "@material-ui/styles"; import {PrimaryButton} from "../UIkit"; import {useDispatch} from "react-redux"; import {push} from "connected-react-router" const useStyles = makeStyles((theme) => ({ list: { background: '#fff', height: 'auto' }, image: { objectFit: 'cover', margin: '8px 16px 8px 0', height: 96, width: 96 }, text: { width: '100%' } })) const OrderedProducts = (props) => { const classes = useStyles(); const dispatch = useDispatch(); const products = props.products; const goToProductDetail = useCallback((id) => { dispatch(push('/product/'+id)) }, []) return ( <List> {products.map(product => ( <> <ListItem className={classes.list} key={product.id}> <ListItemAvatar> <img className={classes.image} src={product.images[0].path} alt="商品のTOP画像" /> </ListItemAvatar> <div className={classes.text}> <ListItemText primary={product.name} secondary={"サイズ:" + product.size} /> <ListItemText primary={"¥"+product.price.toLocaleString()} /> </div> <PrimaryButton label={"商品詳細を見る"} onClick={() => goToProductDetail(product.id)} /> </ListItem> <Divider /> </> ))} </List> ); } export default OrderedProducts;親コンポーネントから渡ってきた
products
をmap
でイテレートして、リスト表示しています。5.src/components/Products/index.jsexport {default as CartListItem} from "./CartListItem" export {default as ImageArea} from "./ImageArea" export {default as ImagePreview} from "./ImagePreview" export {default as ImageSwiper} from "./ImageSwiper" export {default as OrderedProducts} from "./OrderedProducts" //追記 export {default as OrderHistoryItem} from "./OrderHistoryItem" //追記 export {default as ProductCard} from "./ProductCard" export {default as SetSizeArea} from "./SetSizeArea" export {default as SizeTable} from "./SizeTable"エントリーポイントに追加します。
6.src/Router.jsximport React from 'react'; import {Route, Switch} from "react-router"; import {CartList, OrderConfirm,OrderHistory,ProductDetail,ProductEdit,ProductList,Reset,SignIn,SignUp} from "./templates"; import Auth from "./Auth" const Router = () => { return ( <Switch> <Route exact path={"/signup"} component={SignUp} /> <Route exact path={"/signin"} component={SignIn} /> <Route exact path={"/signin/reset"} component={Reset} /> <Auth> <Route exact path={"(/)?"} component={ProductList} /> <Route exact path={"/product/:id"} component={ProductDetail} /> <Route path={"/product/edit(/:id)?"} component={ProductEdit} /> <Route extct path={"/cart"} component={CartList} /> <Route extct path={"/order/confirm"} component={OrderConfirm} /> {*追記*} <Route extct path={"/order/history"} component={OrderHistory} /> {*追記*} </Auth> </Switch> ); }; export default Routerルーティングを定義します。
以上で実装は完了です!
さいごに
今回の要点をおさらいすると、
- React コンポーネントを再利用することで、新規コンポーネントの作成が効率化される。
以上です!
開発を進めれば進めるほどコンポーネントが豊富になり、より開発速度が上がるというのが。Reactの素晴らしいところですね。
このような学習内容を日々呟いていますので、よろしければTwitter(@ddpmntcpbr)のフォローもよろしくお願いします。
- 投稿日:2020-08-17T21:49:37+09:00
Next.jsの共通コンポーネントのPropsに型をつける方法
コンポーネントのPropsに型がつけられない
レイアウトなどを共通コンポーネントを作成し、各コンポーネントで利用しようとすると、下記のようになるかと思います。
layout.tsxconst Layout = ({ children }) => { return ( <>{children}</> ); }; export default Layout;そうしますと、eslintや使っているIDE(vscodeなど)の設定によっては、childrenがanyであるという警告が出てしまうことがあります。
こう修正します
なるべく警告は消しておきたいので、型定義を行いたいと思います。
react
のReactNode
が実体ですので、それを定義してあげる形となります。layout.tsximport { ReactNode } from "react"; interface Props { children: ReactNode; } const Layout = ({ children }: Props) => { return ( <>{children}</> ); }; export default Layout;
- 投稿日:2020-08-17T18:50:32+09:00
TypeScript学習ロードマップ
TypeScript全然わかんない...
という状態から、プロジェクトに導入できるまでになんとかなったので、
学習の参考になったものなどをまとめて学習ロードマップを作成いたしました。
私自身もまだまだのレベルですが、これからTypeScriptを勉強したい!という方の道しるべになれば幸いですLevel 0: TypeScriptってなんぞや?
まず学習する前に、その対象がなんなのか、を見極める作業です。
TypeScriptは
altJS
の1つです。
JSは元々大規模なコードを組むには不向きな設計になっているので、
altJSというメタ言語でラッピングすることで扱いやすくするものです。
altJSで他に有名なのはcoffeeScript
などでしょうか。TypeScriptは
type
と名乗っている通り、静的型付けを特徴としています。
また、jsと互換性があり、jsの上位互換(スーパセット)です。【おすすめ資料】
TypeScriptを入門者向けに解説!JavaScriptとの違いや勉強法までわかりやすくLevel 1: 記述等の基礎学習
以下のような資料をまずはざっくりと読みます。
下記資料全て熟読する、というよりは、それぞれをつまみ食いして大まかに把握していくイメージです。【おすすめ資料】
・TypeScriptチュートリアル① -環境構築編-
-> なにはともあれ環境構築!ですね。・TypeScriptの型入門
-> qiitaの良記事です。かなり長いので途中で苦しくなってきたら、また後から読み直すのもいいかもしれません。(私もお世話になりました)・サバイバルTypeScript
->網羅的な日本語でのts解説です。かなりわかりやすく導入も丁寧に感じました。・TypeScript Deep Dive
-> 私はこれを中心に学習しました。意外と深い部分まで解説されているようです。ただ、元々英語の有志による翻訳なので、少しわかりづらい部分もありました。
訳に混乱したら他の資料を見、また戻って見直して...とすると理解が進みます。Level2: TypeScript完全に理解した()
Level1までで全貌を掴んだところで、実際にどういう風に利用するのか?
という部分の理解の助けになります、みんな大好きオライリーの本です。【おすすめ資料】
・プログラミングTypeScript ――スケールするJavaScriptアプリケーション開発
発行日も2020年3月13日と比較的新しく、deepでかつわかりやすい良書でした。
後半はかなり高度な解説もあり、一読しただけで全てを吸収できるレベルではなかったです
この先もなんども読み返すことになりそう、そんな本です。Level3: TSXとの連携
実際にはReactやVue.jsなどと組み合わせて使う人も多いと思います。
私はReactを使うのでReactの資料中心になってしまいましたが、その他フレームワークでも解説サイトがたくさんあると思います。【おすすめ資料】
・ReactのプロジェクトにTypeScriptを導入する〜npm installからコンパイルまで〜
-> なにはともあれ環境構(ry・React公式チュートリアルをTypeScriptでやる
-> Reactの公式チュートリアルでお馴染みの三目並べゲームをTypeScriptに移行する方法を解説されています。
ざっくりとしたReactのJSからTSへの移行を理解することができます。・typescript-cheatsheets/react
-> react/typescriptの実装をチートシートとしてまとめてくれています。
英語に抵抗がなければ実践的な実装の仕方がわかっていいのではないでしょうか。(実はまだ読んでいる途中です)Level4: とにかくやってみることだ
ここまできたら実際にサンプルでもなんでもいいので組んでみたり、
既存のコードを移行してみるのが一番ですね。
TypeScriptに対応したパッケージなどであれば、公式サイトにtypeScript
という項目があったりしますので、それに目を通してみると色々為になります。【おすすめ資料】
ここは実際には人それぞれですが、型定義ファイルををたくさん探しにいくことになるかと思います。・npm @type探し
・styled-components typescript
・redux-toolkit advancedLevel5: 読める...読めるぞ!!
実際に一通り組めるようになったら人のコードをみて勉強するのがgoodですよね!
ブログなどで解説されている方やQiita記事、GitHubや公式のチュートリアルなんかも学習になります。【おすすめ資料】
・仮想DOMは本当に“速い”のか? DOM操作の新しい考え方を、フレームワークを実装して理解しよう
-> 実際に簡単な仮想DOMフレームワークを実装して、仮想DOMについて考えるサイトです。
ちょっと長いですがすごく勉強になります。・Apollo docs
-> GraphQLでお馴染みApolloの公式チュートリアルはTypeScriptで書かれています。・vercel/next.js
-> Next.js公式のTypeScript記述のサンプルです。かなりシンプルに実装されています。・今までの資料を読み返す
そして伝説へ...
参考資料
- 投稿日:2020-08-17T14:56:24+09:00
React Context APIでメディアクエリを共通化
概要
レスポンシブ対応で利用するメディアクエリをReact Context APIを使って共有・取得をシンプル実装した
Context APIについて
簡単に言うとpropsで渡さなくても、コンポーネント間で共有する方法を提供しているAPIになります。
詳しくはReactの公式を確認いただければと思います!
https://reactjs.org/docs/context.htmlメディアクエリについて
今回、ブレークポイントの設定・取得は「react-responsive」を使います。
Hookになっているので使いやすいです。
https://github.com/contra/react-responsiveそして、ブレークポイントはこちらになります。
- 560px未満をスマホと設定
- 960px未満をタブレットと設定
https://hashimotosan.hatenablog.jp/entry/2019/05/28/164834
さっそく実装を見てみましょう
MediaQuery.tsximport React, { useContext } from 'react' import { useMediaQuery } from 'react-responsive' const MediaQueryContext = React.createContext({ isSmartPhone: false, isTablet: false, isMobile: false, isPc: false }) export const MediaQueryProvider: React.FC = ({ children }) => { const isSmartPhone = useMediaQuery({ maxWidth: 559 }) const isTablet = useMediaQuery({ minWidth: 560, maxWidth: 959 }) const isMobile = isSmartPhone || isTablet const isPc = !isMobile return ( <MediaQueryContext.Provider value={{ isSmartPhone, isTablet, isMobile, isPc }} > {children} </MediaQueryContext.Provider> ) } export const useDeviceType = () => useContext(MediaQueryContext)まずは
Context
とProvider
、useContext
でメディアクエリを取得するための関数を作成します。
Context
はexport
せずに隠蔽します。
また、MediaQueryProvider
はFunctionComponentで定義してuseMediaQuery
にてブレークポイントを設定します。これでほぼ実装はお終いです。
App.tsximport { MediaQueryProvider } from './MediaQuery' render( <MediaQueryProvider> <App /> </MediaQueryProvider>, document.querySelector('#app') )さきほどの
MediaQueryProvider
でアプリケーションを囲います。
これでどのコンポーネントでも利用できるようになります。HogePage.tsximport { useDeviceType } from './MediaQuery' const HogePage: React.FC = () => { const { isSmartPhone, isTablet, isMobile, isPc } = useDeviceType() return ( ︙ ) } export default HogePageコンポーネントで
useDeviceType
を利用してメディアクエリを取得して完了です!まとめ
最初の実装はCustom Hookで作成していましたが、コンポーネント間でもっと簡単に共有したいと思いContext APIを利用しました。
他にも色々な用途(認証、翻訳等)でReact Context APIは利用できそうです。
- 投稿日:2020-08-17T14:54:42+09:00
自分用メモ reactスタイリング styled-components
はじめに
reactとjsをいじり始めて、まだ日が浅い素人ですが、人によってcssに記述方法が違っていて、自分もどれにすればいいのかまた古い情報や新しい情報が入り混じって大変わかりにくいので、ここでまとめておこうと思います!!
至らない点が数あると思いますが、その際はアドバイスしていただけると幸いです。
そしてトラハックさんのこちらの動画を中心にまとめています!!
この記事より詳しくわかりやすく説明しておりますので、ぜひこちらからご覧になってみてください!!インラインスタイル
・JSXのstyle属性を使う
・導入が簡単
・公式では非推奨
・計算が増える パフォーマンスが落ちる!
・擬似要素を使えない 例)before, hover, after,import React from 'react'; const style = { backgroundColor: 'none'; border: 'none', display: 'block', padding: '4px 16px', } const PrimayButton = (props) => { return( <button style={style} 青いボタン </buttoon> ); }; export default PrimaryButton;注意)キャメルケース
クラスによるスタイリング
・HTMLファイル読み込み
・導入が簡単
・全てがグローバルスコープ
→命名規則を工夫しなければならない!
→どこにどのクラスを書いたか?注意)元になるindex.jsで読み込まなければならない!(create-react-app)
一般的なスタイルになるのでコードは省かせていただきます!CSS Modules
・Webpack+Babelを使う
・CSSをモジュールとしてimport
・慣れ親しんだCSS記法
・クラス名の自動変換
・jsの読み取り順序で適用順序も変わる先ほどと違うのはjsのなかでimportする!
モジュール化している(css)webpack・・・モジュールバンドラーと呼ばれ複数のファイルをまとめる!
babel・・・コンパイラー(トランスコンパイラー)中身を変換する(ここではcss).button{ background-color: #42a5f5; border: none; display: block; padding: 4px 16px; }import React from 'react'; import './button.css'; const PrimayButton = (props) => { return( <button className={"button"}> 青いボタン </button> ) }Css in js
・styledーcomponentが有名
・スコープ限定
・ベンダープレフィックス付与
ブラウザ対応を自動でやってくれるもの
・propsでスタイルを変換できる!
・シンタックスハイライト
→style objectで解決import React from 'react'; import styled from 'styled-components'; const Button = styled.button({ backgroundColor: '#442a5f5', border: 'none', display: 'block', padding: '4px 16px', '&:hover': { backgroundColor: '#80d6ff', } }) const PrimaryButton = (props) => { return( <Button> 青いボタン </Button> ) }propsによってスタイル変更もできてスコープも限定し、命名規則によって縛れることもないまたオブジェクト型にすることでシンタックハイライトも入るようになった!
またコンポーネントにスタイルをつけたい場合は、styled.(component名)({})とする!styled-component
この方法がcss in jsの一般的になるので説明しておきます!
これについてもトラハックさんの別動画を参照しておりますので詳しく知りたい方はこちらからご覧いただくようにお願いいたします!記法
・Tagged Template Literal(シンタックスハイライトなし)
→キャメルケースではないconst Temlate Literal = styled.button` background-color: #42a5f5; border: none; display: block; padding: 4px 16px; &:hover: { background-color: #80d6ff } `・style Objects(シンタックスハイライトあり)
→キャメルケースになるconst Button = styled.button({ backgroundColor: '#42a5f5', border: 'none', display: 'block', padding: '4px 16px', '&:hover': { backgroundColor: '#80d6ff' } )}propsでスタイルを切り替える
const Button = styled.button(props => ({ backgroundColor: props.isPrimary ? '#41B&E&' : '#FFB549', border: 'none', display: 'block', padding: '4px 16px', '&:hover':{ backgroundColor: props.isPrimary ? '#a2dbf3' : '#FFCA7C', } })) const BaseButton = (props) => { return( <Button> {props.label} </Button> ); }; //呼び出し側 //<BaseButton isPrimary={true} label={"Primary"} //<BaseButton isPrimary={false} label={"Secondary"}注意)? true : false
ここではisPrimaryのtrue,falseによってスタイルを変えている!(backgroundColor)
themeを使う
import React from 'react'; import styled, {ThemeProvider} from 'styled-components'; const theme = { main: '#41B6E6', light: '#a2dbf3' } const BaseButton = (props) => { return ( <ThemeProvider theme={them}> <Button> {props.label} </Button> </ThemeProvider> ); }; export default BaseButton;ここではthemeを宣言後、使いたいコンポーネントでラッピングする!
そしてthemeを渡す!const Button = styled.button({ backgroundColor: 'theme.main', border: 'none', display: 'block', padding: '4px 16px', '&:hover':{ backgroundColor: theme.light } })そしてコンポーネント内で呼び出す!
最後に
まとめてみてstyle Objectsの形がスコープやシンタックスハイライト、propsの切り替えなどの観点から見て一番使いやすいのかなと感じたので積極的に使っていこうと思いました。
まだまだ知識不足で見落としている箇所などあると思いますがその際はアドバイスしていただけると幸いです!
最初にも書いた通り参照したのはトラハックさんの動画になりますので詳しく知りたい方はそちらの動画を見ていただくようお願いいたします!参照
トラハック 【結論】Reactのスタイリング方法を比較するぞ【CSS in JS推したい】
トラハック Reactのスタイリング定番styled-componentsの活用パターン
- 投稿日:2020-08-17T07:10:52+09:00
【初Laravel】未経験がLaravelでカブトムシ繁殖家のためのWebアプリを作ってみた
はじめに
すっかり夏です!夏といえばカブトムシですね!みなさんカブトムシ採ってますか?
家では今、成虫・幼虫・卵あわせて100頭ほどのカブトムシとクワガタ達が暮らしています。趣味でカブクワブリーダー、本職スマホゲームエンジニア(最近はHTML5が主戦場)、副業でジーズアカデミーのメンターをさせて頂いてます@zprodevです。
ご縁がありメンターさせていただいているジーズアカデミーでは、WebフレームワークとしてLaravelが選ばれており、メンター期間である卒業制作でもLaravelが使われることが多いです。
自分の専門はネイティブアプリやゲームですしLaravelは未経験ですが、Laravel特有の問題で躓く生徒さんもいるため、常々こう思っていました。
Laravel理解するために自分でも何か作ってみねば!
(ついでに、業務で少し触っただけのReactもフルスクラッチで書いてみたい!)ということでLaravelとReactの勉強がてら、カブトムシ・クワガタ繁殖家のためのWebアプリを7月1日にリリースしました。
お気づきの方もいるかもしれませんが、タイトルは7月上旬にトレンドになっていた@hara_taku_さんの記事のパクリです!
ウシとムシという違いはあれど、リリース時期もコンセプトも似ていて勝手に親近感を覚えています。
@hara_taku_さんの記事はサービス設計する上でとても参考になるので、これから自分のサービスを開発したい方は一度読んでみることをオススメします。本記事では、これからWebアプリを作りたい方向けに「Webアプリちゃんとする」という観点で、お作法やそれを実現するためのツールやライブラリ、独自の工夫なんかをゆるっと紹介します。
以下、虫苦手な人は閲覧注意
まず作ったもの紹介
集めて繋がるカブクワ情報共有サイトBeetlect(ビートレクト)
https://beetlect.com/主なフレームワーク・ライブラリ
バックエンド
Laravel v7.2.2
Intervention Image v2.5.1フロントエンド
React v16.13.1
react-router-dom v5.1.3
react-chartjs-2 v2.9.0
Material-UI v4.9.7機能概要
飼育中のカブクワ情報を登録できる
スマートフォンで撮影した写真と共に、種類やコメント、その他詳細情報を個体情報として登録して電子管理できます。
普通はラベルシールなどを自作して飼育ケースに貼り付けて管理しますが、汚れたり無くしたり結構めんどくさいのです。
幼虫は成長過程も記録できる
幼虫の育て方で成虫の大きさが変わるため、与えたエサや管理温度、体重の変化などを記録するのは重要なのです。
情報はみんなで共有
飼育を始めた頃は「どのくらいの期間で成虫になるのか?」とか「サイズはどのくらいを目指せば良いのか?」とか「飼育温度はどのくらいが良いのか?」とかの情報収集に苦労します。
各自の飼育情報管理を効率化すると共に、新米ブリーダーの助けになる情報を共有し、カブクワ界隈の盛り上げに貢献出来る訳です。
【本題】ちゃんとしたいポイント
自分の知る範囲・調べた範囲で、ちゃんとしたWebアプリに必要な要素と、対応するメリットをゆるっと紹介します。
SPA(シングルページアプリケーション)
複数のHTMLファイルをURL遷移で繋ぐのでは無く、1つのHTMLファイルの中でJavaScriptによるHTML要素を書き換えでページを構築する手法。
主なメリット
- 画面遷移時の無駄な通信や待ち時間を減らせる
- 画面遷移アニメーション(トランジション)とかいい感じにできる
- 結果、ネイティブアプリっぽいモダンなWebアプリになる
PWA(プログレッシブウェブアプリケーション)
端末のホーム画面に追加してネイティブアプリのように利用させる仕組み。
主なメリット
- アクセシビリティの向上
- フルスクリーンで表示できる
- ブラウザによってはストレージ永続化効果がある
レスポンシブウェブデザイン
端末の画面サイズに合わせて柔軟にUIレイアウトを変更する手法。
主なメリット
- PCでもスマートフォンでも最適な表示になる
- いろいろな端末で利用できるというWebアプリ最大の利点を殺さない
OGP(Open Graph Protocol)
FacebookやTwitterでURLがシェアされた際、そのページの情報をSNS側に知らせて適切なリンク画像やタイトルを表示させるための仕組み。
FacebookとTwitterに対応させておけば、LINEやSlackなど様々なツールでも効果がある。主なメリット
- 利用者がシェアしやすい
- シェアされた側にも内容が分かりやすい
- 結果、SNSからの流入が期待できる
SEO(Search Engine Optimization)
Googleなどの検索エンジンが理解できるようページ構成を最適化する手法。
主なメリット
- Googleなどの検索結果に正しく表示されるようになる
- 結果、ブラウザ検索からの流入が期待できる
AMP(Accelerated Mobile Pages)
Googleが推進する、モバイルページを高速に表示させるための仕組み。
主なメリット
- GoogleのCDNに事前にキャッシュされ、初回表示速度が高速化される(と思われる)
Beetlectでの対応状況
以下、Beetlectでの対応状況と対応方法や参考リンクになります。
SPA
Reactを使ってJavaScriptでの画面構築しています。
react-router-domを使ってURLでのページルーティングや、Laravel標準のページネーション機能を使って無限スクロールも実装してみました。
![]()
PWA
iOS13.4未満ではPWAでWebカメラ(WebRTC)が使えないため、iOSは13.4以降のみPWAになるよう実装しました。
AndroidではMaskable Iconにも対応しています。
↓のFavicon Generatorを使うと、主要OS・ブラウザを考慮したfaviconやアイコン系画像リソースと共にPWA用のmanifestファイルも簡単に生成できます。
https://realfavicongenerator.net/ただmanifestの設定はアプリに合わせて調整した方が良いので、設定値の参考までに自分のPWAテスト実験用リポジトリを貼っておきます。
https://github.com/zprodev/pwa-testレスポンシブウェブデザイン
Material-UIのGridを活用して実現しています
Responsive UI - Material-UIMaterial-UIはGoogleの提唱するマテリアルデザインをReactで実現するためのライブラリなので、フラットデザインとかスキューモーフィズムとか他のデザインを採用する場合は適宜ライブラリ選定が必要です。モダンなライブラリならレスポンシブウェブデザインへの対応策は何かしら用意されているんじゃないでしょうか。
OGP
TwitterやFacebookからOGP情報を収集に来るbotはJavaScriptを解釈しない為、サーバー側で制御する必要があります。
Twitterシェアボタンを実装している個体情報ページにアクセスがあった場合のみ、サーバー側でUserAgentをチェックしてOGP収集botかどうか判定し、botであれば動的にOGP系タグを設定したHTMLを返却すようにしました。
個体情報ページ以外のURLは、SPAのベースのHTMLファイルに設定した共通情報が表示されます。
SEO
無限スクロールで表示データが逐次追加される仕様なので、スクロールしてくれないGoogleのbotでは全てのページをインデックス登録してくれません。
とりあえずサイトマップの動的生成で全個体情報ページのリストを作成し、インデックスさせるようにしています。site:beetlect.comでインデックス状況確認
サイトマップについて
GoogleがサポートしているSEO絡みのタグAMP
個体情報ページで試してみようと思いながら、手をつけられていません
UX向上のための対応
Webアプリによって要否は異なりますが、BeetlectでのUX向上のための工夫なども一部紹介します。
WebP対応
サーバーにアップロードされた写真はWebPとJPEGで保存し、対応しているブラウザにはWebPを配信することで利用者の通信量削減や表示速度向上を図っています。
最初はサーバーのストレージ圧迫を懸念してJPEGのみ対応にしようと思っていましたが、SafariもiOS14からWebP対応というニュースを聞きつけ、勢いでWebP対応に踏み切りました。クライアントからPNGの画像をアップロードし、サーバー側でIntervention Imageを使用してWebPとJPEGに変換しています。
ついでに、サムネイル的に使用する小さい画像も生成し、画面に応じて使い分けています。画像遅延ロード
最近のChromium系ブラウザはネイティブでlazy loadingに対応しています。Safariでもデフォルトでは無効ですが、実験的な機能として既に搭載されているので設定変更すれば使えます。
Beetlectでは無限スクロールで少しづつ情報を取得することが実現できているので画像遅延ロードの効果は薄いですが、「ネイティブで使えるなら…」ということで実装しています。Native image lazy-loading for the web
入力値のオートコンプリート
種名なんかは入力パターンが限られるので、過去に登録されている種名が候補として表示されるようにしました。
![]()
Material-UIの機能で実現しています。
Autocomplete React component - Material-UI入力値の即時バリデーション
入力桁数やフォーマットのバリデーションは即時に行い画面に反映するようにしています。
![]()
チェック自体は正規表現などで泥臭くやってますが、エラーの表示はMaterial-UIの機能を活用しています。
Text Field React component - Material-UI必須入力値は最小限に
投稿のハードルを下げるため、投稿時の必須情報は写真と種名のみにしました。ガチ勢は任意で詳細情報も入力できます。
まとめ
Webアプリ開発を通して、LaravelとReactの思想や使い方をある程度理解できたかなと思います。
何か勉強するときは、自分が必要としているものを作るとか明確な目的を持って取り組むと、モチベーションが続くのでオススメです。そして、カブクワ系エンジニアの方々は是非 Beetlect 使ってみてください!
非カブクワ系エンジニアの方々は、とりあえずカブクワ採集からはじめましょう!(去年は東京23区内で40匹ほどのカブクワを見つけましたよ)
- 投稿日:2020-08-17T02:58:00+09:00
Amazon-connect-streams を React FC / TypeScript で書く(イベント処理)
はじめに
タイトルのことを、最近の React / Function Conponents / TypeScript で書こうとして苦労したので備忘録
第2弾です。
今回は amazon-connect-streams にある、イベントの処理を追加します。
開発環境
- Visual Studio Code
- React 16.13
- Create-React-App
- AmazonConnect Streams
参照:https://github.com/amazon-connect/amazon-connect-streams
手順
前回構築した環境にイベントの処理を追加します。
マニュアルには下記のように記述がありますが、これを TypeScript で書きます。
前回構築ソースの useEffect 内に記述します。
イベントが処理されたら、コンソールにログを出力するよう記述します。connect.contact( (contact: connect.Contact) => { // onRefresh contact.onRefresh( function () { console.log("onRefresh"); }); // onIncoming contact.onIncoming( function () { console.log("onIncoming"); }); // onPending contact.onPending( function () { console.log("onPending"); }); // onConnecting contact.onConnecting( function () { console.log("onConnecting"); }); // onAccepted contact.onAccepted( function () { console.log("onAccepted"); }); // onMissed contact.onMissed( function () { console.log("onMissed"); }); // onEnded contact.onEnded( function () { console.log("onEnded"); }); });続いて Agent についても同様に記述します。
connect.agent( (agent: connect.Agent) => { // onRefresh agent.onRefresh( function () { console.log("onRefresh"); }); // onStateChange agent.onStateChange( function () { console.log("onStateChange"); }); // onRoutable agent.onRoutable( function () { console.log("onRoutable"); }); // onNotRoutable agent.onNotRoutable( function () { console.log("onNotRoutable"); }); // onOffline agent.onOffline( function () { console.log("onOffline"); }); // onError agent.onError( function () { console.log("onError"); }); // onSoftphoneError agent.onSoftphoneError( function () { console.log("onSoftphoneError"); }); // onAfterCallWork agent.onAfterCallWork( function () { console.log("onAfterCallWork"); }); });TypeScript なので、型を気にして記述しました。
解説
完成したコードを実行した結果が以下です。
電話を受信し、顧客側から切断しました。onEnded イベントやonRefresh イベントをキャッチしています。
最終的に完成したソースです。
App.tsximport React, {useRef,useEffect} from 'react'; import logo from './logo.svg'; import './App.css'; import 'amazon-connect-streams'; import pkg from '../package.json'; import { Agent } from 'http'; import moment from "moment"; const style = { minWidth: 64, // 数値は"64px"のように、pxとして扱われます lineHeight: "32px", borderRadius: 4, border: "none", padding: "0 16px", height: "500px", color: "#fff", // background: "#639" }; declare var connect: any; function App() { // let containerDiv = React.createRef<HTMLDivElement>(); const El = useRef<HTMLDivElement>(null); useEffect( ()=>{ connect.core.initCCP(El.current, { ccpUrl: 'https://xxx.awsapps.com/connect/ccp-v2/', loginPopup: true, // optional, defaults to `true` region: "ap-northeast-1", // REQUIRED for `CHAT`, optional otherwise softphone: { // optional allowFramedSoftphone: true, // optional disableRingtone: false, // optional }); } }); connect.contact( (contact: connect.Contact) => { // onRefresh contact.onRefresh( function () { console.log("onRefresh"); }); // onIncoming contact.onIncoming( function () { console.log("onIncoming"); }); // onPending contact.onPending( function () { console.log("onPending"); }); // onConnecting contact.onConnecting( function () { console.log("onConnecting"); }); // onAccepted contact.onAccepted( function () { console.log("onAccepted"); }); // onMissed contact.onMissed( function () { console.log("onMissed"); }); // onEnded contact.onEnded( function () { console.log("onEnded"); }); }); connect.agent( (agent: connect.Agent) => { // onRefresh agent.onRefresh( function () { console.log("onRefresh"); }); // onStateChange agent.onStateChange( function () { console.log("onStateChange"); }); // onRoutable agent.onRoutable( function () { console.log("onRoutable"); }); // onNotRoutable agent.onNotRoutable( function () { console.log("onNotRoutable"); }); // onOffline agent.onOffline( function () { console.log("onOffline"); }); // onError agent.onError( function () { console.log("onError"); }); // onSoftphoneError agent.onSoftphoneError( function () { console.log("onSoftphoneError"); }); // onAfterCallWork agent.onAfterCallWork( function () { console.log("onAfterCallWork"); }); }); },); return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.tsx</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> <main> <div className="ccp"> {/* ccp */} <div style={style} ref={El} /> </div> <div className="content" /> </main> <footer> <p className="version">version: {pkg.version}</p> </footer> </div> ); } export default App;最後に
自分のための備忘録です。
次はカスタムのUIに挑戦してきたいと思います。