GraphQL codegen

クエリの作成

import { gql } from '@apollo/client'

export const GET_USERS = gql`
  query GetUsers {
    users(order_by: { created_at: desc }) {
      id
      name
      created_at
    }
  }
`
export const GET_USERS_LOCAL = gql`
  query GetUsers {
    users(order_by: { created_at: desc }) @client {
      id
      name
      created_at
    }
  }
`
export const GET_USERIDS = gql`
  query GetUserIds {
    users(order_by: { created_at: desc }) {
      id
    }
  }
`
export const GET_USERBY_ID = gql`
  query GetUserById($id: uuid!) {
    users_by_pk(id: $id) {
      id
      name
      created_at
    }
  }
`
export const CREATE_USER = gql`
  mutation CreateUser($name: String!) {
    insert_users_one(object: { name: $name }) {
      id
      name
      created_at
    }
  }
`
export const DELETE_USER = gql`
  mutation DeleteUser($id: uuid!) {
    delete_users_by_pk(id: $id) {
      id
      name
      created_at
    }
  }
`
export const UPDATE_USER = gql`
  mutation UpdateUser($id: uuid!, $name: String!) {
    update_users_by_pk(pk_columns: { id: $id }, _set: { name: $name }) {
      id
      name
      created_at
    }
  }
`

@clientでGraphQLサーバーではなく、クライアントのキャッシュから取得できる

export const GET_USERS_LOCAL = gql`
  query GetUsers {
    users(order_by: { created_at: desc }) @client {
      id
      name
      created_at
    }
  }
`

$id 引数でidを渡してデータを取得する

export const GET_USERBY_ID = gql`
  query GetUserById($id: uuid!) {
    users_by_pk(id: $id) {
      id
      name
      created_at
    }
  }
`

CreateUser($name: String!)
$name: クエリの引数を変数化
String: 変数の型定義
!: 必須

export const CREATE_USER = gql`
  mutation CreateUser($name: String!) {
    insert_users_one(object: { name: $name }) {
      id
      name
      created_at
    }
  }
`

型の自動生成

yarn gen-types

キャッシュを使ってグローバルなステートの管理を実現。

Queryを発行し、GraphQLサーバからデータを取得
Apollo Clientは取得したデータを自動的にキャッシュに保存する
そのためクライアントサイドでは他のコンポーネントから
同じクエリで@clientをつけるだけで保存されているキャッシュへアクセスできる

ローカルのステートマネジメント
makeVar(userReacticeVar)を標準で提供している

const createApolloClient = () => {
  return new ApolloClient({
    // window === 'undefined' ブラウザではない = サーバーサイドで処理が走る場合
    ssrMode: typeof window === 'undefined',
    link: new HttpLink({
      uri: '',
    }),

    cache: new InMemoryCache(),
  })
}

export const initializeApollo = (initialState = null) => {
  const _apolloClient = apolloClient ?? createApolloClient()
  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient

  return _apolloClient
}
import { ApolloProvider } from '@apollo/client'
import { AppProps } from 'next/app'
import { initializeApollo } from '../lib/apolloClient'
import '../styles/globals.css'

function MyApp({ Component, pageProps }: AppProps) {
  const client = initializeApollo()
  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  )
}

export default MyApp
import Link from 'next/link'
import { useQuery } from '@apollo/client'
import { GET_USERS } from '../queries/queries'
import { GetUsersQuery } from '../types/generated/graphql'
import { Layout } from '../components/Layout'

const FetchMain = () => {
  // useQueryの引数には実行したいクエリのコマンドを入れる
  // ジェネリクスでGetUsersQueryのデータ型定義
  const { data, error } = useQuery<GetUsersQuery>(GET_USERS, {
    // useQueryが実行される度にGraphQLサーバに毎回アクセスする。最新のデータを取得しキャッシュに格納してくれる。
    fetchPolicy: 'network-only',
    // 通信中はキャッシュにあるデータを表示し最新のデータが取得できたら上書きする
    // fetchPolicy: 'cache-and-network',
    // 初回のデータ取得以降はキャッシュのデータを表示する。デフォルトではcache-firstが利用される。
    // fetchPolicy: 'cache-first',
    // キャッシュを利用しない
    // fetchPolicy: 'no-cache',
  })
  if (error)
    return (
      <Layout title="Hasura fetchPolicy">
        <p>Error: {error.message}</p>
      </Layout>
    )
  return (
    <Layout title="Hasura fetchPolicy">
      <p className="mb-6 font-bold">Hasura main page</p>
      {data?.users.map((user) => {
        return (
          <p className="my-1" key={user.id}>
            {user.name}
          </p>
        )
      })}
      <Link href="/hasura-sub">
        <a className="mt-6">Next</a>
      </Link>
    </Layout>
  )
}

export default FetchMain

gRPCあれこれ

RPCとは

・Remote Procedure Call(遠隔手続き呼び出し)
 あるサービスから別のサービスの
 アプリケーションの処理(サブルーチン・クラス・関数など)を呼び出す技術。
 RPCを使うことで、違うアプリケーションのロジックを
 あたかも自分のアプリケーションの中に実装されているかのように扱うことができる。
 RPCでよく使われる技術には、gRPC・JSON-RPC・SOAPApache Thriftなどがある。

gRPCとは

Googleが開発した高速なAPI通信とスキーマ駆動開発を実現するRPCフレームワーク

REST

・RESTはリソース思考を強く打ち出している
 リソース志向はリソース(オブジェクト)を中心に考え、
 これに対してHTTPメソッドで操作していく考え。
 RPCではメソッドの呼び出しが基点となり、
 データはあくまでその副産物であるため考え方としては逆になる。
 RESTは規格が厳格に決められたものではなく、
 シンプルでスケーラブルなAPIを作るための設計原則。
 そのため、RESTでは原則に沿って自分で仕様を決めて実装することが求められる。
 一方でRPCフレームワークは、
 規格や仕様に沿って実装されたライブラリやフレームワークとして提供される。

特徴

・HTTP/2による高速な通信
・Protocol Buffers
・柔軟なストリーミング形式

HTTP/2

gRPCではHTTP/2のプロトコル上で通信が行われる。
HTTP/2では通信時にデータがテキストではなく、
バイナリにシリアライズされて送られる。
そのため小さな容量で転送でき、
ネットワーク内のリソースをより効率的に使用することができる。
またHTTP/2では一つのコネクションで
複数のリクエスト・レスポンスをやりとりできる。
そのためgRPCでもコネクションは常時張られっぱなしの状態になる。
リクエストのたびに接続と切断を行う必要がなく、
またヘッダーを都度おくる必要がないためより効率的な通信になる。

Protocol Buffers

gRPCではProtocol Buffersのフォーマットにシリアライズしてデータをやり取りする。
Protocol BuffersもgRPCと同様にGoogleが開発。
一番の特徴は、.protoファイルというIDL(インタフェース記述言語)。
.protoファイルを書いて、コンパイラを実行すると
任意の言語のサーバ・クライアント用コードを自動生成する。
そのため自分でAPIインタフェースを実装したり、
シリアライズされたデータのエンコード・デコード処理を書く必要がない。
gRPCではスキーマが最初に書かれるため
.protoファイルを見ればAPIの仕様は常に明確。

柔軟なストリーミング形式

・シンプルなRPC
・サーバーストリーミングRPC
・クライアントストリーミングRPC
・双方向ストリーミングRPC

protoファイル
// バージョン
syntax = "proto3";

// パッケージ定義
package myapp;

// サービス定義
service AddressBookService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

message SearchRequest {
  string name = 1;
}

message SearchResponse {
  Person person = 1;
}

message Person {
  int32 id = 1;
  string name = 2;
  string email = 3;
  // リスト(配列)
  repeated PhoneNumber phone_number = 4;

  // 列挙型(先頭は必ず0)
  enum PhoneType {
    UNKNOWN = 0;
    MOBILE = 1;
    HOME = 2;
    WORK = 3;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType phone_type = 2;
  }
}

※メッセージ型は各言語のコードに自動生成された際に、
構造体やクラスとして書き出される。

・マップ(連想配列
key_typeにキーとなる型を、value_typeに値となる型を定義

map<key_type, value_type> map_field = N;

// 文字列をキーとしたAddressBookを格納させたい場合
message Person {
  int32 id = 1;
  map<string, AddressBook> address_books = 2;
}

・oneof
フィールドの先頭にoneofと付与することで、
複数の型の中からどれかひとつという定義を行える。
※oneofはrepeatedにすることができず、
oneofの中でもrepeatedを使うことはできない。

message GreetingCard {
  int32 id = 1;
  oneof message {
    string text = 2;
    Image image = 3;
    Video video = 4;
  }
}

message Photo {
  ...
}

message Video {
  ...
}

・日時
google.protobuf.Durationは期間を表す型

import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";

message Person {
  int32 id = 1;
  google.protobuf.Duration apart_duration = 2;
google.protobuf.Timestamp create_time = 3;
google.protobuf.Timestamp update_time = 4;
}

※特に値を返す必要がない場合

import "google/protobuf/empty.proto";

Service AddressBookService {
  rpc Delete(DeleteRequest) google.protobuf.Empty
}
ストリーミングRPCの場合のメソッド定義
// サーバストリーミングRPC
rpc SearchHello (SearchRequest) returns (stream SearchResponse);

// クライアントストリーミングRPC
rpc SearchHello (stream SearchRequest) returns (SearchResponse);

// 双方向ストリーミングRPC
rpc SearchHello (stream SearchRequest) returns (stream SearchResponse);
proto作成
mkdir -p proto
touch proto/hello.proto
syntax = "proto3";
package hello;
option go_package = "./pb";

import "google/protobuf/timestamp.proto";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

message Greet {
  enum GreetingType {
    UNKNOWN = 0;
    GOODMORNING = 1;
    HI = 2;
    HELLO = 3;
  }

  string name = 1;
  Greet greet = 2;
  float smile_score = 3;
  google.protobuf.Timestamp create_time = 15;
}

message Report {
  message GreetCount {
    Greet.GreetingType greet = 1;
    int32 count = 2;
  }
  repeated GreetCount greet_counts = 1;
}

message HelloRequest {
  Greet.GreetingType greet = 1;
}

message HelloResponse {
  Greet greet = 1;
}
コード生成

必要なプラグインのインストール

Quick start | Go | gRPC

mkdir -p proto/go
protoc --go_out=proto/go --go-grpc_out=proto/go proto/*.proto
grpc_cli ls localhost:3000 パッケージ名.サービス名 -l

Rustあれこれ

Rustとは

・2015年に正式リリース
・実行速度が速い
・静的型付け言語
・メモリ安全性の保証
機械語に直接コンパイルされる
GCガベージコレクションをもたない)

JavaPythonなどは独自の仮想マシンを持つ
仮想マシンを入れるメリット
 さまざまな環境下でプログラミング言語を実行しやすくなる点
インタプリタコンパイラを介すと仮想マシン用の言語を生成する。
仮想マシンはソフトウェア上に実装されているため、速度面で不利になる。
C言語C++、Rustなどはコンパイル後の最終結果は機械語になる。
仮想マシンを介さずに実行できるため仮想マシンに起因する速度低下が起きない。
また、さまざまな環境下で実行できるよう
GNU Compiler Collection(GCC)やLLVMといったコンパイラ
環境に応じた機械語を生成するようになっている。

インストール

doc.rust-lang.org

・パッケージ作成

cargo new <パッケージ名>

・ビルド

cargo build

※バイナリファイルはtargetファイルへ格納される

・ビルド+実行

cargo run

コンパイルによるコードチェック

cargo check

モジュール作成

// publicで公開
pub fn run() {
  println!("Here is vars module!");
}
mod vars;

fn main() {
  vars::run();
}
cargo run
Here is vars module!
サブモジュール
pub fn func_a() {
  println!("func a called!");
}
pub fn func_b() {
  println!("func b called!");
}
pub mod sub_a.rs;
pub mod sub_b.rs;

pub fn run() {
  println!("Here is vars module!");
  // 関数の呼び出し
  sub_a::func_a();
  sub_b::func_b():
}
mod vars;

fn main() {
  vars::run();
  vars::sub_a::func_a();
  vars::sub_b::func_b();
}
cargo run
Here is vars module!
func a called!
func b called!
func a called!
func b called!

メモリについて

Heap(ヒープ)
 可変長データ(String, Vector)、容量が大きく可変長なデータを扱うことができるが
 メモリへのアクセスはスタックに比べると遅くなる
Stack(スタック)
 サイズが決まった変数や配列、容量は限られているが高速なメモリアクセスができる
Static(静的領域)
 const、文字列リテラルの実体
Text(コード)
 バイナリコードが保存される領域

Last In Fast Out (Stack)高速
スタックは変数や配列がスタック上に積まれていき、
取り出すとき(pop)は一番最後に積まれたものが最初に取り出される
popで取り出してメモリを解放するときに
どれだけのデータ量を取り出していいかというのが事前にわかっていないと
データの整合性が取れなくなるためスタックに格納されるデータは
あらかじめデータサイズが決まっている必要がある

Mutable・Immutable
pub mod sub_a;
pub mod sub_b;

const MAX_POINTS: u32 = 100_000;

pub fn run() {
  println!("Here is vars module!");
  // sub_a::func_a();
  // sub_b::func_b();
  let mut x = 5;
  println!("The value of x is: {}", x);
  x = 6;
  println!("The value of x is: {}", x);
  let _i1 = 3;
  let _f1 = 0.1;

  println!("{}", usize::BITS);
  println!("Memory address of const is: {:p}", &MAX_POINTS);

  let i2: i64 = 1;
  let i3: i64 = 2;
  println!("Stack address of i2 is: {:p}", &i2);
  println!("Stack address of i3 is: {:p}", &i3);

  let y = 5;
  println!("Stack address of y is: {:p}", &y);
  let y = y + 1;
  println!("Stack address of y is: {:p}", &y);
  let y = y * 2;
  println!("Stack address of y is: {:p}", &y);
  println!("The value of y is: {}", y);
  {
    let y = 0;
    println!("The value of y is: {}", y);
  }
  println!("The value of y is: {}", y);

  let t1 = (500, 6.4, "dummy");
  let (_x, _y, _z) = t1;
  println!("The value of t1 is: {} {} {}", t1.0, t1.1, t1.2);

  let mut t2 = ((0, 1), (2, 3));
  let ((ref mut x1_ptr, ref mut y1_ptr), _) = t2;
  *x1_ptr = 5;
  *y1_ptr = -5;
  println!("{:?}", t2);

  let a1 = [1, 2, 3, 4, 5];
  let a2 = [0; 10];
  println!("{:?} {:?} {} {}", a1, a2, a1[2], a1[3]);

  let s1 = "helloこんにちは挨拶"; //26bytes
  let s2 = "hello";
  println!("Stack address of s1 is: {:p}", &s1);
  println!("Stack address of s2 is: {:p}", &s2);
  println!("Stack memory address of s1 is: {:?}", &s1.as_ptr());
  println!("Stack memory address of s2 is: {:?}", &s2.as_ptr());
  println!("Len of s1 is: {}", &s1.len());
  println!("Len of s2 is: {}", &s2.len());

  let mut s1 = String::from("hello");
  let mut s2 = String::from("helloworld");
  println!("Stack address of s1 is: {:p}", &s1);
  println!("Stack address of s2 is: {:p}", &s2);
  println!("Stack memory address of s1 is: {:?}", s1.as_ptr());
  println!("Stack memory address of s2 is: {:?}", s2.as_ptr());
  println!("Len of s1 is: {}", s1.len());
  println!("Len of s2 is: {}", s2.len());
  println!("Capacity of s1 is: {}", s1.capacity());
  println!("Capacity of s2 is: {}", s2.capacity());
  s1.push_str("_new1");
  s2.push_str("_new2");
  println!("{} {}", s1, s2);
}
Stack overflow・Vector
enum List {
  Node(i32, Box<List>),
  Nil,
}

pub fn run() {
  // let a1: [u8; 9000000] = [1; 9000000];

  let mut v1 = vec![1, 2, 3, 4];
  let v2 = vec![5, 6, 7, 8];
  let mut v3 = vec![9, 10];
  println!("Stack address of v1 is: {:p}", &v1);
  println!("Stack address of v2 is: {:p}", &v2);
  println!("Heap memory address of v1: {:?}", v1.as_ptr());
  println!("Len of v1 is: {}", v1.len());
  println!("Capacity of v1 is: {}", v1.capacity());
  v1.insert(1, 10);
  println!("{:?}", v1);
  v1.remove(0);
  println!("{:?}", v1);
  v1.append(&mut v3);
  println!("{:?}", v1);
  println!("{:?}", v3);

  let t1: (i64, String) = (10, String::from("hello"));
  println!("Stack address of tuple data is: {:p}", &t1);
  println!("Heap memory address of t1: {:?}", t1.1.as_ptr());
  println!("Len of v1 is: {}", t1.1.len());
  println!("Capacity of v1 is: {}", t1.1.capacity());
  let mut b1 = Box::new(t1);
  (*b1).1 += "world";
  println!("{} {}", b1.0, b1.1);
  println!("Stack address of box pointer is: {:p}", &b1);
  println!("Heap address of tuple data is: {:p}", b1);
}
所有権・参照・借用
pub fn run() {
  let s1 = String::from("hello");
  let s2 = s1;
  println!("{}", s2);

  let i1 = 1;
  let i2 = i1;
  println!("{} {}", i1, i2);
  println!("Stack address of i1 is: {:p}", &i1);
  println!("Stack address of i2 is: {:p}", &i2);

  let sl1 = "literal";
  let sl2 = sl1;
  println!("{} {}", sl1, sl2);
  println!("Stack address of sl1 is: {:p}", &sl1);
  println!("Stack address of sl2 is: {:p}", &sl2);

  let s3 = String::from("hello");
  let s4 = s3.clone();
  println!("Stack address of s3 is: {:p}", &s3);
  println!("Stack address of s4 is: {:p}", &s4);
  println!("Heap memory address of hello: {:p}", &s3.as_ptr());
  println!("Heap memory address of hello: {:p}", &s4.as_ptr());
  println!("{} {}", s3, s4);

  let s5 = String::from("hello");
  println!("Stack address of s5: {:p}", &s5);
  println!("Heap address of hello: {:?}", s5.as_ptr());
  println!("Len is: {}", s5.len());
  println!("Cap is: {}", s5.capacity());
  take_ownership(s5);
  // println!("{}", s5);
  let s6 = String::from("hello");
  println!("Stack address of s6: {:p}", &s6);
  println!("Heap address of hello: {:?}", s6.as_ptr());
  println!("Len is: {}", s6.len());
  let s7 = take_giveback_ownership(s6);
  println!("Stack address of s7: {:p}", &s7);
  println!("Heap address of hello: {:?}", s7.as_ptr());
  println!("Len is: {}", s7.len());

  let s8 = String::from("hello");
  let len = calculate_length(&s8);
  println!("The length of '{}' is {}", s8, len);

  let mut s9 = String::from("hello");
  change(&mut s9);
  println!("{}", s9);

  let s10 = String::from("hello");
  let r1 = &s10;
  let r2 = &s10;
  println!("{} {} {}", s10, r1, r2);

  // let mut s10 = String::from("hello");
  // let r1 = &s10;
  // let r2 = &mut s10;
  // println!("{} {}", r1, r2);

  let mut s11 = String::from("hello");
  let r1 = &mut s11;
  println!("{}", r1);
  println!("{}", s11);
  let mut s12 = String::from("hello");
  let r1 = &s12;
  let r2 = &s12;
  println!("{} and {}", r1, r2);
  let r3 = &mut s12;
  *r3 = String::from("hello_updated");
  println!("{}", s12);
}
fn take_ownership(s: String) {
  println!("Stack address of s: {:p}", &s);
  println!("Heap address of hello: {:?}", s.as_ptr());
  println!("Len is: {}", s.len());
  println!("Cap is: {}", s.capacity());
  println!("{}", s);
}
fn take_giveback_ownership(s: String) -> String {
  s
}
fn calculate_length(s: &String) -> usize {
  s.len()
}
fn change(s: &mut String) {
  s.push_str("_world");
}
Life time・Dangling pointer
pub fn run() {
  let st1 = String::from("x");
  let st2 = String::from("y");
  let res1 = get_longest(&st1, &st2);
  println!("{}", res1);

  let st3 = String::from("x");
  let res2;
  {
    let st4 = String::from("y");
    res2 = get_longest(&st3, &st4);
    println!("{}", res2);
  }
}
fn get_longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  if x.len() > y.len() {
    x
  } else {
    y
  }
}
// fn dummy1<'a>() -> &'a str {
//   let s = String::from("demo");
//   &s
// }
// fn dummy2<'a>() -> &'a i32 {
//   let x = 10;
//   &x
// }
fn dummy3() -> String {
  let s = String::from("demo");
  s
}
Generics
struct Point<T> {
  x: T,
  y: T,
}
struct PointAnother<T, U> {
  x: T,
  y: U,
}
impl<T, U> PointAnother<T, U> {
  fn mixup<V, W>(self, other: PointAnother<V, W>) -> PointAnother<T, W> {
    PointAnother {
      x: self.x,
      y: other.y,
    }
  }
}
pub fn run() {
  let number_list = vec![34, 50, 25, 100, 65];
  // let mut largest = number_list[0];
  // for number in number_list {
  //   if number > largest {
  //     largest = number;
  //   }
  // }
  // println!("The largest is {}", largest);
  // println!("{}", largest_i32(number_list));
  let char_list = vec!['a', 'b', 'c', 'd'];
  println!("{}", largest(char_list));
  println!("{}", largest(number_list));
  let p1 = Point { x: 1, y: 2 };
  let p2 = Point { x: 1.0, y: 2.0 };
  let p3 = PointAnother { x: 5, y: 10.4 };
  let p4 = PointAnother { x: "Rust", y: 'a' };
  let p5 = p3.mixup(p4);
  println!("{} {}", p5.x, p5.y);
}
fn largest_i32(list: Vec<i32>) -> i32 {
  let mut largest = list[0];
  for item in list {
    if item > largest {
      largest = item;
    }
  }
  largest
}
fn largest<T: PartialOrd + Copy>(list: Vec<T>) -> T {
  let mut largest = list[0];
  for item in list {
    if item > largest {
      largest = item;
    }
  }
  largest
}
Struct・Enum・Pattern matching
#[derive(Debug)]
struct User {
  username: String,
  email: String,
  sign_in_count: u64,
  active: bool,
}
#[derive(Debug)]
struct Rectangle {
  width: u32,
  height: u32,
}
impl Rectangle {
  fn create(width: u32, height: u32) -> Self {
    Self { width, height }
  }
  fn area(&self) {
    println!("{}", self.width * self.height);
  }
}

pub fn run() {
  let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someoneusername123"),
    active: true,
    sign_in_count: 1,
  };
  let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someoneusername123"),
    active: true,
    sign_in_count: 1,
  };
  user1.email = String::from("anotheremail@example.com");
  println!("{:#?}", user1);
  let user2 = build_user(String::from("user2@xxx.com"), String::from("user2"));
  println!("{:#?}", user2);

  let rect = Rectangle::create(20, 20);
  println!("{:#?}", rect);
  rect.area();
  println!("{:#?}", rect);
}
fn build_user(email: String, username: String) -> User {
  User {
    email,
    username,
    active: true,
    sign_in_count: 1,
  }
}
Traits
trait Fruits {
  fn price(&self) -> u32;
}
struct Apple;

impl Fruits for Apple {
  fn price(&self) -> u32 {
    10
  }
}

struct Banana;
impl Fruits for Banana {
  fn price(&self) -> u32 {
    5
  }
}
trait Summary {
  fn summarize(&self) -> String {
    String::from("(Read more...)")
  }
}
trait Message {
  fn message(&self) -> String {
    String::from("Message")
  }
}
struct NewsArticle {
  headline: String,
  location: String,
  author: String,
  content: String,
}
impl Summary for NewsArticle {
  // fn summarize(&self) -> String {
  //   format!("{}, by {} ({})", self.headline, self.author, self.location)
  // }
}
impl Message for NewsArticle {}
struct Tweet {
  username: String,
  content: String,
  reply: bool,
  retweet: bool,
}
impl Summary for Tweet {
  fn summarize(&self) -> String {
    format!("{}: {}", self.username, self.content)
  }
}

pub fn run() {
  let apple = Apple {};
  let banana = Banana {};
  get_price(apple);
  get_price(banana);
  let tweet = Tweet {
    username: String::from("horse_ebooks"),
    content: String::from("of course, as you probably already know, people"),
    reply: false,
    retweet: false,
  };
  println!("1 new tweet: {}", tweet.summarize());
  let article = NewsArticle {
    headline: String::from("Penguins win the Stanley Cup Championship!"),
    location: String::from("Pittsburgh, PA, USA"),
    author: String::from("Iceburgh"),
    content: String::from(
      "The Pittsburgh Penguins once again are the best hockey team in the NHL.",
    ),
  };
  println!("{}", article.summarize());
  notify(&article);
  notify_another(&article);
}
fn get_price<T: Fruits>(fruits: T) {
  println!("price is: {}", fruits.price());
}
fn notify(item: &impl Summary) {
  println!("Breaking news! {}", item.summarize());
}
fn notify_another(item: &(impl Summary + Message)) {
  println!("Breaking news! {}", item.summarize());
  println!("Message! {}", item.message());
}
error handling
pub fn run() {
  let res1 = division_option(5.0, 0.0);
  match res1 {
    Some(x) => println!("Result: {:.3}", x),
    None => println!("Not allowed !!"),
  }
  let res2 = division_result(5.0, 1.0);
  match res2 {
    Ok(x) => println!("Result: {:.3}", x),
    Err(e) => println!("{}", e),
  }
  let a = [0, 1];
  let res3 = sum(&a);
  match res3 {
    Some(x) => println!("Total is: {}", x),
    None => println!("Out of index !!"),
  }
}
fn division_option(x: f64, y: f64) -> Option<f64> {
  if y == 0.0 {
    None
  } else {
    Some(x / y)
  }
}
fn division_result(x: f64, y: f64) -> Result<f64, String> {
  if y == 0.0 {
    Err(String::from("Not allowed !!"))
  } else {
    Ok(x / y)
  }
}
fn sum(a: &[i32]) -> Option<i32> {
  let a0 = a.get(0)?;
  let a1 = a.get(1)?;
  let a2 = a.get(2)?;
  Some(a0 + a1 + a2)
}
Unit test
struct Rectangle {
  width: u32,
  height: u32,
}
impl Rectangle {
  fn compare_area(&self, other: &Rectangle) -> bool {
    self.width * self.height > other.width * other.height
  }
}

fn double_value(a: i32) -> i32 {
  a * 2
}
fn greeting(name: &str) -> String {
  format!("Hello {} san", name)
}

#[cfg(test)]
mod tests {
  use super::*;
  #[test]
  fn test_a_is_larger() {
    let a = Rectangle {
      width: 5,
      height: 5,
    };
    let b = Rectangle {
      width: 3,
      height: 3,
    };
    assert!(a.compare_area(&b));
  }
  #[test]
  fn test_a_is_smaller() {
    let a = Rectangle {
      width: 3,
      height: 3,
    };
    let b = Rectangle {
      width: 5,
      height: 5,
    };
    assert!(!(a.compare_area(&b)));
  }
  #[test]
  fn test_double() {
    assert_eq!(6, double_value(3));
  }
  #[test]
  fn test_contains_name() {
    let res = greeting("rust");
    assert!(res.contains("rust"));
  }
}

Lambdaあれこれ

ファンアウト

ファンアウトパターンとは

処理を呼び出すプロセスから直接各処理を呼び出すのではなく、
間にノーティフィケーション(通知)コンポーネント
キューイングコンポーネントを入れることで
非同期かつ並列に処理が可能になる。
処理を呼び出すプロセスは、通知先への通知の後に処理を続行できる。
また、通知先の処理については知っている必要がないため処理を増やしたい場合も
通知コンポーネントへの通知先登録を増やせば対応できる。
また、キューごとに処理を割り当てて動作させれば並列に処理を実行できる。

通知コンポーネントSNS
キューイングコンポーネント:SQS

SQS(Amazon Simple Queue Service)

AWSが提供するメッセージキューイングサービス。
コンポーネント同士を分離し、スケールアウトをサポート。
LambdaのサポートによりSQSのエンキュー(キューにデータを入れること)をトリガーにLambdaを起動し、Lambdaが正常に終了すればメッセージは自動的に削除してくれる。
※トリガーとして対応しているキューは標準キューのみ(FIFOキューはポーリングが必要)

SNSAmazon Simple Notification Service)

AWSが提供するPub / Subメッセージングサービス
メッセージはプッシュ型で、トピックをサブスクライブできるのは以下
・Lambda、SQS、http/https、Eメール、SMS、アプリケーション

API Gateway

AWSが提供するバックエンドシステムへのAPIを作成するためのサービス
バックエンドはLambdaだけでなく、他のAWSサービスやHTTP(AWS以外でもパブリックに公開されているエンドポイントが存在するサービス)、Mockが選択できる。
LambdaをバックエンドにAPIを開発する際にはリクエストとレスポンスのマッピングを理解する必要がある。
API Gatewayでリクエストを受けてLambdaにどう渡すか、Lambdaでの処理結果をAPI Gatewayで利用するにあたりどう渡すかなど
SAMを使ってデプロイを行う場合、API GatewayとLambdaの統合にはLambdaプロキシ統合が利用される
Lambdaプロキシ統合ではカスタムのマッピングを利用することもできますが、それ以外ではフォーマットに従うことでマッピングが完了する
Lambda内でエラーハンドリング する際、エラーを返すとAPI Gatewayからクライアントへのレスポンスは常にMalformed Lambda proxy responseとなり、ステータスコードは502になる
本当に予期しないエラー発生時以外はLambdaからAPI Gatewayにエラーを返さないように注意する

DynamoDB

AWSが提供するNoSQLデータベースサービス
読み込み・書き込みのスペックを指定(購入)でき、スケーラビリティに優れている

RDSではなく、DynamoDBを使う場面
・DB同時接続の問題
Lambdaへのリクエストが多い時、同時実行制限数までLambdaのコンテナが作成、起動される
それから個別にDBにアクセスする際、DBの性能要件(CPU数、メモリー数)は接続に比例して大きくなっていく
DBが無限にスケールアップできるなら別

VPC内のリソースへのアクセス
RDSはVPC内に作成する一方で、LambdaはデフォルトではVPC内のリソースにアクセスできない
これを解決するためにENIをセットアップするのに10 ~ 30秒かかる
これらの制約からLambdaから接続するデータベースはDynamoDBが推奨される

useQueryについて

react-query

・サーバーからのデータ取得回数を減らす(fetch)
 (キャッシュに格納する)
・不要な再レンダリングを無くす

従来のstate管理

●State management(useState, redux store)
・Server data(REST APIから取得したデータ)
・React app state(ローカルのstate、isOpenなどのbooleanなど)
→Reduxのstoreに全てまとめて状態管理されていた

●Reactのアプリケーション性能を高める、状態管理をより分かりやすくするためには Server dataの取り扱いとReact app stateの取り扱いを分けて考える必要がある
React app stateは、従来のReduxやuseContextを使う
Server dataは、キャッシュ機構を活用することができるreact-queryを使用する
キャッシュには他のコンポーネントから自由にアクセスすることができるため、
ReduxやuseContextを使わずサーバーから取得したデータは
react-queryのキャッシュ機構を使って他のコンポーネントから自由にアクセスできる

キャッシュのメカニズムをうまく活用することで
・他のコンポーネントからアクセス可能
・Fetch回数の最適化

●Redux(useContext + useState)
コンポーネントAからfetchでアクセスし取得したデータを
dispatchを使用してstoreに保存
こうすることでコンポーネントBやコンポーネントCから
取得したデータに自由にアクセスできるようになっていた

●React-query
コンポーネントAからREST APIへuseQueryを使用してアクセスする
データを新規作成・更新、削除したりする場合は、useMutationを使用する
取得したデータは自動的にキャッシュに保存してくれる
キャッシュに保存されたデータにコンポーネントBやコンポーネントCなど
他のコンポーネントから自由にアクセスが可能になる
(ReduxやuseContextなどと同じようにグローバルに管理できる)

利点

1、Fetch回数の最適化
従来:
あるコンポーネントにuseEffectを導入
useEffectの中でREST APIにfetchするための処理を記述
例えば、第二引数を空の[ ]とすることで
コンポーネントがマウントされる度にfetchが実行されサーバから最新のデータを取得
別のコンポーネントへページを遷移して戻ってきたとき
再度マウントされるためfetchが毎回実行される

useQuery:
あるコンポーネントにuseQueryを導入
そのコンポーネントがマウントされた時にREST APIにアクセスし
取得したデータをキャッシュの機構に保存
react-queryではstaleTimeというパラメータを設定することができる
これによりfetchの回数を調整することができる
例えばstaleTimeを10秒(staleTime: 10000[ms])に設定した場合、
マウントしてから10秒間は、保存したキャッシュを最新のもの(freshなcache)と見なすようになる
10秒後にキャッシュの内容をstale(古いcache)と見なすようになる
そのため、今回の場合だと10秒間に再度マウントしたとしても
useQueryを実行したとしてもサーバへのfetchを行わないようにすることができる
また、取得するデータのエンドポイント毎にstaleTimeを設定することができる
サーバのデータがほとんど変わらないデータの場合、毎回fetchをするのではなく、
staleTimeを長めにとっておくことで無駄なfetchをなくすことが出来る

2、Better UX by Stale While Revalidation
従来:
あるコンポーネントにuseEffectを導入
マウント時にfetchの処理が実行されREST APIからデータを取得
非同期通信のため、データを取得するまでに少し時間がかかる
データ取得できるまでは何も表示されない
コンポーネントがマウントされる度にloadingのstateがユーザに見えてしまう形になる

useQuery:
あるコンポーネントにuseQueryを導入
初回はキャッシュが存在しないのでREST APIからデータを取得するため
従来と同じようにloadingの画面が表示される
2回目以降マウントされた際は既にキャッシュに何らかの値が保存されているため
キャッシュからデータをすぐに返してくれる
キャッシュのデータが古い可能性もあるがとりあえずキャッシュにあるデータを返してくれる
返している間に最新のデータを評価していく形になる
データが取得でき次第、最新のデータでstale dataを書き換えてくれる

3、コード量を少なくして処理をシンプルにすることが出来る
従来:
・fetch処理
・通信状況処理(データを取得できたか通信が終わったのか、エラーが発生したのかなど)
・グローバルState化(dispatch、useContextを使うなど)

import { useState, useEffect } from "react";
import { useStateContext } from "../context/StateProvider";
import axios from "axios";

export const useClassicFetch = () => {
  const { tasks, setTasks } = useStateContext();
  const { isLoading, setLoading } = useState(false);
  const { isError, setError } = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setError(false);
      setLoading(false);
      try {
        const res = await axios('API');
        setTasks(res.data);
      } catch (error) {
        setError(true);
      }
      setLoading(false);
    };
    fetchData();
  }, [setTasks]);

  return { tasks, isLoading, isError };
};

取得したデータをuseContextを使って
グローバル化する必要があるためProviderの設定が必要

import { useContext, useState, createContext } from "react";
import { Tasks } from "../types/types";

interface StateContextType {
  tasks: Task[] | null;
  dark: boolean;
  setTasks: React.Dispatch<React.SetStateAction<Task[] | null>>;
  setDark: React.Dispatch<React.SetStateAction<boolean>>;
}

const StateContext = createContext({} as StateContextType);

export const StateProvider: React.FC = ({ children }) => {
  const [ tasks, setTasks ] = useState<Task[] | null>(null);
  const [ dark, setDark ] = useState(false);

  return (
    <StateContext.Provider value={{ tasks, setTasks, dark, setDark }}>
      {children}
    </StateContext.Provider>
  )
}
export const useStateContext = (): StateContextType => useContext(StateContext)

useQuery:
下記で上記のようなことと同じようなことが出来る

const { isLoading, error, data } = useQuery('repoDate', () =>
  fetch('API').then(res =>
    res.json()
  )
)

Next.jsを使用する場合は、SWR(Vercel)を採用する方が良いかもしれない。
APOLLOクライアントは標準でキャッシュ機構が備わっている。

カスタムフック

カスタムフックとは

・hooksの各機能を使用
コンポーネントからロジックを分離
・使い回し、テスト容易、見通しが良くなる
・自由に作成できる

カスタムフックなしで実装

$ mkdir ./src/components
$ mkdir -p ./src/types/api
$ touch ./src/components/UserCard.tsx
$ touch ./scr/types/userProfile.ts
$ touch ./scr/types/api/user.ts
export type User = {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
};
export type UserProfile = {
  id: number;
  name: string;
  email: string;
  address: string;
};
import { VFC } from "react";
import { UserProfile } from "../types/userProfile";

type Props = {
  user: UserProfile;
};

export const UserCard: VFC<Props> = (props) => {
  const { user } = props;

  const style = {
    border: "solid 1px #ccc",
    borderRadius: "8px",
    padding: "0 16px",
    margin: "8px"
  };

  return (
    <div style={style}>
      <dl>
        <dt>名前</dt>
        <dd>{user.name}</dd>
        <dt>メール</dt>
        <dd>{user.email}</dd>
        <dt>住所</dt>
        <dd>{user.address}</dd>
      </dl>
    </div>
  );
};

・データ取得
・ローディング
・エラー

import "./styles.css";
import { UserCard } from "./components/UserCard";
import axios from "axios";
import { User } from "./types/api/user";
import { useState } from "react";
import { UserProfile } from "./types/userProfile";

export default function App() {
  const [userProfiles, setUserProfiles] = useState<Array<UserProfile>>([]);

  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  const onClickFetchUser = () => {
    setLoading(true);
    setError(false);

    axios
      .get<Array<User>>("https://jsonplaceholder.typicode.com/users")
      .then((res) => {
        const data = res.data.map((user) => ({
          id: user.id,
          name: `${user.name}(${user.username})`,
          email: user.email,
          address: `${user.address.city}${user.address.suite}${user.address.street}`
        }));
        setUserProfiles(data);
      })
      .catch(() => {
        setError(true);
      })
      .finally(() => {
        setLoading(false);
      });
  };

  return (
    <div className="App">
      <button onClick={onClickFetchUser}>データ取得</button>
      <br />
      {error ? (
        <p style={{ color: "red" }}>データの取得に失敗しました</p>
      ) : loading ? (
        <p>Loading...</p>
      ) : (
        <>
          {userProfiles.map((user) => (
            <UserCard key={user.id} user={user} />
          ))}
        </>
      )}
    </div>
  );
}

コンポーネントの肥大化。

カスタムフックを使用して実装

$ mkdir ./src/hooks
$ touch ./src/hooks/useAllUsers.ts
import axios from "axios";
import { useState } from "react";
import { UserProfile } from "../types/userProfile";
import { User } from "../types/api/user";

// 全ユーザを一覧取得するカスタムフック
export const useAllUsers = () => {
  const [userProfiles, setUserProfiles] = useState<Array<UserProfile>>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  const getUsers = () => {
    setLoading(true);
    setError(false);

    axios
      .get<Array<User>>("https://jsonplaceholder.typicode.com/users")
      .then((res) => {
        const data = res.data.map((user) => ({
          id: user.id,
          name: `${user.name}(${user.username})`,
          email: user.email,
          address: `${user.address.city}${user.address.suite}${user.address.street}`
        }));
        setUserProfiles(data);
      })
      .catch(() => {
        setError(true);
      })
      .finally(() => {
        setLoading(false);
      });
  };

  return { getUsers, userProfiles, loading, error };
};
import "./styles.css";
import { UserCard } from "./components/UserCard";
import { useAllUsers } from "./hooks/useAllUsers";

export default function App() {
  const { getUsers, userProfiles, loading, error } = useAllUsers();

  const onClickFetchUser = () => getUsers();

  return (
    <div className="App">
      <button onClick={onClickFetchUser}>データ取得</button>
      <br />
      {error ? (
        <p style={{ color: "red" }}>データの取得に失敗しました</p>
      ) : loading ? (
        <p>Loading...</p>
      ) : (
        <>
          {userProfiles.map((user) => (
            <UserCard key={user.id} user={user} />
          ))}
        </>
      )}
    </div>
  );
}

TypeScript 復習

基本的な型

$ npx create-react-app <プロジェクト名> --template typescript
/* eslint-disable @typescript-eslint/no-unused-vars */

/** TypeScriptの基本の型 */

// boolean
let bool: boolean = true;

// number
let num: number = 0;

// string
let str: string = "A";

// Array
let arr: Array<number> = [0, 1, 2];
let arr1: Array<string> = ["A", "B", "C"];
let arr2: number[] = [0, 1, 2];

// tuple
let tuple: [number, string] = [0, "A"];

// any
let any1: any = false;

// void 何も返却値がないことを表す
const funcA = () => {
  const test = "TEST";
};

// null
let null1: null = null;

// undefined
let undefined1: undefined = undefined;

// object
let obj1: object = {};
let obj2: {} = {};
let obj3: { id: number; name: string } = { id: 0, name: "AAA" };

引数の型指定

export const Practice1 = () => {
  const calcTotalFee = (num: number) => {
    const total = num * 1.1;
    console.log(total);
  };
  const onClickPractice = () => calcTotalFee(1000);

  return (
    <div>
      <p>練習問題:引数の型指定</p>
      <button onClick={onClickPractice}>練習問題1を実行</button>
    </div>
  );
};

引数の戻り値の型指定

export const Practice2 = () => {
  const calcTotalFee = (num: number): number => {
    const total = num * 1.1;
    return total;
  };
  const onClickPractice = () => calcTotalFee(1000);

  return (
    <div>
      <p>練習問題:引数の戻り値の型指定</p>
      <button onClick={onClickPractice}>練習問題2を実行</button>
    </div>
  );
};

変数の型指定

export const Practice3 = () => {
  const calcTotalFee = (num: number): number => {
    const total = num * 1.1;
    return total;
  };
  const onClickPractice = () => {
    let total: number = 0;
    total = calcTotalFee(1000);
    console.log(total);
  };

  return (
    <div>
      <p>練習問題:変数の型指定</p>
      <button onClick={onClickPractice}>練習問題3を実行</button>
    </div>
  );
};

型指定しない場合

import axios from "axios";
import { useState } from "react";
import "./styles.css";
import { Todo } from "./Todo";

export default function App() {
  const [todos, setTodos] = useState<any>([]);

  const onClickFetchData = () => {
    axios.get("https://jsonplaceholder.typicode.com/todos").then((res) => {
      setTodos(res.data);
    });
  };

  return (
    <div className="App">
      <button onClick={onClickFetchData}>データ取得</button>
      {todos.map((todo) => (
        <Todo title={todo.title} userid={todo.userid} />
      ))}
    </div>
  );
}
export const Todo = (props) => {
  const { title, userid } = props;

  return <p>{`${title}(ユーザ:${userid})`}</p>;
};

エラーに気づけず意図した通りにデータを取得できない

型指定する場合

import axios from "axios";
import { useState } from "react";
import "./styles.css";
import { Todo } from "./Todo";

type TodoType = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

export default function App() {
  // 配列の型指定 Array<>
  const [todos, setTodos] = useState<Array<TodoType>>([]);

  const onClickFetchData = () => {
    axios
      .get<Array<TodoType>>("https://jsonplaceholder.typicode.com/todos")
      .then((res) => {
        setTodos(res.data);
      });
  };

  return (
    <div className="App">
      <button onClick={onClickFetchData}>データ取得</button>
      {todos.map((todo) => (
        <Todo title={todo.title} userid={todo.userId} />
      ))}
    </div>
  );
}

propsの型指定

import axios from "axios";
import { useState } from "react";
import "./styles.css";
import { Todo } from "./Todo";

type TodoType = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

export default function App() {
  const [todos, setTodos] = useState<Array<TodoType>>([]);

  const onClickFetchData = () => {
    axios
      .get<Array<TodoType>>("https://jsonplaceholder.typicode.com/todos")
      .then((res) => {
        setTodos(res.data);
      });
  };

  return (
    <div className="App">
      <button onClick={onClickFetchData}>データ取得</button>
      {todos.map((todo) => (
        <Todo
          key={todo.id}
          title={todo.title}
          userId={todo.userId}
          completed={todo.completed}
        />
      ))}
    </div>
  );
}
type TodoType = {
  userId: number;
  title: string;
  // 必須でない場合は?をつける
  completed?: boolean;
};

export const Todo = (props: TodoType) => {
  const { title, userId, completed = false } = props;
  const completeMark = completed ? "[完]" : "[未]";

  return <p>{`${completeMark} ${title}(ユーザ:${userId})`}</p>;
};

型をまとめる

$ mkdir ./src/types
$ touch ./src/types/todo.ts

共通して使用する型を定義

export type TodoType = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

Pickで使用したい型のみを指定する

import { TodoType } from "./types/todo";

export const Todo = (
  props: Pick<TodoType, "userId" | "title" | "completed">
) => {};

もしくは、Omitで必要ない型を指定する

import { TodoType } from "./types/todo";

export const Todo = (props: Omit<TodoType, "id">) => {};

定義した型はimportで読み込む

import axios from "axios";
import { useState } from "react";
import "./styles.css";
import { Todo } from "./Todo";
import { TodoType } from "./types/todo";

export default function App() {
  const [todos, setTodos] = useState<Array<TodoType>>([]);

  const onClickFetchData = () => {
    axios
      .get<Array<TodoType>>("https://jsonplaceholder.typicode.com/todos")
      .then((res) => {
        setTodos(res.data);
      });
  };

  return (
    <div className="App">
      <button onClick={onClickFetchData}>データ取得</button>
      {todos.map((todo) => (
        <Todo
          key={todo.id}
          title={todo.title}
          userId={todo.userId}
          completed={todo.completed}
        />
      ))}
    </div>
  );
}

関数コンポーネントの型定義

$ touch ./src/Text.tsx

Functional Component(FC)

import { FC } from "react";

type Props = {
  color: string;
  fontSize: string;
};

export const Text: FC<Props> = (props) => {
  const { color, fontSize } = props;

  return <p style={{ color, fontSize }}>テキストです</p>;
};

FCは暗黙的にchildrenを受け取るため、reactのバージョン18まではVFCを使用する
VFCは暗黙的にchildrenを含まないFunctional Componentの型指定

import { VFC } from "react";

type Props = {
  color: string;
  fontSize: string;
};

export const Text: VFC<Props> = (props) => {
  const { color, fontSize } = props;

  return <p style={{ color, fontSize }}>テキストです</p>;
};
import { VFC } from "react";
import { TodoType } from "./types/todo";

export const Todo: VFC<Omit<TodoType, "id">> = (props) => {
  const { title, userId, completed = false } = props;
  const completeMark = completed ? "[完]" : "[未]";

  return <p>{`${completeMark} ${title}(ユーザ:${userId})`}</p>;
};
import axios from "axios";
import { useState } from "react";
import "./styles.css";
import { Todo } from "./Todo";
import { TodoType } from "./types/todo";
import { Text } from "./Text";

export default function App() {
  const [todos, setTodos] = useState<Array<TodoType>>([]);

  const onClickFetchData = () => {
    axios
      .get<Array<TodoType>>("https://jsonplaceholder.typicode.com/todos")
      .then((res) => {
        setTodos(res.data);
      });
  };

  return (
    <div className="App">
      <Text color="red" fontSize="18px" />
      <button onClick={onClickFetchData}>データ取得</button>
      {todos.map((todo) => (
        <Todo
          key={todo.id}
          title={todo.title}
          userId={todo.userId}
          completed={todo.completed}
        />
      ))}
    </div>
  );
}

オプショナルチェイニング

$ touch ./src/types/user.ts
$ touch ./src/UserProfile.tsx
export type User = {
  name: string;
  hobbies?: Array<string>;
};
import { VFC } from "react";
import { User } from "./types/user";

type Props = {
  user: User;
};

export const UserProfile: VFC<Props> = (props) => {
  const { user } = props;

  return (
    <dl>
      <dt>名前</dt>
      <dd>{user.name}</dd>
      <dt>趣味</dt>
      // オプショナルチェイニング hobbiesに値が入っていなくてもエラーを吐かない
      <dd>{user.hobbies?.join(" / ")}</dd>
    </dl>
  );
};
import "./styles.css";
import { UserProfile } from "./UserProfile";
import { User } from "./types/user";

const user: User = {
  name: "Bob",
  hobbies: ["映画", "ゲーム"]
};

export default function App() {
  return (
    <div className="App">
      <UserProfile user={user} />
    </div>
  );
}