20200817のReactに関する記事は9件です。

Next.jsのルーティング使ったみた

はじめに

next.jsのLinkコンポーネントを使い、動的なルーティング(ページ外遷移)や静的なルーティング(ページ内遷移)を実装する機会があったので復習としてこの記事を残します。今回はNext.js 9から追加されたDynamic Routingを簡易的に使ってみました。

完成形

画面収録 2020-08-17 21.49.35.mov.gif

実装

遷移させたい要素に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

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

React FormikでFormの各部品のcomponentを制作

先日、「React Formikの入門編」の記事を投稿しました。
その記事をもとにformの各部品(input、textarea、select)などをcomponentとして分けたので共有します。
※実際にアプリケーションを開発する際はこのようにcomponent分けをするのではないでしょうか。

FormContainer

  • form画面の親コンポーネント
  • initialValuesvalidationSchemaonSubmitを定義
  • 呼び出し先に送るpropsを設定
FormContainer.js
import { 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.js
import 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.js
import { 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.js
import { 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.js
import { 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.js
import { 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.js
import { 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.js
import { 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.js
import { 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.js
import '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.js
import React from 'react';

function SubmitButton(props) {
  const { formik } = props;
  return (
    <button type="submit" disabled={!formik.isValid}>
      Submit
    </button>
  );
}

export default SubmitButton;

以上です。
修正した方が良い箇所がありましたら教えていただけますと幸いです。

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

【備忘録】日本一わかりやすいReact-Redux講座 実践編 #13 「注文履歴を確認しよう〜コンポーネントの再利用〜」

はじめに

概要

この記事は、トラハック氏(@torahack_)が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。

前回の講座で、トランザクション処理を含めた購入処理を実装しました。

今回は、これまでに作成してきたコンポーネントを再利用しながら、注文履歴画面を作成します。

※ 前回記事: 【備忘録】日本一わかりやすいReact-Redux講座 実践編 #12後半 「トランザクションを使って商品を注文しよう(後半)」

動画URL

注文履歴を確認しよう〜コンポーネントの再利用〜【日本一わかりやすいReact-Redux講座 実践編#13】

要点

  • React コンポーネントを再利用することで、新規コンポーネントの作成が効率化される。

完成系イメージ

http://localhost:3000/order/confirm

image.png

カート内の商品を入れた状態で、「注文を確定する」をクリックすると、

http://localhost:3000/order/completes

image.png

ここは未定義です。Drawerメニューの「商品履歴」をクリックすると、

image.png

カートに入れた商品が、購入履歴として表示されています。

再度、商品を購入すると、別の注文IDとして購入履歴が追加されていきます。

image.png

メイン

実装概要

今回は、購入履歴を確認する画面を作ります。これまでの講座で学習してきたことの復習になります。

前回の講座で、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.js
1.src/reducks/store/initialState.js
const initialState = {
  products: {
    list: []
  },

  users: {
    cart: [],
    isSignedIn: false,
    orders: [], //追記
    role:  "",
    uid: "",
    username: ""
  }
};

export default initialState

usersの配列要素として、orders配列を定義します。

2.src/reducks/users/operations.js
import { 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.js
export const FETCH_ORDERS_HISTORY = "FETCH_ORDERS_HISTORY";
export const fetchOrdersHistoryAction = (orders) => {
  return {
    type: "FETCH_ORDERS_HISTORY",
    payload: orders
  }
}
.
.
.
4.src/reducks/users/reducers.js
import * 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.js
import { createSelector } from "reselect";

const usersSelector = (state) => state.users;
.
.
.
export const getOrdersHistory = createSelector(
  [usersSelector],
  state => state.orders
)
.
.
.

actions, reducers,selectorsを定義します。

getOrdersHistory()から、Store内のordersを参照できるようになりました。

コンポーネントファイル実装

コンポーネント構想は以下のイメージ。

13-1.png

ここに書いてあるコンポーネントだけでなく、過去の講座で作成したコンポーネントをどんどん再利用していきます

実装ファイル(コンポーネント)
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.jsx
1.src/templates/OrderHistory.jsx
import 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.js
export {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として取得します。
  • ordersmapを用いてイテレートし、<OrderHistoryItem>へ展開します。
3.src/components/Products/OrderHistoryItem.jsx
import 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.jsx
import 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;

親コンポーネントから渡ってきたproductsmapでイテレートして、リスト表示しています。

5.src/components/Products/index.js
export {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.jsx
import 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)のフォローもよろしくお願いします。

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

Next.jsの共通コンポーネントのPropsに型をつける方法

コンポーネントのPropsに型がつけられない

レイアウトなどを共通コンポーネントを作成し、各コンポーネントで利用しようとすると、下記のようになるかと思います。

layout.tsx
const Layout = ({ children }) => {
  return (
    <>{children}</>
  );
};
export default Layout;

そうしますと、eslintや使っているIDE(vscodeなど)の設定によっては、childrenがanyであるという警告が出てしまうことがあります。

こう修正します

なるべく警告は消しておきたいので、型定義を行いたいと思います。
reactReactNodeが実体ですので、それを定義してあげる形となります。

layout.tsx
import { ReactNode } from "react";

interface Props {
  children: ReactNode;
}

const Layout = ({ children }: Props) => {
  return (
    <>{children}</>
  );
};
export default Layout;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript学習ロードマップ

TypeScript全然わかんない...
という状態から、プロジェクトに導入できるまでになんとかなったので、
学習の参考になったものなどをまとめて学習ロードマップを作成いたしました。
私自身もまだまだのレベルですが、これからTypeScriptを勉強したい!という方の道しるべになれば幸いです:runner_tone3:

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でかつわかりやすい良書でした。
後半はかなり高度な解説もあり、一読しただけで全てを吸収できるレベルではなかったです:eyes:
この先もなんども読み返すことになりそう、そんな本です。

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 advanced

Level5: 読める...読めるぞ!!

実際に一通り組めるようになったら人のコードをみて勉強するのがgoodですよね!
ブログなどで解説されている方やQiita記事、GitHubや公式のチュートリアルなんかも学習になります。

【おすすめ資料】
仮想DOMは本当に“速い”のか? DOM操作の新しい考え方を、フレームワークを実装して理解しよう
-> 実際に簡単な仮想DOMフレームワークを実装して、仮想DOMについて考えるサイトです。
ちょっと長いですがすごく勉強になります。

Apollo docs
-> GraphQLでお馴染みApolloの公式チュートリアルはTypeScriptで書かれています。

vercel/next.js
-> Next.js公式のTypeScript記述のサンプルです。かなりシンプルに実装されています。

・今までの資料を読み返す

そして伝説へ...

参考資料

altJS(代替JavaScript言語)とは?選び方と注意点を徹底比較!

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

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.tsx
import 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)

まずはContextProvideruseContextでメディアクエリを取得するための関数を作成します。

Contextexportせずに隠蔽します。

また、MediaQueryProviderはFunctionComponentで定義してuseMediaQueryにてブレークポイントを設定します。

これでほぼ実装はお終いです。

App.tsx
import { MediaQueryProvider } from './MediaQuery'

render(
  <MediaQueryProvider>
    <App />
  </MediaQueryProvider>,
  document.querySelector('#app')
)

さきほどのMediaQueryProviderでアプリケーションを囲います。

これでどのコンポーネントでも利用できるようになります。

HogePage.tsx
import { 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は利用できそうです。

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

自分用メモ 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の活用パターン

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

【初Laravel】未経験がLaravelでカブトムシ繁殖家のためのWebアプリを作ってみた

はじめに

すっかり夏です!夏といえばカブトムシですね!みなさんカブトムシ採ってますか?
家では今、成虫・幼虫・卵あわせて100頭ほどのカブトムシとクワガタ達が暮らしています。

趣味でカブクワブリーダー、本職スマホゲームエンジニア(最近はHTML5が主戦場)、副業でジーズアカデミーのメンターをさせて頂いてます@zprodevです。

ご縁がありメンターさせていただいているジーズアカデミーでは、WebフレームワークとしてLaravelが選ばれており、メンター期間である卒業制作でもLaravelが使われることが多いです。

自分の専門はネイティブアプリやゲームですしLaravelは未経験ですが、Laravel特有の問題で躓く生徒さんもいるため、常々こう思っていました。

Laravel理解するために自分でも何か作ってみねば!
(ついでに、業務で少し触っただけのReactもフルスクラッチで書いてみたい!)

ということでLaravelとReactの勉強がてら、カブトムシ・クワガタ繁殖家のためのWebアプリを7月1日にリリースしました。

お気づきの方もいるかもしれませんが、タイトルは7月上旬にトレンドになっていた@hara_taku_さんの記事のパクリです!

【初アプリ】未経験がFlutterで肉牛繁殖農家のためのアプリを作ってみた - Qiita

ウシとムシという違いはあれど、リリース時期もコンセプトも似ていて勝手に親近感を覚えています。
@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

機能概要

飼育中のカブクワ情報を登録できる
スマートフォンで撮影した写真と共に、種類やコメント、その他詳細情報を個体情報として登録して電子管理できます。
普通はラベルシールなどを自作して飼育ケースに貼り付けて管理しますが、汚れたり無くしたり結構めんどくさいのです。
スクリーンショット 2020-08-09 10.20.26.png

幼虫は成長過程も記録できる
幼虫の育て方で成虫の大きさが変わるため、与えたエサや管理温度、体重の変化などを記録するのは重要なのです。
スクリーンショット 2020-08-09 10.26.31.pngスクリーンショット 2020-08-09 10.26.40.png

情報はみんなで共有
飼育を始めた頃は「どのくらいの期間で成虫になるのか?」とか「サイズはどのくらいを目指せば良いのか?」とか「飼育温度はどのくらいが良いのか?」とかの情報収集に苦労します。
各自の飼育情報管理を効率化すると共に、新米ブリーダーの助けになる情報を共有し、カブクワ界隈の盛り上げに貢献出来る訳です。
スクリーンショット 2020-08-09 10.51.52.png

【本題】ちゃんとしたいポイント

自分の知る範囲・調べた範囲で、ちゃんとした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標準のページネーション機能を使って無限スクロールも実装してみました。
url.gif scroll.gif

PWA

iOS13.4未満ではPWAでWebカメラ(WebRTC)が使えないため、iOSは13.4以降のみPWAになるよう実装しました。
AndroidではMaskable Iconにも対応しています。
pwa.gif

↓のFavicon Generatorを使うと、主要OS・ブラウザを考慮したfaviconやアイコン系画像リソースと共にPWA用のmanifestファイルも簡単に生成できます。
https://realfavicongenerator.net/

ただmanifestの設定はアプリに合わせて調整した方が良いので、設定値の参考までに自分のPWAテスト実験用リポジトリを貼っておきます。
https://github.com/zprodev/pwa-test

レスポンシブウェブデザイン

Material-UIのGridを活用して実現しています
responsive.gif
Responsive UI - Material-UI

Material-UIはGoogleの提唱するマテリアルデザインをReactで実現するためのライブラリなので、フラットデザインとかスキューモーフィズムとか他のデザインを採用する場合は適宜ライブラリ選定が必要です。モダンなライブラリならレスポンシブウェブデザインへの対応策は何かしら用意されているんじゃないでしょうか。

OGP

TwitterやFacebookからOGP情報を収集に来るbotはJavaScriptを解釈しない為、サーバー側で制御する必要があります。
Twitterシェアボタンを実装している個体情報ページにアクセスがあった場合のみ、サーバー側でUserAgentをチェックしてOGP収集botかどうか判定し、botであれば動的にOGP系タグを設定したHTMLを返却すようにしました。
スクリーンショット 2020-08-11 2.48.04.png

個体情報ページ以外のURLは、SPAのベースのHTMLファイルに設定した共通情報が表示されます。
スクリーンショット 2020-08-11 2.45.28.png

Twitterの仕様
Twitterの確認用ツール

Facebookの仕様
Facebookの確認用ツール

SEO

無限スクロールで表示データが逐次追加される仕様なので、スクロールしてくれないGoogleのbotでは全てのページをインデックス登録してくれません。
とりあえずサイトマップの動的生成で全個体情報ページのリストを作成し、インデックスさせるようにしています。

site:beetlect.comでインデックス状況確認
サイトマップについて
GoogleがサポートしているSEO絡みのタグ

AMP

個体情報ページで試してみようと思いながら、手をつけられていません :innocent:

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

入力値のオートコンプリート

種名なんかは入力パターンが限られるので、過去に登録されている種名が候補として表示されるようにしました。
スクリーンショット 2020-08-14 19.01.27.png スクリーンショット 2020-08-14 19.01.51.png

Material-UIの機能で実現しています。
Autocomplete React component - Material-UI

入力値の即時バリデーション

入力桁数やフォーマットのバリデーションは即時に行い画面に反映するようにしています。
validation.gif valid.gif

チェック自体は正規表現などで泥臭くやってますが、エラーの表示はMaterial-UIの機能を活用しています。
Text Field React component - Material-UI

必須入力値は最小限に

投稿のハードルを下げるため、投稿時の必須情報は写真と種名のみにしました。ガチ勢は任意で詳細情報も入力できます。
require.gif

まとめ

Webアプリ開発を通して、LaravelとReactの思想や使い方をある程度理解できたかなと思います。
何か勉強するときは、自分が必要としているものを作るとか明確な目的を持って取り組むと、モチベーションが続くのでオススメです。

そして、カブクワ系エンジニアの方々は是非 Beetlect 使ってみてください!
非カブクワ系エンジニアの方々は、とりあえずカブクワ採集からはじめましょう!(去年は東京23区内で40匹ほどのカブクワを見つけましたよ)

最後に、うちのお気に入りのクワガタ達を置いておきますね:rainbow:
20200705_171207.jpg

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

Amazon-connect-streams を React FC / TypeScript で書く(イベント処理)

はじめに

タイトルのことを、最近の React / Function Conponents / TypeScript で書こうとして苦労したので備忘録

第2弾です。

今回は amazon-connect-streams にある、イベントの処理を追加します。

開発環境

  1. Visual Studio Code
  2. React 16.13
  3. Create-React-App
  4. AmazonConnect Streams

参照:https://github.com/amazon-connect/amazon-connect-streams

手順

前回構築した環境にイベントの処理を追加します。

マニュアルには下記のように記述がありますが、これを TypeScript で書きます。
image.png

前回構築ソースの 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 なので、型を気にして記述しました。

解説

完成したコードを実行した結果が以下です。

image.png

電話を受信し、顧客側から切断しました。onEnded イベントやonRefresh イベントをキャッチしています。

image.png

最終的に完成したソースです。

App.tsx
import 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に挑戦してきたいと思います。

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