Open12

chakura ui

どるあがどるあが

globalなスタイル

thema.ts
import { extendTheme } from "@chakra-ui/react";

const theme = extendTheme({
  styles: {
    global: {
      body: {
        backgroundColor: "gray.100",
        color: "gray.800"
      }
    }
  }
});
export default theme;

以下のように使います

App.ts
return (
    <ChakraProvider theme={theme}>
      <Button colorScheme="teal">ボタン</Button>
    
    </ChakraProvider>
  );

どるあがどるあが

簡単にレスポンシブデザインに対応できる

以下のようにchakraではあらかじめ大きさの定義をしてくれているので、これに対応して大きさを変えたりできる。

const breakpoints = {
  sm: '30em',
  md: '48em',
  lg: '62em',
  xl: '80em',
  '2xl': '96em',
}
{/* Heading:htmlのh1やh2といったタグのようなもの */}
      <Heading as="h1" fontSize={{ base: "md", md: "lg" }}>
        ユーザー管理アプリ
      </Heading>
どるあがどるあが

Drawerによるメニュー画面の表示

MenuDrawer.tsx
  return (
    <Drawer placement="left" size="xs" onClose={onClose} isOpen={isOpen}>
      <DrawerOverlay>
        <DrawerContent>
          <DrawerBody p={0} bg="gray.100">
            <Button onClick={onClickHome} w="100%">
              TOP
            </Button>
            <Button onClick={onClickUserManagement} w="100%">
              ユーザー一覧
            </Button>
            <Button onClick={onClickSetting} w="100%">
              設定
            </Button>
          </DrawerBody>
        </DrawerContent>
      </DrawerOverlay>
    </Drawer>
  );


ボタンでisOpenを切り替えることで表示する

<MenuIconButton onOpen={onOpen} />
MenuIconButton.tsx
type Props = {
  // 関数の型は以下のようにする。引数も戻り値もない。
  onOpen: () => void;
};

export const MenuIconButton: VFC<Props> = memo((props) => {
  const { onOpen } = props;
  return (
    <IconButton
      aria-label="メニューボタン"
      icon={<HamburgerIcon />}
      size="sm"
      variant="unstyled"
      display={{ base: "block", md: "none" }}
      onClick={onOpen}
    />
  );
});

https://chakra-ui.com/docs/components/drawer

どるあがどるあが

テキストボックス

  const [userId, setUserId] = useState("");

  // textBoxの型指定は以下のもので覚えちゃう
  const onChangeUserId = (e: ChangeEvent<HTMLInputElement>) => {
    setUserId(e.target.value);
  };
  <Input
     placeholder="ユーザーID"
     value={userId}
     onChange={onChangeUserId}
   />
どるあがどるあが

ログイン機能

かなりそのまま使えるので、そのまま載せとく

Login.tsx
import { Box, Divider, Flex, Heading, Input, Stack } from "@chakra-ui/react";
import { ChangeEvent, memo, useState, VFC } from "react";
import { useAuth } from "../../hooks/useAuth";
import { PrimaryButton } from "../atoms/button/PrimaryButton";

// VFCをつけるとchildrenの有無が厳格になる
export const Login: VFC = memo(() => {
  const { login, loading } = useAuth();

  // useStateには型推論があるので、型を書かなくても大丈夫
  // 以下の例では、初期値string確定なので書かなくてよい
  const [userId, setUserId] = useState("");

  const onClickLogin = () => login(userId);

  // textBoxの型指定は以下のもので覚えちゃう
  const onChangeUserId = (e: ChangeEvent<HTMLInputElement>) => {
    setUserId(e.target.value);
  };

  return (
    <Flex align="center" justify="center" height="100vh">
      <Box bg="white" w="sm" p={4} borderRadius="md" shadow="md">
        <Heading as="h1" size="lg" textAlign="center">
          ユーザー管理アプリ
        </Heading>
        {/* 線を引いてくれる */}
        <Divider my={4} />
        {/* 等間隔にしてくれる */}
        <Stack spacing={6} py={4} px={10}>
          <Input
            placeholder="ユーザーID"
            value={userId}
            onChange={onChangeUserId}
          />
          <PrimaryButton
            // userIdに何も入力されてなければtrue,非活性
            disabled={userId === ""}
            loading={loading}
            onClick={onClickLogin}
          >
            ログイン
          </PrimaryButton>
        </Stack>
      </Box>
    </Flex>
  );
});

PrimaryButton.tsx
import { Button } from "@chakra-ui/react";
import { memo, ReactNode, VFC } from "react";

type Props = {
  children: ReactNode;
  disabled?: boolean;
  loading?: boolean;
  onClick: () => void;
};

export const PrimaryButton: VFC<Props> = memo((props) => {
  const { children, disabled = false, loading = false, onClick } = props;
  return (
    <Button
      bg="teal.400"
      color="white"
      _hover={{ opacity: 0.8 }}
      // 念のためloadingもつけておく
      disabled={disabled || loading}
      // isLoading: trueにすると非活性
      isLoading={loading}
      onClick={onClick}
    >
      {children}
    </Button>
  );
});

useAuth.ts
import axios from "axios";
import { useCallback, useState } from "react";
import { useHistory } from "react-router-dom";
import { User } from "../types/api/user";

export const useAuth = () => {
  const history = useHistory();

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

  const login = useCallback(
    (id: string) => {
      setLoading(true);

      axios
        .get<User>(`https://jsonplaceholder.typicode.com/users/${id}`)
        .then((res) => {
          if (res.data) {
            // データが見つかった場合はページ遷移する
            history.push("/home");
          } else {
            alert("ユーザーが見つかりません");
          }
        })
        .catch(() => alert("ログインできません"))
        .finally(() => setLoading(false));
    },
    [history]
  );

  return { login, loading };
};

どるあがどるあが

メッセージ toast

めっちゃ便利でおしゃれなメッセージの出し方
Hooksで作るということを考える
Hooksはクラスというかメソッドというかなんというか

useMessage.ts
import { useToast } from "@chakra-ui/react";
import { useCallback, useState } from "react";

type Props = {
  title: string;
  // 4つの文字列のどれかしか受け取れない
  status: "info" | "warning" | "success" | "error";
};

export const useMessage = () => {
  const toast = useToast();

  const showMessage = useCallback(
    (props: Props) => {
      const { title, status } = props;
      toast({
        title,
        status,
        position: "top",
        duration: 2000,
        isClosable: true
      });
    },
    [toast]
  );

  return { showMessage };
};

使い方

useAuth.ts
axios
        .get<User>(`https://jsonplaceholder.typicode.com/users/${id}`)
        .then((res) => {
          if (res.data) {
            showMessage({ title: "ログインしました", status: "success" });
            // データが見つかった場合はページ遷移する
            history.push("/home");
          } else {
            showMessage({ title: "ユーザが見つかりません", status: "error" });
          }
        })
        .catch(() =>
          showMessage({ title: "ログインできません", status: "error" })
        )
        .finally(() => setLoading(false));
    },
どるあがどるあが

ユーザー一覧を表示

ユーザー一覧データを取得する関数を実行するためのHooksを作成

useAllUsers
/* eslint-disable */
import axios from "axios";
import { useCallback, useState } from "react";
import { User } from "../types/api/user";
import { useMessage } from "./useMessage";

export const useAllUsers = () => {
  const [loading, setLoading] = useState<boolean>();
  // usersのundifined防止にから配列[]を初期値にしておく
  const [users, setUsers] = useState<Array<User>>([]);
  const { showMessage } = useMessage();

  const getUsers = useCallback(() => {
    setLoading(true);
    axios
      .get<Array<User>>("https://jsonplaceholder.typicode.com/users")
      // 返ってくるものはresで受け取り、その中にdataがあるのでres.dataで受け取る
      .then((res) => setUsers(res.data))
      .catch(() => {
        showMessage({ title: "ユーザ取得に失敗しました", status: "error" });
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);

  return { getUsers, loading, users };
};

ユーザー一覧を取得の実行

UserMaggement.tsx
const { getUsers, users, loading } = useAllUsers();

  // 一回だけ実行する時はuseEffent
  useEffect(() => getUsers(), []);

loadingの値を見て、falseになる、つまりgetUsersの実行が終わったら表示を行う
外側を<>で囲う意味がいまいちわからないが…

UserManagement.tsx
<>
      {loading ? (
        <Center h="100vh">
          {/* ぐるぐる回るローディング */}
          <Spinner />
        </Center>
      ) : (
        <Wrap p={{ base: 4, md: 10 }}>
          {users.map((user) => (
            <WrapItem key={user.id} mx="auto">
              <UserCard
                imageUrl="https://source.unsplash.com/random"
                userName={user.username}
                fullName={user.name}
              />
            </WrapItem>
          ))}
        </Wrap>
      )}
    </>
どるあがどるあが

モーダルを表示する

少々複雑なので、よく考える必要がある
逆順で考えてみると

  1. モーダルを表示させるには、クリックされたこと(isOpen)情報と、何のユーザを表示するかの情報(selectedUser<User>)が必要

  2. selectedUserを得るには、クリックされたUserCardの情報が必要。UserCardにonClickUserを設定。クリックされたカードのidと、全てのユーザidを突合し抽出する(onSelectUser)

  3. そのために全てのユーザ情報が必要なため、useAllUsersフックスを作成し、getUsers関数でusersに情報全てを格納するようにする

  4. すべてのユーザを取得するビジネスロジックであるuseAllUsersフックスを作成する

  5. 選択されたユーザを取得するビジネスロジックであるuseSelectUserフックスを作成する

  6. モーダルを表示させるには、クリックされたこと(isOpen)情報と、何のユーザを表示するかの情報(selectedUser<User>)が必要

UserManagement.tsx
{/* id, users, onOpenが設定された状態で以下が実行される */}
      <UserDetailModal user={selectedUser} isOpen={isOpen} onClose={onClose} />

  1. selectedUserを得るには、クリックされたUserCardの情報が必要。UserCardにonClickUserを設定。クリックされたカードのidと、全てのユーザidを突合し抽出する(onSelectUser)
UserManagement.tsx
<UserCard
                id={user.id}
                imageUrl="https://source.unsplash.com/random"
                userName={user.username}
                fullName={user.name}
                // UserCardの中でonClickUserにはidが引数として渡されている
                onClick={onClickUser}
              />
UserManagement.tsx
  // userをクリックしたときの挙動
  // usersの中からクリックしたuserのidと一致するものをselectedUserに設定
  // onOpenを実行することでisOpenをtrueにする
  const onClickUser = useCallback(
    (id: number) => {
      onSelectUser({ id, users, onOpen });
    },
    [users, onSelectUser, onOpen]
  );

  1. そのために全てのユーザ情報が必要なため、useAllUsersフックスを作成し、getUsers関数でusersに情報全てを格納するようにする
UserManagement.tsx
  // 一回だけ実行する時はuseEffent
  // ここでuser検索をしており、usersにArray<user>型でjson形式から受け取っている
  // ビジネスロジックの分離
  useEffect(() => getUsers(), [getUsers]);
  1. すべてのユーザを取得するビジネスロジックであるuseAllUsersフックスを作成する
useAllUsers.ts
export const useAllUsers = () => {
  const [loading, setLoading] = useState<boolean>();
  // usersのundifined防止にから配列[]を初期値にしておく
  const [users, setUsers] = useState<Array<User>>([]);
  const { showMessage } = useMessage();

  const getUsers = useCallback(() => {
    setLoading(true);
    axios
      // Json形式のデータをuser型の配列として取得する
      // axiosは受け取ったjsonを簡単に扱うことができる
      .get<Array<User>>("https://jsonplaceholder.typicode.com/users")
      // 返ってくるものはresで受け取り、その中にdataがあるのでres.dataで受け取る
      .then((res) => setUsers(res.data))
      .catch(() => {
        showMessage({ title: "ユーザ取得に失敗しました", status: "error" });
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);

  return { getUsers, loading, users };
};

  1. 選択されたユーザを取得するビジネスロジックであるuseSelectUserフックスを作成する
useSelectUser.tsx
type Props = {
  id: number;
  users: Array<User>;
  onOpen: () => void;
};

// 選択したユーザー情報を特定しモーダルを表示するカスタムフック
export const useSelectUser = () => {
  const [selectedUser, setSelectedUser] = useState<User | null>(null);

  const onSelectUser = useCallback((props: Props) => {
    const { id, users, onOpen } = props;
    const targetUser = users.find((user) => user.id === id);

    setSelectedUser(targetUser!);
    onOpen();
  }, []);

  return { onSelectUser, selectedUser };
};