chakura ui
chakura ui
今回の設定
globalなスタイル
import { extendTheme } from "@chakra-ui/react";
const theme = extendTheme({
styles: {
global: {
body: {
backgroundColor: "gray.100",
color: "gray.800"
}
}
}
});
export default theme;
以下のように使います
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>
flexGrowについて
flex-growは余ったスペースをどういう比率で使うかというもの
解説
chakra UI のサイズ指定
ここからbreakpointの指定が見れる
サイト自体は使いにくいと感じる
Drawerによるメニュー画面の表示
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} />
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}
/>
);
});
テキストボックス
const [userId, setUserId] = useState("");
// textBoxの型指定は以下のもので覚えちゃう
const onChangeUserId = (e: ChangeEvent<HTMLInputElement>) => {
setUserId(e.target.value);
};
<Input
placeholder="ユーザーID"
value={userId}
onChange={onChangeUserId}
/>
ログイン機能
かなりそのまま使えるので、そのまま載せとく
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>
);
});
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>
);
});
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はクラスというかメソッドというかなんというか
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 };
};
使い方
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を作成
/* 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 };
};
ユーザー一覧を取得の実行
const { getUsers, users, loading } = useAllUsers();
// 一回だけ実行する時はuseEffent
useEffect(() => getUsers(), []);
loadingの値を見て、falseになる、つまりgetUsersの実行が終わったら表示を行う
外側を<>で囲う意味がいまいちわからないが…
<>
{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>
)}
</>
モーダルを表示する
少々複雑なので、よく考える必要がある
逆順で考えてみると
-
モーダルを表示させるには、クリックされたこと(isOpen)情報と、何のユーザを表示するかの情報(selectedUser<User>)が必要
-
selectedUserを得るには、クリックされたUserCardの情報が必要。UserCardにonClickUserを設定。クリックされたカードのidと、全てのユーザidを突合し抽出する(onSelectUser)
-
そのために全てのユーザ情報が必要なため、useAllUsersフックスを作成し、getUsers関数でusersに情報全てを格納するようにする
-
すべてのユーザを取得するビジネスロジックであるuseAllUsersフックスを作成する
-
選択されたユーザを取得するビジネスロジックであるuseSelectUserフックスを作成する
-
モーダルを表示させるには、クリックされたこと(isOpen)情報と、何のユーザを表示するかの情報(selectedUser<User>)が必要
{/* id, users, onOpenが設定された状態で以下が実行される */}
<UserDetailModal user={selectedUser} isOpen={isOpen} onClose={onClose} />
- selectedUserを得るには、クリックされたUserCardの情報が必要。UserCardにonClickUserを設定。クリックされたカードのidと、全てのユーザidを突合し抽出する(onSelectUser)
<UserCard
id={user.id}
imageUrl="https://source.unsplash.com/random"
userName={user.username}
fullName={user.name}
// UserCardの中でonClickUserにはidが引数として渡されている
onClick={onClickUser}
/>
// userをクリックしたときの挙動
// usersの中からクリックしたuserのidと一致するものをselectedUserに設定
// onOpenを実行することでisOpenをtrueにする
const onClickUser = useCallback(
(id: number) => {
onSelectUser({ id, users, onOpen });
},
[users, onSelectUser, onOpen]
);
- そのために全てのユーザ情報が必要なため、useAllUsersフックスを作成し、getUsers関数でusersに情報全てを格納するようにする
// 一回だけ実行する時はuseEffent
// ここでuser検索をしており、usersにArray<user>型でjson形式から受け取っている
// ビジネスロジックの分離
useEffect(() => getUsers(), [getUsers]);
- すべてのユーザを取得するビジネスロジックであるuseAllUsersフックスを作成する
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 };
};
- 選択されたユーザを取得するビジネスロジックであるuseSelectUserフックスを作成する
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 };
};
型の追加
isAdminが追加されていることがわかる