Engineering from Scratch

エンジニア目指してます

getServerSideProps

Next.js は getServerSideProps によって取得したデータを使って page の pre-render を行う。

import { InferGetServerSidePropsType } from "next";

export const getServerSideProps = async (context) => {
  const res = await fetch("https://api.github.com/users/octokit");
  const data = await res.json();
  console.log(data); ← serverで実行される
  return { props: { data } };
};

const HomePage = ({
  data,
}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
  console.log(data); ← ブラウザで実行される

  return <h1>Hello World</h1>;
};

export default HomePage;

getServerSideProps は以下のいずれかの値を返す必要がある。

  • props
    • {props: Object}の形で渡す必要がある
  • notFound
    • {notFound: true}を渡すと,page は 404 を返す
  • redirect
    • {redirect: {destination: string, permanent: boolean}}
    • redirect処理が走る
参考

nextjs.org

useImmer

useState と同様に,state と state を更新する関数のタプル型を返す hook。state を更新する関数は,immer の produce 関数か値を引数に取れる。

immer producer function

produce関数は以下のような interface となる。

produce(baseState, recipe: (draftState) => void): nextState

baseStateを,recipe関数によって変更した結果を返す。しかし,baseState は immutable。

import produce from "immer";

const baseState = [
  {
    title: "Learn TypeScript",
    done: true,
  },
  {
    title: "Try Immer",
    done: false,
  },
];

const nextState = produce(baseState, (draftState) => {
  draftState.push({ title: "Tweet about it", done: false });
  draftState[1].done = true;
});

Managing state with immer producer function

import { useImmer } from "use-immer";

export default function App() {
  const [person, updatePerson] = useImmer({
    name: "Michel",
    age: 33,
  });

  function updateName(name: string) {
    updatePerson((draft) => {
      draft.name = name;
    });
  }

  function becomeOlder() {
    updatePerson((draft) => {
      draft.age++;
    });
  }

  return (
    <div className="App">
      <h1>
        Hello {person.name} ({person.age})
      </h1>
      <input
        onChange={(e) => {
          updateName(e.target.value);
        }}
        value={person.name}
      />
      <br />
      <button onClick={becomeOlder}>Older</button>
    </div>
  );
}

updatePersonpersonstate を更新する関数を渡すことで,state が更新される。

Managing state as simple useState hook

import { MouseEvent } from "react";
import { useImmer } from "use-immer";

export default function App() {
  const [age, setAge] = useImmer(20);

  function birthDay(event: MouseEvent<HTMLButtonElement>) {
    setAge(age + 1);
  }

  return (
    <div>
      {age}
      <button onClick={birthDay}>It is my birthday</button>
    </div>
  );
}

useImmer の更新関数(setAge)に値を渡すと,useState と同じ挙動となる。

useImmerReducer

useReducerと同じことができる。

import { useImmerReducer } from "use-immer";

type State = {
  count: number;
};

const initialState: State = { count: 0 };

type Action = { type: "reset" | "increment" | "decrement" };

function reducer(draft: State, action: Action) {
  switch (action.type) {
    case "reset":
      return initialState;
    case "increment":
      return void draft.count++;
    case "decrement":
      return void draft.count--;
  }
}

export default function App() {
  const [state, dispatch] = useImmerReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}
参考

github.com

Updating Objects in State

Treat state as read-only

import { useState } from "react";

function App() {
  const [plan, setPlan] = useState({ x: 1, y: 2 });

  return (
    <>
      {plan.x}
      <button onClick={() => (plan.x += 1)}>a</button>
    </>
  );
}

export default App;

上記のコードでボタンを押下しても,画面に表示されるplan.xの値は変化しない。それは,state の setter 関数を使用しないと,React は object が変更されたことがわからないため。 state の setter 関数により,以下のことを React に伝える。

Using a single event handler for multiple fields

import { ChangeEvent, useState } from "react";

function App() {
  const [person, setPerson] = useState({
    firstName: "Barbara",
    lastName: "Hepworth",
    email: "bhepworth@sculpture.com",
  });

  function handleChange(e: ChangeEvent<HTMLInputElement>) {
    setPerson({
      ...person,
      [e.target.name]: e.target.value,
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          name="firstName"
          value={person.firstName}
          onChange={handleChange}
        />
      </label>
      <label>
        Last name:
        <input
          name="lastName"
          value={person.lastName}
          onChange={handleChange}
        />
      </label>
      <label>
        Email:
        <input name="email" value={person.email} onChange={handleChange} />
      </label>
      <p>
        {person.firstName} {person.lastName} ({person.email})
      </p>
    </>
  );
}

export default App;

[e.target.name]: e.target.valueとすることで,動的に key を指定できる。

Updating a nested object

import { ChangeEvent, useState } from "react";

type Event = ChangeEvent<HTMLInputElement>;

function App() {
  const [person, setPerson] = useState({
    name: "Niki de Saint Phalle",
    artwork: {
      title: "Blue Nana",
      city: "Hamburg",
      image: "https://i.imgur.com/Sd1AgUOm.jpg",
    },
  });

  function handleNameChange(e: Event) {
    setPerson({
      ...person,
      name: e.target.value,
    });
  }

  function handleTitleChange(e: Event) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value,
      },
    });
  }

  function handleCityChange(e: Event) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value,
      },
    });
  }

  function handleImageChange(e: Event) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value,
      },
    });
  }

  return (
    <>
      <label>
        Name:
        <input value={person.name} onChange={handleNameChange} />
      </label>
      <label>
        Title:
        <input value={person.artwork.title} onChange={handleTitleChange} />
      </label>
      <label>
        City:
        <input value={person.artwork.city} onChange={handleCityChange} />
      </label>
      <label>
        Image:
        <input value={person.artwork.image} onChange={handleImageChange} />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {" by "}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img src={person.artwork.image} alt={person.artwork.title} />
    </>
  );
}

export default App;

nest した object でも,コピーを使用して値を更新する必要がある。

Write concise update logic with Immer

import { ChangeEvent } from "react";
import { useImmer } from "use-immer";

type Event = ChangeEvent<HTMLInputElement>;

function App() {
  const [person, updatePerson] = useImmer({
    name: "Niki de Saint Phalle",
    artwork: {
      title: "Blue Nana",
      city: "Hamburg",
      image: "https://i.imgur.com/Sd1AgUOm.jpg",
    },
  });

  function handleNameChange(e: Event) {
    updatePerson((draft) => {
      draft.name = e.target.value;
    });
  }

  function handleTitleChange(e: Event) {
    updatePerson((draft) => {
      draft.artwork.title = e.target.value;
    });
  }

  function handleCityChange(e: Event) {
    updatePerson((draft) => {
      draft.artwork.city = e.target.value;
    });
  }

  function handleImageChange(e: Event) {
    updatePerson((draft) => {
      draft.artwork.image = e.target.value;
    });
  }

  return (
    <>
      <label>
        Name:
        <input value={person.name} onChange={handleNameChange} />
      </label>
      <label>
        Title:
        <input value={person.artwork.title} onChange={handleTitleChange} />
      </label>
      <label>
        City:
        <input value={person.artwork.city} onChange={handleCityChange} />
      </label>
      <label>
        Image:
        <input value={person.artwork.image} onChange={handleImageChange} />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {" by "}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img src={person.artwork.image} alt={person.artwork.title} />
    </>
  );
}

export default App;

useImmerを使用することで,nest したオブジェクトの更新も楽に行える。

github.com

参考

beta.reactjs.org

Choosing the State Structure

state の構造を考えるときに,考慮すべき原則は以下。

  1. 関連する state をグループ化する。 二つ以上の state を常に同時に更新しているなら,それらの state を単一の state に統合することを考えるべき。
  2. state 間での矛盾を避ける。
  3. 冗長な state を避ける。 props や既存の state から算出できる情報を新たに state で管理すべきではない。
  4. 重複した state を避ける。
  5. 深くネストした state を避ける。 深い階層の state は更新がしにくい。
import { useState } from "react";

function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  return (
    <div
      onPointerMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}
      style={{
        position: "relative",
        width: "100vw",
        height: "100vh",
      }}
    >
      <div
        style={{
          position: "absolute",
          backgroundColor: "red",
          borderRadius: "50%",
          transform: `translate(${position.x}px, ${position.y}px)`,
          left: -10,
          top: -10,
          width: 20,
          height: 20,
        }}
      />
    </div>
  );
}

export default App;

positionx,yプロパティは常に同時に更新されるため,単一の state で管理した方がよい。

Avoid contradictions in state

import { FormEvent, useState } from "react";

function App() {
  const [text, setText] = useState("");
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

  if (isSent) {
    return <h1>Thanks for feedback!</h1>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Pricing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <br />
      <button disabled={isSending} type="submit">
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

function sendMessage(text: string): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, 2000);
  });
}

export default App;

上記のコードだと,isSendingisSentが両方とも true という矛盾した state が存在する可能性がある。statusというユニオン型を持つ state を導入することで解決できる。

import { FormEvent, useState } from "react";

function App() {
  const [text, setText] = useState("");
  const [status, setStatus] =
    (useState < "typing") | "sending" | ("sent" > "typing");

  async function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("sending");
    await sendMessage(text);
    setStatus("sent");
  }

  const isSending = status === "sending";
  const isSent = status === "sent";

  if (isSent) {
    return <h1>Thanks for feedback!</h1>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Pricing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <br />
      <button disabled={isSending} type="submit">
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

function sendMessage(text: string): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, 2000);
  });
}

export default App;

typing,sending,sentというユニオン型を持つstatusstate を定義することで,矛盾した state が発生しなくなった。

Avoid redundant state

import { ChangeEvent, FormEvent, useState } from "react";

function App() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastNaem] = useState("");
  const [fullName, setFullName] = useState("");

  function handleFirstNameChange(e: ChangeEvent<HTMLInputElement>) {
    setFirstName(e.target.value);
    setFullName(e.target.value + " " + lastName);
  }

  function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) {
    setLastNaem(e.target.value);
    setFullName(firstName + " " + e.target.value);
  }

  return (
    <>
      <h2>Let's check you in</h2>
      <label>
        First name: <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name: <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

export default App;

fullNamefirstNamelastNameから算出可能な値であり,冗長な state となっている。

以下のように,fullName = firstName + ' ' + lastNamefullNameを直接算出すれば良い。

import { ChangeEvent, useState } from "react";

function App() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");

  const fullName = firstName + " " + lastName;

  function handleFirstNameChange(e: ChangeEvent<HTMLInputElement>) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e: ChangeEvent<HTMLInputElement>) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let's check you in</h2>
      <label>
        First name: <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name: <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

export default App;

Don't mirror props in state

import { useState } from "react";

function App() {
  const [firstName, setFirstName] = useState("");

  return (
    <>
      <label>Parent First Name</label>
      <input onChange={(e) => setFirstName(e.target.value)} />
      <label>Child First Name</label>
      <FirstName firstName={firstName} />
    </>
  );
}

type FirstNameProps = {
  firstName: string,
};

function FirstName({ firstName }: FirstNameProps) {
  const [name, setName] = useState(firstName);

  return <input value={name} onChange={(e) => setName(e.target.value)} />;
}

export default App;

上記のFirstNameコンポーネントは,firstNameを props として受け取り,useState の引数として受け取っている。しかし,Appコンポーネント内のfirstNameが更新され,FirstNameコンポーネントfirstNameprops が更新されても,useState の値は更新されない。なぜなら,useState の初期化は最初のレンダリングのみで行われるため。

Avoid duplication in state

import { ChangeEvent, useState } from "react";

const initialItems = [
  { title: "pretzels", id: 0 },
  { title: "crispy seaweed", id: 1 },
  { title: "granola bar", id: 2 },
];

function App() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(items[0]);

  function handleItemChange(id: number, e: ChangeEvent<HTMLInputElement>) {
    setItems(
      items.map((item) => {
        if (item.id === id) {
          return {
            ...item,
            title: e.target.value,
          };
        } else {
          return item;
        }
      })
    );
  }

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={(e) => handleItemChange(item.id, e)}
            />{" "}
            <button
              onClick={() => {
                setSelectedItem(item);
              }}
            >
              Choose
            </button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}</p>
    </>
  );
}

export default App;

上記の実装だと,selectedItemitems内に存在するitemで重複が生まれてしまう。そのため,itemsに変更が加えられても,selectedItemに反映されない。selectedIdという state を持つことで state の重複を回避できる。

import { ChangeEvent, useState } from "react";

const initialItems = [
  { title: "pretzels", id: 0 },
  { title: "crispy seaweed", id: 1 },
  { title: "granola bar", id: 2 },
];

function App() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find((item) => item.id === selectedId);

  function handleItemChange(id: number, e: ChangeEvent<HTMLInputElement>) {
    setItems(
      items.map((item) => {
        if (item.id === id) {
          return {
            ...item,
            title: e.target.value,
          };
        } else {
          return item;
        }
      })
    );
  }

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={(e) => handleItemChange(item.id, e)}
            />{" "}
            <button
              onClick={() => {
                setSelectedId(item.id);
              }}
            >
              Choose
            </button>
          </li>
        ))}
      </ul>
      {selectedItem && <p>You picked {selectedItem.title}</p>}
    </>
  );
}

export default App;

Avoid deeply nested state

import { ChangeEvent, useState } from "react";

const initialTravelPlan = {
  id: 0,
  title: "(Root)",
  childPlaces: [
    {
      id: 1,
      title: "Earth",
      childPlaces: [
        {
          id: 2,
          title: "Africa",
          childPlaces: [
            {
              id: 3,
              title: "Botswana",
              childPlaces: [],
            },
            {
              id: 4,
              title: "Egypt",
              childPlaces: [],
            },
            {
              id: 5,
              title: "Kenya",
              childPlaces: [],
            },
            {
              id: 6,
              title: "Madagascar",
              childPlaces: [],
            },
            {
              id: 7,
              title: "Morocco",
              childPlaces: [],
            },
            {
              id: 8,
              title: "Nigeria",
              childPlaces: [],
            },
            {
              id: 9,
              title: "South Africa",
              childPlaces: [],
            },
          ],
        },
        {
          id: 10,
          title: "Americas",
          childPlaces: [
            {
              id: 11,
              title: "Argentina",
              childPlaces: [],
            },
            {
              id: 12,
              title: "Brazil",
              childPlaces: [],
            },
            {
              id: 13,
              title: "Barbados",
              childPlaces: [],
            },
            {
              id: 14,
              title: "Canada",
              childPlaces: [],
            },
            {
              id: 15,
              title: "Jamaica",
              childPlaces: [],
            },
            {
              id: 16,
              title: "Mexico",
              childPlaces: [],
            },
            {
              id: 17,
              title: "Trinidad and Tobago",
              childPlaces: [],
            },
            {
              id: 18,
              title: "Venezuela",
              childPlaces: [],
            },
          ],
        },
        {
          id: 19,
          title: "Asia",
          childPlaces: [
            {
              id: 20,
              title: "China",
              childPlaces: [],
            },
            {
              id: 21,
              title: "Hong Kong",
              childPlaces: [],
            },
            {
              id: 22,
              title: "India",
              childPlaces: [],
            },
            {
              id: 23,
              title: "Singapore",
              childPlaces: [],
            },
            {
              id: 24,
              title: "South Korea",
              childPlaces: [],
            },
            {
              id: 25,
              title: "Thailand",
              childPlaces: [],
            },
            {
              id: 26,
              title: "Vietnam",
              childPlaces: [],
            },
          ],
        },
        {
          id: 27,
          title: "Europe",
          childPlaces: [
            {
              id: 28,
              title: "Croatia",
              childPlaces: [],
            },
            {
              id: 29,
              title: "France",
              childPlaces: [],
            },
            {
              id: 30,
              title: "Germany",
              childPlaces: [],
            },
            {
              id: 31,
              title: "Italy",
              childPlaces: [],
            },
            {
              id: 32,
              title: "Portugal",
              childPlaces: [],
            },
            {
              id: 33,
              title: "Spain",
              childPlaces: [],
            },
            {
              id: 34,
              title: "Turkey",
              childPlaces: [],
            },
          ],
        },
        {
          id: 35,
          title: "Oceania",
          childPlaces: [
            {
              id: 36,
              title: "Australia",
              childPlaces: [],
            },
            {
              id: 37,
              title: "Bora Bora (French Polynesia)",
              childPlaces: [],
            },
            {
              id: 38,
              title: "Easter Island (Chile)",
              childPlaces: [],
            },
            {
              id: 39,
              title: "Fiji",
              childPlaces: [],
            },
            {
              id: 40,
              title: "Hawaii (the USA)",
              childPlaces: [],
            },
            {
              id: 41,
              title: "New Zealand",
              childPlaces: [],
            },
            {
              id: 42,
              title: "Vanuatu",
              childPlaces: [],
            },
          ],
        },
      ],
    },
    {
      id: 43,
      title: "Moon",
      childPlaces: [
        {
          id: 44,
          title: "Rheita",
          childPlaces: [],
        },
        {
          id: 45,
          title: "Piccolomini",
          childPlaces: [],
        },
        {
          id: 46,
          title: "Tycho",
          childPlaces: [],
        },
      ],
    },
    {
      id: 47,
      title: "Mars",
      childPlaces: [
        {
          id: 48,
          title: "Corn Town",
          childPlaces: [],
        },
        {
          id: 49,
          title: "Green Hill",
          childPlaces: [],
        },
      ],
    },
  ],
};

type PlaceTreeProps = {
  id: number,
  title: string,
  childPlaces: PlaceTreeProps[],
};

function PlaceTree({ id, title, childPlaces }: PlaceTreeProps) {
  return (
    <>
      <li>{title}</li>
      {childPlaces.length > 0 && (
        <ol>
          {childPlaces.map((place) => (
            <PlaceTree
              key={place.id}
              id={place.id}
              title={place.title}
              childPlaces={place.childPlaces}
            />
          ))}
        </ol>
      )}
    </>
  );
}

function App() {
  const [plan, setPlan] = useState(initialTravelPlan);
  const planets = plan.childPlaces;

  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {planets.map((place) => (
          <PlaceTree
            key={place.id}
            id={place.id}
            title={place.title}
            childPlaces={place.childPlaces}
          />
        ))}
      </ol>
    </>
  );
}

export default App;

上記のような実装だと,ネストが深く更新が行いにくい。そのため,構造を以下のようにフラットにする。

import { useState } from "react";

type Plan = { id: number, title: string, childIds: number[] };

type TravelPlan = {
  [key: number]: Plan,
};

const initialTravelPlan: TravelPlan = {
  0: {
    id: 0,
    title: "(Root)",
    childIds: [1, 43, 47],
  },
  1: {
    id: 1,
    title: "Earth",
    childIds: [2, 10, 19, 27, 35],
  },
  2: {
    id: 2,
    title: "Africa",
    childIds: [3, 4, 5, 6, 7, 8, 9],
  },
  3: {
    id: 3,
    title: "Botswana",
    childIds: [],
  },
  4: {
    id: 4,
    title: "Egypt",
    childIds: [],
  },
  5: {
    id: 5,
    title: "Kenya",
    childIds: [],
  },
  6: {
    id: 6,
    title: "Madagascar",
    childIds: [],
  },
  7: {
    id: 7,
    title: "Morocco",
    childIds: [],
  },
  8: {
    id: 8,
    title: "Nigeria",
    childIds: [],
  },
  9: {
    id: 9,
    title: "South Africa",
    childIds: [],
  },
  10: {
    id: 10,
    title: "Americas",
    childIds: [11, 12, 13, 14, 15, 16, 17, 18],
  },
  11: {
    id: 11,
    title: "Argentina",
    childIds: [],
  },
  12: {
    id: 12,
    title: "Brazil",
    childIds: [],
  },
  13: {
    id: 13,
    title: "Barbados",
    childIds: [],
  },
  14: {
    id: 14,
    title: "Canada",
    childIds: [],
  },
  15: {
    id: 15,
    title: "Jamaica",
    childIds: [],
  },
  16: {
    id: 16,
    title: "Mexico",
    childIds: [],
  },
  17: {
    id: 17,
    title: "Trinidad and Tobago",
    childIds: [],
  },
  18: {
    id: 18,
    title: "Venezuela",
    childIds: [],
  },
  19: {
    id: 19,
    title: "Asia",
    childIds: [20, 21, 22, 23, 24, 25, 26],
  },
  20: {
    id: 20,
    title: "China",
    childIds: [],
  },
  21: {
    id: 21,
    title: "Hong Kong",
    childIds: [],
  },
  22: {
    id: 22,
    title: "India",
    childIds: [],
  },
  23: {
    id: 23,
    title: "Singapore",
    childIds: [],
  },
  24: {
    id: 24,
    title: "South Korea",
    childIds: [],
  },
  25: {
    id: 25,
    title: "Thailand",
    childIds: [],
  },
  26: {
    id: 26,
    title: "Vietnam",
    childIds: [],
  },
  27: {
    id: 27,
    title: "Europe",
    childIds: [28, 29, 30, 31, 32, 33, 34],
  },
  28: {
    id: 28,
    title: "Croatia",
    childIds: [],
  },
  29: {
    id: 29,
    title: "France",
    childIds: [],
  },
  30: {
    id: 30,
    title: "Germany",
    childIds: [],
  },
  31: {
    id: 31,
    title: "Italy",
    childIds: [],
  },
  32: {
    id: 32,
    title: "Portugal",
    childIds: [],
  },
  33: {
    id: 33,
    title: "Spain",
    childIds: [],
  },
  34: {
    id: 34,
    title: "Turkey",
    childIds: [],
  },
  35: {
    id: 35,
    title: "Oceania",
    childIds: [36, 37, 38, 39, 40, 41, 42],
  },
  36: {
    id: 36,
    title: "Australia",
    childIds: [],
  },
  37: {
    id: 37,
    title: "Bora Bora (French Polynesia)",
    childIds: [],
  },
  38: {
    id: 38,
    title: "Easter Island (Chile)",
    childIds: [],
  },
  39: {
    id: 39,
    title: "Fiji",
    childIds: [],
  },
  40: {
    id: 40,
    title: "Hawaii (the USA)",
    childIds: [],
  },
  41: {
    id: 41,
    title: "New Zealand",
    childIds: [],
  },
  42: {
    id: 42,
    title: "Vanuatu",
    childIds: [],
  },
  43: {
    id: 43,
    title: "Moon",
    childIds: [44, 45, 46],
  },
  44: {
    id: 44,
    title: "Rheita",
    childIds: [],
  },
  45: {
    id: 45,
    title: "Piccolomini",
    childIds: [],
  },
  46: {
    id: 46,
    title: "Tycho",
    childIds: [],
  },
  47: {
    id: 47,
    title: "Mars",
    childIds: [48, 49],
  },
  48: {
    id: 48,
    title: "Corn Town",
    childIds: [],
  },
  49: {
    id: 49,
    title: "Green Hill",
    childIds: [],
  },
};

type PlaceTreeProps = {
  id: number,
  parentId: number,
  placesById: TravelPlan,
  onComplete: (parentId: number, id: number) => void,
};

function PlaceTree({ id, parentId, placesById, onComplete }: PlaceTreeProps) {
  const place = placesById[id];
  const childIds = place.childIds;

  return (
    <>
      <li>
        {place.title}
        <button onClick={() => onComplete(parentId, id)}>Complete</button>
      </li>
      {childIds.length > 0 && (
        <ol>
          {childIds.map((childId) => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      )}
    </>
  );
}

function App() {
  const [plan, setPlan] = useState(initialTravelPlan);
  const root = plan[0];
  const planetIds = root.childIds;

  function handleComplete(parentId: number, childId: number) {
    const parent = plan[parentId];
    const nextParent: Plan = {
      ...parent,
      childIds: parent.childIds.filter((id) => id !== childId),
    };

    setPlan({
      ...plan,
      [parentId]: nextParent,
    });
  }

  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {planetIds.map((id) => (
          <PlaceTree
            key={id}
            id={id}
            parentId={0}
            placesById={plan}
            onComplete={handleComplete}
          />
        ))}
      </ol>
    </>
  );
}

export default App;

2022/07/21

readonly

readonly のチェックがされない時がある

const sum = (obj: { foo: number; bar: number; baz: number }) => {
  obj.foo = 999999;

  return obj.foo + obj.bar + obj.baz;
};

const myObj = {
  foo: 0,
  bar: 100,
  baz: 1000,
} as const;

sum(myObj); ← sum内でfooの書き換えが行われているのに,コンパイルエラーとならない

配列における readonly

const sum = (arr: number[]) => {
  return arr.reduce((a, b) => a + b, 0);
};

const myArr = [1, 2, 3, 4] as const;

sum(myArr); ← sum内でarrの書き換えは行わないのに,readonlyに関してのコンパイルエラーとなる

参考

https://qiita.com/uhyo/items/0fd033ff1aed9b4b32dd

2022/07/19

mapped type

  • readonly型をつけることで,書き換え不可能なものであるというインターフェースだとわかる
const convertToNumberAll = (strArray: readonly string[]): number[] => {
  return strArray.map((str) => parseInt(str, 10));
};

const array = ["1", "2"] as const;

const intArray = convertToNumberAll(array);
  • mapped type
const convertToNumberAll = <A extends readonly string[]>(
  strArray: A
): { [K in keyof A]: number } => {
  return strArray.map((str) => parseInt(str, 10)) as any;
};

const array = ["1", "2"] as const;

const intArray = convertToNumberAll(array);

素数が引数と返り値で同じだと型からわかる

conditional type

type Options<IsOptional extends boolean> = { optional: IsOptional };
const getUserInput = async <IsOptional extends boolean>({
  optional,
}: Options<IsOptional>): Promise<
  IsOptional extends false ? string : string | undefined
> => {
  while (true) {
    const result = await fetch("/");
    if (result !== undefined || !optional) {
      return result.json();
    }
  }
};

const foo = getUserInput({ optional: false }); => Promise<string>
const bar = getUserInput({ optional: true }); => Promise<string | undefined>

参考

https://blog.uhy.ooo/entry/2020-08-31/dont-fear-ts/