グローバルなstate管理
グローバルなstate管理
グローバルなstate管理あり・なしで管理者の時は編集リンクを表示させ、
一般ユーザの時は編集リンクを表示させないことを実装する
ツラい実装
まずはグローバルなstate管理なしで管理者の時は編集リンクを表示させ、
一般ユーザの時は編集リンクを表示させないことを実装する
管理者ユーザと一般ユーザ用のボタンを用意する
import { useHistory } from "react-router-dom"; import styled from "styled-components"; import { SecondaryButton } from "../atoms/button/SecondaryButton";export const Top = () => { const history = useHistory();
const onClickAdmin = () => history.push({ pathname: "/users", state: { isAdmin: true } }); const onClickGeneral = () => history.push({ pathname: "/users", state: { isAdmin: false } });
return ( <SContainer> <h2>TOPページです</h2> <SecondaryButton onClick={onClickAdmin}>管理者ユーザ</SecondaryButton> <br /> <br /> <SecondaryButton onClick={onClickGeneral}>一般ユーザ</SecondaryButton> </SContainer> ); };
const SContainer = styled.div
text-align: center;
;
SecondaryButtonはonClickを受け取れるようになっている
import styled from "styled-components"; import { BaseButton } from "./BaseButton";export const SecondaryButton = (props) => { const { children, onClick } = props;
return <SButton onClick={onClick}>{children}</SButton>; };
const SButton = styled(BaseButton)
background-color: #11999e;
;
ページ遷移時のstateをuseLocationで取得
import { useLocation } from "react-router-dom"; import styled from "styled-components"; import { SearchInput } from "../molecules/SearchInput"; import { UserCard } from "../organisms/user/UserCard";const users = [...Array(10).keys()].map((val) => { return { id: val, name:
Bob$<span class="synIdentifier">{</span>val<span class="synIdentifier">}</span>
, image: "https://source.unsplash.com/2l0CWTpcChI", email: "12345@example.com", phone: "090-1111-2222", company: { name: "テスト株式会社" }, website: "https://google.com" }; });export const Users = () => { const { state } = useLocation(); const isAdmin = state ? state.isAdmin : false;
return ( <SContainer> <h2>ユーザー一覧</h2> <SearchInput /> <SUserArea> {users.map((obj) => ( <UserCard key={obj.id} user={obj} isAdmin={isAdmin} /> ))} </SUserArea> </SContainer> ); };
const SContainer = styled.div
text-align: center; flex-direction: column; align-items: center; padding: 24px;
;const SUserArea = styled.div
padding-<span class="synStatement">top</span>: 40px; width: 100%; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 20px;
;
渡されたisAdminを受け取る
import styled from "styled-components"; import { Card } from "../../atoms/card/Card"; import { UserIconWithName } from "../../molecules/user/UserIconWithName";export const UserCard = (props) => { const { user, isAdmin } = props;
return ( <Card> <UserIconWithName image={user.image} name={user.name} isAdmin={isAdmin} /> <SDL> <dt>メール</dt> <dd>{user.email}</dd> <dt>TEL</dt> <dd>{user.phone}</dd> <dt>会社名</dt> <dd>{user.company.name}</dd> <dt>WEB</dt> <dd>{user.website}</dd> </SDL> </Card> ); };
const SDL = styled.dl
text-align: left; margin-bottom: 0px; dt <span class="synIdentifier">{</span> <span class="synStatement">float</span>: left; <span class="synIdentifier">}</span> dd <span class="synIdentifier">{</span> padding-left: 32px; padding-bottom: 8px; overflow-wrap: <span class="synStatement">break</span>-word; <span class="synIdentifier">}</span>
;
渡されたisAdminを受け取る
import styled from "styled-components";export const UserIconWithName = (props) => { const { image, name, isAdmin } = props; return ( <SContainer> <SImg height={160} width={160} src={image} alt={name} /> <SName>{name}</SName> {isAdmin && <SEdit>編集</SEdit>} </SContainer> ); };
const SContainer = styled.div
text-align: center;
;const SImg = styled.img
border-radius: 50%;
; const SName = styled.pfont-size: 18px; font-weight: bold; margin: 0; color: #40514e;
; const SEdit = styled.spantext-decoration: underline; color: #aaa; cursor: pointer;
;
これがもっと規模が大きくなるとツラい。
これをグローバルなstate管理で解決していく。
Context
$ mkdir -p src/providers
$ touch src/providers/UserProvider.jsx
コンテキストのコンポーネントを作成
import { createContext, useState } from "react";// 新しいコンテキストの作成 export const UserContext = createContext({});
export const UserProvider = (props) => { const { children } = props;
const [userInfo, setUserInfo] = useState(null);
return ( // valueというグローバルなstateを渡す <UserContext.Provider value={{ userInfo, setUserInfo }}> {children} </UserContext.Provider> ); };
Providerは要素を囲む必要がある
import { UserProvider } from "./providers/UserProvider"; import { Router } from "./router/Router"; import "./styles.css";export default function App() { return ( <UserProvider> <Router /> </UserProvider> ); }
コンテキストの値を参照していくにはuseContextを使用する
import { useContext } from "react"; import styled from "styled-components"; import { UserContext } from "../../../providers/UserProvider";export const UserIconWithName = (props) => { const { image, name } = props; const { userInfo } = useContext(UserContext); const isAdmin = userInfo ? userInfo.isAdmin : false;
return ( <SContainer> <SImg height={160} width={160} src={image} alt="プロフィール写真" /> <SName>{name}</SName> {isAdmin && <SEdit>編集</SEdit>} </SContainer> ); };
const SContainer = styled.div
text-align: center;
;const SImg = styled.img
border-radius: 50%;
; const SName = styled.pfont-size: 18px; font-weight: bold; margin: 0; color: #40514e;
; const SEdit = styled.spantext-decoration: underline; color: #aaa; cursor: pointer;
;
import { useContext } from "react"; import { useHistory } from "react-router-dom"; import styled from "styled-components"; import { UserContext } from "../../providers/UserProvider"; import { SecondaryButton } from "../atoms/button/SecondaryButton";export const Top = () => { const history = useHistory();
const { setUserInfo } = useContext(UserContext);
const onClickAdmin = () => { setUserInfo({ isAdmin: true }); history.push("/users"); }; const onClickGeneral = () => { setUserInfo({ isAdmin: false }); history.push("/users"); };
return ( <SContainer> <h2>TOPページです</h2> <SecondaryButton onClick={onClickAdmin}>管理者ユーザ</SecondaryButton> <br /> <br /> <SecondaryButton onClick={onClickGeneral}>一般ユーザ</SecondaryButton> </SContainer> ); };
const SContainer = styled.div
text-align: center;
;
Atomic Design
Atomic Designとは
・Brad Frost氏が考案したデザインシステム
・画面要素を5段階に分け、組み合わせることでUIを実現
・コンポーネント化された要素が画面を構成しているという考え方
・React、Vue用というわけではない
・モダンJavaScriptと相性がいい
5段階のコンポーネント
ATOMS → MOLECULES → ORGANISMS → TEMPLATES → PAGES
原子 → 分子 → 有機体 → テンプレート → ページ
ATOMS
Atoms 原子
最も小さくそれ以上分解できない要素
・ボタン
・ツイート入力テキストボックス
・アイコン 等
MOLECULES
Molecules 分子
Atomの組み合わせで意味を持つデザインパターン
・アイコン + メニュー名
・プロフィール画像 + テキストボックス
・アイコンセット 等
ORGANISMS
Organisms 有機体
AtomやMoleculeの組み合わせで構成される単体である程度の意味を持つ要素群
・ツイート入力エリア
・サイドメニュー
・1つのツイートエリア 等
TEMPLATES
Templates テンプレート
ページのレイアウトのみを表現する要素(実際のデータは持たない)
・サイドメニュー
・ツイートエリア
・トピックエリア等のレイアウト情報 等
PAGES
Pages ページ
最終的に表示される1画面
・ページ遷移毎に表示される各画面
Atom 作成
$ mkdir -p src/components/atoms/button
$ touch src/components/atoms/button/BaseButton.jsx $ touch src/components/atoms/button/PrimaryButton.jsx $ touch src/components/atoms/button/SecondaryButton.jsx
./components/atoms/button/BaseButton.jsx
import styled from "styled-components"; export const BaseButton = styled.button` color: #fff; padding: 6px 24px; border: none; border-radius: 9999px; outline: none; &:hover { cursor: pointer; opacity: 0.8; } `;
./components/atoms/button/PrimaryButton.jsx
import styled from "styled-components"; import { BaseButton } from "./BaseButton"; export const PrimaryButton = (props) => { const { children } = props; return <SButton>{children}</SButton>; }; const SButton = styled(BaseButton)` background-color: #40514e; `;
./components/atoms/button/SecondaryButton.jsx
import styled from "styled-components"; import { BaseButton } from "./BaseButton"; export const SecondaryButton = (props) => { const { children } = props; return <SButton>{children}</SButton>; }; const SButton = styled(BaseButton)` background-color: #11999e; `;
Molecules 作成
$ mkdir -p src/components/molecules $ mkdir -p src/components/atoms/input
$ touch src/components/molecules/SearchInput.jsx $ touch src/components/atoms/input/Input.jsx
./components/atoms/input/Input.jsx
import styled from "styled-components"; export const Input = (props) => { const { placeholder = "" } = props; return <SInput type="text" placeholder={placeholder} />; }; const SInput = styled.input` padding: 8px 16px; border: solid #ddd 1px; border-radius: 9999px; outline: none; `;
./components/molecules/SearchInput.jsx
import styled from "styled-components"; import { PrimaryButton } from "../atoms/button/PrimaryButton"; import { Input } from "../atoms/input/Input"; export const SearchInput = () => { return ( <SContainer> <Input placeholder="検索条件を入力" /> <SButtonWrapper> <PrimaryButton>検索</PrimaryButton> </SButtonWrapper> </SContainer> ); }; const SContainer = styled.div` display: flex; align-items: center; `; const SButtonWrapper = styled.div` padding-left: 8px; `;
Organisms 作成
$ mkdir -p src/components/organisms/user $ mkdir -p src/components/atoms/card $ mkdir -p src/components/molecules/user
$ touch src/components/organisms/user/UserCard.jsx $ touch src/components/atoms/card/Card.jsx $ touch src/components/molecules/user/UserIconWithName.jsx
./components/atoms/card/Card.jsx
import styled from "styled-components"; export const Card = (props) => { const { children } = props; return <SCard>{children}</SCard>; }; const SCard = styled.div` background-color: #fff; box-shadow: #ddd 0px 0px 4px 2px; border-radius: 8px; padding: 16px; `;
./components/organisms/user/UserCard.jsx
import styled from "styled-components"; import { Card } from "../../atoms/card/Card"; import { UserIconWithName } from "../../molecules/user/UserIconWithName"; export const UserCard = (props) => { const { user } = props; return ( <Card> <UserIconWithName image={user.image} name={user.name} /> <SDL> <dt>メール</dt> <dd>{user.email}</dd> <dt>TEL</dt> <dd>{user.phone}</dd> <dt>会社名</dt> <dd>{user.company.name}</dd> <dt>WEB</dt> <dd>{user.website}</dd> </SDL> </Card> ); }; const SDL = styled.dl` text-align: left; margin-bottom: 0px; dt { float: left; } dd { padding-left: 32px; padding-bottom: 8px; overflow-wrap: break-word; } `;
/components/molecules/user/UserIconWithName.jsx
import styled from "styled-components"; export const UserIconWithName = (props) => { const { image, name } = props; return ( <SContainer> <SImg height={160} width={160} src={image} alt={name} /> <SName>{name}</SName> </SContainer> ); }; const SContainer = styled.div` text-align: center; `; const SImg = styled.img` border-radius: 50%; `; const SName = styled.p` font-size: 18px; font-weight: bold; margin: 0; color: #40514e; `;
./App.jsx
import { PrimaryButton } from "./components/atoms/button/PrimaryButton"; import { SecondaryButton } from "./components/atoms/button/SecondaryButton"; import { SearchInput } from "./components/molecules/SearchInput"; import { UserCard } from "./components/organisms/user/UserCard"; import "./styles.css"; export default function App() { const user = { name: "Bob", image: "https://source.unsplash.com/2l0CWTpcChI", email: "12345@example.com", phone: "090-1111-2222", company: { name: "テスト株式会社" }, website: "https://google.com" }; return ( <div className="App"> <PrimaryButton>テスト</PrimaryButton> <SecondaryButton>検索</SecondaryButton> <br /> <SearchInput /> <UserCard user={user} /> </div> ); }
Template 作成
$ mkdir -p src/components/templates $ mkdir -p src/components/atoms/layout
$ touch src/components/templates/DefaultLayout.jsx $ touch src/components/templates/HeaderOnly.jsx $ touch src/components/atoms/layout/Header.jsx $ touch src/components/atoms/layout/Footer.jsx
./components/templates/HeaderOnly.jsx
import { Link } from "react-router-dom"; import styled from "styled-components"; export const Header = () => { return ( <SHeader> <SLink to="/">HOME</SLink> <SLink to="/users">USERS</SLink> </SHeader> ); }; const SHeader = styled.header` background-color: #11999e; color: #fff; text-align: center; padding: 8px 0px; `; const SLink = styled(Link)` margin: 0 8px; `;
./components/templates/DefaultLayout.jsx
import { Footer } from "../atoms/layout/Footer"; import { Header } from "../atoms/layout/Header"; export const DefaultLayout = (props) => { const { children } = props; return ( <> <Header /> {children} <Footer /> </> ); };
/components/atoms/layout/Header.jsx
import { Link } from "react-router-dom"; import styled from "styled-components"; export const Header = () => { return ( <SHeader> <SLink to="/">HOME</SLink> <SLink to="/users">USERS</SLink> </SHeader> ); }; const SHeader = styled.header` background-color: #11999e; color: #fff; text-align: center; padding: 8px 0px; `; const SLink = styled(Link)` margin: 0 8px; `;
./components/atoms/layout/Footer.jsx
import { Link } from "react-router-dom"; import styled from "styled-components"; export const Footer = () => { return <SFooter>© 2021 test Inc.</SFooter>; }; const SFooter = styled.footer` background-color: #11999e; color: #fff; text-align: center; padding: 8px 0px; position: fixed; bottom: 0; width: 100%; `;
./App.jsx
import { BrowserRouter } from "react-router-dom"; import { PrimaryButton } from "./components/atoms/button/PrimaryButton"; import { SecondaryButton } from "./components/atoms/button/SecondaryButton"; import { SearchInput } from "./components/molecules/SearchInput"; import { UserCard } from "./components/organisms/user/UserCard"; import { DefaultLayout } from "./components/templates/DefaultLayout"; import { HeaderOnly } from "./components/templates/HeaderOnly"; import "./styles.css"; export default function App() { const user = { name: "Bob", image: "https://source.unsplash.com/2l0CWTpcChI", email: "12345@example.com", phone: "090-1111-2222", company: { name: "テスト株式会社" }, website: "https://google.com" }; return ( <BrowserRouter> <DefaultLayout> <PrimaryButton>テスト</PrimaryButton> <SecondaryButton>検索</SecondaryButton> <br /> <SearchInput /> <UserCard user={user} /> </DefaultLayout> </BrowserRouter> ); }
Pages 作成
$ mkdir -p src/components/pages $ mkdir -p src/router
$ touch src/components/pages/Top.jsx $ touch src/components/page/Users.jsx $ touch src/components/atoms/layout/Header.jsx $ touch src/components/atoms/layout/Footer.jsx $ touch src/router/Router.jsx
./components/pages/Top.jsx
import styled from "styled-components"; export const Top = () => { return ( <SContainer> <h2>TOPページです</h2> </SContainer> ); }; const SContainer = styled.div` text-align: center; `;
./components/page/Users.jsx
import styled from "styled-components"; import { SearchInput } from "../molecules/SearchInput"; import { UserCard } from "../organisms/user/UserCard"; const users = [...Array(10).keys()].map((val) => { return { id: val, name: `Bob${val}`, image: "https://source.unsplash.com/2l0CWTpcChI", email: "12345@example.com", phone: "090-1111-2222", company: { name: "テスト株式会社" }, website: "https://google.com" }; }); export const Users = () => { return ( <SContainer> <h2>ユーザー一覧です</h2> <SearchInput /> <SUserArea> {users.map((user) => ( <UserCard key={user.id} user={user} /> ))} </SUserArea> </SContainer> ); }; const SContainer = styled.div` text-align: center; flex-direction: column; align-items: center; padding: 24px; `; const SUserArea = styled.div` padding-top: 40px; width: 100%; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 20px; `;
./components/atoms/layout/Header.jsx
import { Link } from "react-router-dom"; import styled from "styled-components"; export const Header = () => { return ( <SHeader> <SLink to="/">HOME</SLink> <SLink to="/users">USERS</SLink> </SHeader> ); }; const SHeader = styled.header` background-color: #11999e; color: #fff; text-align: center; padding: 8px 0px; `; const SLink = styled(Link)` margin: 0 8px; `;
./components/atoms/layout/Footer.jsx
import styled from "styled-components"; export const Footer = () => { return <SFooter>© 2021 test Inc.</SFooter>; }; const SFooter = styled.footer` background-color: #11999e; color: #fff; text-align: center; padding: 8px 0px; position: fixed; bottom: 0; width: 100%; `;
./router/Router.jsx
import { BrowserRouter, Switch, Route } from "react-router-dom"; import { Top } from "../components/pages/Top"; import { Users } from "../components/pages/Users"; import { DefaultLayout } from "../components/templates/DefaultLayout"; import { HeaderOnly } from "../components/templates/HeaderOnly"; export const Router = () => { return ( <BrowserRouter> <Switch> <Route exact path="/"> <DefaultLayout> <Top /> </DefaultLayout> </Route> <Route path="/users"> <HeaderOnly> <Users /> </HeaderOnly> </Route> </Switch> </BrowserRouter> ); };
./App.jsx
import { Router } from "./router/Router"; import "./styles.css"; export default function App() { return <Router />; }
React Router
ページ遷移
// BrowserRouter このコンポーネントで囲った配下でルーティングを有効にする import { BrowserRouter, Link, Switch, Route } from "react-router-dom"; import { Home } from "./Home"; import { Page1 } from "./Page1"; import { Page2 } from "./Page2"; import "./styles.css"; export default function App() { return ( <BrowserRouter> <Link to="/">Home</Link> <br /> <Link to="/page1">Page1</Link> <br /> <Link to="/page2">Page2</Link> <br /> <div className="App"></div> {/* Switch どのパスの時にどのコンポーネントを表示するか設定する */} <Switch> {/* Route Routeにマッチしたパス・画面を表示する */} {/* exact 完全一致の場合 */} <Route exact path="/"> <Home /> </Route> <Route path="/page1"> <Page1 /> </Route> <Route path="/page2"> <Page2 /> </Route> </Switch> </BrowserRouter> ); }
ネストされたページ遷移
// BrowserRouter このコンポーネントで囲った配下でルーティングを有効にする import { BrowserRouter, Link, Switch, Route } from "react-router-dom"; import { Home } from "./Home"; import { Page1 } from "./Page1"; import { Page1DetailA } from "./Page1DetailA"; import { Page1DetailB } from "./Page1DetailB"; import { Page2 } from "./Page2"; import "./styles.css"; export default function App() { return ( <BrowserRouter> <Link to="/">Home</Link> <br /> <Link to="/page1">Page1</Link> <br /> <Link to="/page2">Page2</Link> <br /> <div className="App"></div> {/* Switch どのパスの時にどのコンポーネントを表示するか設定する */} <Switch> {/* Route Routeにマッチしたパス・画面を表示する */} {/* exact 完全一致の場合 */} <Route exact path="/"> <Home /> </Route> <Route path="/page1" // render関数はpropsをデフォルトで受け取っている // console.log(props)で中身を確認 // matchのurlを使用する render={({ match: { url }}) => ( <Switch> <Route exact path={url}> <Page1 /> </Route> <Route path={`${url}/detailA`}> <Page1DetailA /> </Route> <Route path={`${url}/detailB`}> <Page1DetailB /> </Route> </Switch> )} ></Route> <Route path="/page2"> <Page2 /> </Route> </Switch> </BrowserRouter> ); }
コンポーネントへ切り出し
// BrowserRouter このコンポーネントで囲った配下でルーティングを有効にする import { BrowserRouter, Link } from "react-router-dom"; import { Router } from "./router/Router"; import "./styles.css"; export default function App() { return ( <BrowserRouter> <Link to="/">Home</Link> <br /> <Link to="/page1">Page1</Link> <br /> <Link to="/page2">Page2</Link> <br /> <div className="App"></div> <Router /> </BrowserRouter> ); }
import { Switch, Route } from "react-router-dom"; import { Home } from "../Home"; import { Page2 } from "../Page2"; import { page1Routes } from "./Page1Routes"; export const Router = () => { return ( <Switch> {/* Route Routeにマッチしたパス・画面を表示する */} {/* exact 完全一致の場合 */} <Route exact path="/"> <Home /> </Route> <Route path="/page1" render={({ match: { url } }) => ( <Switch> {page1Routes.map((route) => ( <Route key={route.path} exact={route.exact} path={`${url}${route.path}`} > {route.children} </Route> ))} </Switch> )} ></Route> <Route path="/page2"> <Page2 /> </Route> </Switch> ); };
import { Page1 } from "../Page1"; import { Page1DetailA } from "../Page1DetailA"; import { Page1DetailB } from "../Page1DetailB"; export const page1Routes = [ { path: "/", exact: true, children: <Page1 /> }, { path: "/detailA", exact: false, children: <Page1DetailA /> }, { path: "/detailB", exact: false, children: <Page1DetailB /> } ];
URLパラメータ
import { Page2 } from "../Page2"; import { UrlParameter } from "../UrlParameter"; export const page2Routes = [ { path: "/", exact: true, children: <Page2 /> }, { path: "/:id", exact: false, children: <UrlParameter /> } ];
import { Switch, Route } from "react-router-dom"; import { Home } from "../Home"; import { page1Routes } from "./Page1Routes"; import { page2Routes } from "./Page2Routes"; export const Router = () => { return ( <Switch> {/* Route Routeにマッチしたパス・画面を表示する */} {/* exact 完全一致の場合 */} <Route exact path="/"> <Home /> </Route> <Route path="/page1" render={({ match: { url } }) => ( <Switch> {page1Routes.map((route) => ( <Route key={route.path} exact={route.exact} path={`${url}${route.path}`} > {route.children} </Route> ))} </Switch> )} ></Route> <Route path="/page2" render={({ match: { url } }) => ( <Switch> {page2Routes.map((route) => ( <Route key={route.path} exact={route.exact} path={`${url}${route.path}`} > {route.children} </Route> ))} </Switch> )} /> </Switch> ); };
import { Link } from "react-router-dom"; export const Page2 = () => { return ( <div> <h1>Page2ページです</h1> <Link to="/page2/999">URL Parameter</Link> </div> ); };
import { useParams } from "react-router-dom"; export const UrlParameter = () => { // useParams Page2Routes.jsxでidを設定しているためidを受け取れる const { id } = useParams(); return ( <div> <h1>UrlParameterページです</h1> <p>パラメータは {id} です</p> </div> ); };
クエリパラメータ
import { Link } from "react-router-dom"; export const Page2 = () => { return ( <div> <h1>Page2ページです</h1> <Link to="/page2/999">URL Parameter</Link> <br /> <Link to="/page2/999?name=hogehoge">Query Parameter</Link> </div> ); };
import { useLocation, useParams } from "react-router-dom"; export const UrlParameter = () => { // useParams Page2Routes.jsxでidを設定しているためidを受け取れる const { id } = useParams(); // useLocation searchにあるクエリパラメータを使用できる const { search } = useLocation(); // URLSearchParams クエリパラメータを便利に扱えるメソッドを提供してくれる const query = new URLSearchParams(search); console.log(query); return ( <div> <h1>UrlParameterページです</h1> <p>パラメータは {id} です</p> {/* getメソッドでnameを取得する */} <p>クエリパラメータは {query.get("name")} です</p> </div> ); };
stateを渡すページ遷移
import { Link } from "react-router-dom"; export const Page1 = () => { // この100件の配列をDtailAページへ持っていく const arr = [...Array(100).keys()]; console.log(arr); return ( <div> <h1>Page1ページです</h1> {/* stateに対してarr変数を渡す */} <Link to={{ pathname: "page1/detailA", state: arr }}>DetailA</Link> <br /> <Link to="/page1/detailB">DetailB</Link> </div> ); };
import { useLocation } from "react-router-dom"; export const Page1DetailA = () => { const { state } = useLocation(); // stateが渡ってきているか確認 console.log(state); return ( <div> <h1>Page1DetailAページです</h1> </div> ); };
Linkを使わない遷移(js側で画面遷移)
import { Link, useHistory } from "react-router-dom"; export const Page1 = () => { const arr = [...Array(100).keys()]; const history = useHistory(); // pushメソッドで遷移先のURLを設定 const onClickDetailA = () => history.push("page1/detailA"); return ( <div> <h1>Page1ページです</h1> <Link to={{ pathname: "page1/detailA", state: arr }}>DetailA</Link> <br /> <Link to="/page1/detailB">DetailB</Link> <br /> <button onClick={onClickDetailA}>DetailA</button> </div> ); };
import { useHistory, useLocation } from "react-router-dom"; export const Page1DetailA = () => { const { state } = useLocation(); const history = useHistory(); // goBackメソッドで遷移元へ戻る const onClickBack = () => history.goBack(); return ( <div> <h1>Page1DetailAページです</h1> <button onClick={onClickBack}>戻る</button> </div> ); };
404ページ
import { Link } from "react-router-dom"; export const Page404 = () => { return ( <div> <h1>ページが見つかりません</h1> <Link to="/">TOPに戻る</Link> </div> ); };
import { Switch, Route } from "react-router-dom"; import { Home } from "../Home"; import { Page404 } from "../Page404"; import { page1Routes } from "./Page1Routes"; import { page2Routes } from "./Page2Routes"; export const Router = () => { return ( <Switch> <Route exact path="/"> <Home /> </Route> <Route path="/page1" render={({ match: { url } }) => ( <Switch> {page1Routes.map((route) => ( <Route key={route.path} exact={route.exact} path={`${url}${route.path}`} > {route.children} </Route> ))} </Switch> )} ></Route> <Route path="/page2" render={({ match: { url } }) => ( <Switch> {page2Routes.map((route) => ( <Route key={route.path} exact={route.exact} path={`${url}${route.path}`} > {route.children} </Route> ))} </Switch> )} /> {/* * 全て一致 */} <Route path="*"> <Page404 /> </Route> </Switch> ); };
React 復習
JSX
// { } でJavaScriptの記法と認識される <button onClick={}>ボタン</button>
// 外側の{ } はJavaScriptの記法と認識される // 内側の{ } はJavaScriptのオブジェクトのこと <h1 style={{ color: 'red' }}>こんにちは!</h1>
イベント発火
export const App = () => { const onClickButton = () => alert(); return ( <div> <button onClick={onClickButton}>ボタン</button> </div> ) }
Props
コンポーネントに対して渡す引数のようなもの
import { HelloWorldMessage } from "./components/HelloWorldMessage"; // コンポーネントに対して値を渡す <HelloWorldMessage message="hello" /> <HelloWorldMessage message="world" />
// propsを受け取る(helloとworldが表示される) export const HelloWorldMessage = (props) => { return ( <p>{props.message}</p> ); }
children
タグで囲った値をchildrenとして渡すこともできる
import { HelloWorldMessage } from "./components/HelloWorldMessage"; // コンポーネントに対して値を渡す <HelloWorldMessage> hello </HelloWorldMessage> <HelloWorldMessage> world </HelloWorldMessage>
// propsを受け取る(helloとworldが表示される) export const HelloWorldMessage = (props) => { return ( <p>{props.children}</p> ); }
分割代入
分割代入で毎回propsを指定しなくてもよくなる
export const HelloWorldMessage = (props) => { const { color, children } = props; const contentStyle = { color: color, // ↑オブジェクトのプロパティ名と当てはめる値が同じ時は省略できる↓ color, fontSize: '18px', }; return ( <p style={contentStyle}>{children}</p> ); }
State
各コンポーネントが持つ状態(可変)
useState
import { useState } from "react"; // 第一引数:stateとして使用する変数名 // 第二引数:stateを変更するた関数名 // useState('初期値')を設定できる const [num, setNum] useState(0); <p>{num}</p> // 0
ボタンをクリックするとnumの値が1ずつ増えるイベント
import { useState } from "react"; export const App = () => { const [num, setNum] = useState(0); const onClickCountUp = () => { setNum((prevNum) => prevNum + 1); }; return ( <div> <button onClick={onClickCountUp}>カウントアップ</button> <p>{num}</p> </div> ); };
※js
// アロー関数はreturnで返す行が一行の時は省略できる const a = (str) => { return str } // ↓ const a = (str) => str; // ↓( ) も省略することもできる const a = str => str; // && 左辺がtrueの時に右辺を返す、左辺がfalseの時は右辺を返さない {faceShowFlag && <p>orz</p>} // || は左辺がfalseなら右辺を返す、左辺がtrueの時は右辺を返さない {faceShowFlag || <p>orz</p>}
useEffect
Stateの更新があった時に再描画される(set関数が呼ばれてstateが更新された時に関数コンポーネントが頭から読み込まれる)
このような特定の条件の時にコンポーネントを再レンダリングして差分を反映することで
画面遷移を表現している
●再レンダリングする条件
・stateを変更した時 ・コンポーネントのpropsの中身が変更した時 ・親のコンポーネントが再レンダリングされた時(子コンポーネントも追随していく)
// useEffectの引数に空の配列[]を設定するとコンポーネント内で最初の1回だけ通したい処理を実行することができる useEffect(() => { console.log('useEffect'); }, []);
// 引数にnumを設定した場合、このuseEffectはnumにだけ関心を持つようになる // そのため今回であればnumのstateが変更した時だけuseEffectが通るようになる useEffect(() => { console.log('useEffect'); }, [num]);
const [faceShowFlag, setFaceShowFlag] = useState(false); useEffect(() => { if (num % 3 === 0) { // faceShowFlagがfalseの時だけset関数を呼び出しtrueに変更する faceShowFlag || setFaceShowFlag(true); } else { // faceShowFlagがtrueの時だけset関数を呼び出しfalseに変更する faceShowFlag && setFaceShowFlag(false); } }, [num]);
●行うこと
・ボタン(on/off)をクリックすると顔文字を表示・非表示にする
・3の倍数の時だけ顔文字を表示する
/*eslint react-hooks/exhaustive-deps: off */ import { useEffect, useState } from "react"; export const App = () => { const [num, setNum] = useState(0); const [faceShowFlag, setFaceShowFlag] = useState(false); const onClickCountUp = () => { setNum((prevNum) => prevNum + 1); }; const onClickSwitchShowFlag = () => setFaceShowFlag(!faceShowFlag); // 「Too many re-renders」が発生するため引数にnumを指定し、このuseEffectはnumにだけ関心を持つように設定する useEffect(() => { if (num <= 0) { return } if (num % 3 === 0) { // faceShowFlagがfalseの時だけset関数を呼び出しtrueに変更する faceShowFlag || setFaceShowFlag(true); } else { // faceShowFlagがtrueの時だけset関数を呼び出しfalseに変更する faceShowFlag && setFaceShowFlag(false); } }, [num]); return ( <div> <button onClick={onClickCountUp}>カウントアップ</button> <br/> <button onClick={onClickSwitchShowFlag}>on/off</button> <p>{num}</p> {faceShowFlag && <p>orz</p>} </div> ); };
Todo
import { useState } from "react"; import "./styles.css"; export const App = () => { const [incompleteTodos, setIncompleteTodos] = useState(['テスト1', 'テスト2']); const [completeTodos, setCompleteTodos] = useState(['テストend']); return ( <div> <div className="input-area"> <input placeholder="TODOを入力" /> <button>送信</button> </div> <dir className="incomplete-area"> <p className="title">未完了のTODO</p> <ul> {incompleteTodos.map((todo) => { return ( <div key={todo} className="list-row"> <li>{todo}</li> <button>完了</button> <button>削除</button> </div> ) })} </ul> </dir> <dir className="complete-area"> <p className="title">完了したTODO</p> <ul> <div className="list-row"> <li>テストend</li> <button>戻す</button> </div> </ul> </dir> </div> ); };
const [incompleteTodos, setIncompleteTodos] = useState(['テスト1', 'テスト2']); // ループ処理させる場合、ループ内で返却している一番親タグにkeyの指定が必要 // 変更前と変更後で差分だけ抽出しその差分のみ実際のDOMに反映していくため、ループでレンダリングされた場合、何個目の要素なのかを正確に比較するために目印をつける必要がある {incompleteTodos.map((todo) => { return ( <div key={todo} className="list-row"> <li>{todo}</li> <button>完了</button> <button>削除</button> </div> ); })}
inputタグに入力した値を取得・追加
const [todoText, setTodoText] = useState(''); // 引数にイベントを受け取る // e.target.valueで入力した値を取得しsetTodoText関数で更新する const onChangeTodoText = (e) => setTodoText(e.target.value); const onClickAdd = () => { if (todoText === '') return alert('TODOを入力してください'); // スプレッド構文で現在のincompleteTodosの中身を複製しtodoTextで新しい配列を生成(newTodos) // 新しく生成したnewTodosをsetIncompleteTodos関数で更新 const newTodos = [...incompleteTodos, todoText]; setIncompleteTodos(newTodos); setTodoText(''); }; // onChangeでinputの値に変更があれば検知する(onChangeがないと初期値の空文字が設定され続ける) <input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} /> <button onClick={onClickAdd}>送信</button>
関数に引数を渡したい場合
// アロー関数を使い新しく関数を生成する必要がある <button onClick={() => onClickDelete(index)}>削除</button>
削除
const onClickDelete = (index) => { const newTodos = [...incompleteTodos]; // splice() 第一引数に何番目の要素か、第二引数にいくつ削除するか指定する newTodos.splice(index, 1); // このnewTodosでsetIncompleteTodosを更新する setIncompleteTodos(newTodos); } <ul> {incompleteTodos.map((todo, index) => { return ( <div key={todo} className="list-row"> <li>{todo}</li> <button>完了</button> <button onClick={() => onClickDelete(index)}>削除</button> </div> ); })} </ul>
PHP基礎
<?php // シングルクォーテーションまたはダブルクォーテーションが必要 // function(パラメータ); という構文となる // print 文字列を出力する // print(string $arg) :int // 終了タグ(?)は必要ない(省略できる) print('PHPで出力しています'); // echo(string $arg1 [, string $...]) : void // voidは何もない(値を返さない) // echoはパラメータを複数とることができる // echoは関数ではないため( )を省略することができる echo '/ echoで出力しています', '/ 2つめの文章です', '/ 3つめの文章です'; // エスケープシーケンス echo 'I\'am Japanese'; echo "I am \"Japanese\""; // 改行 // シングルクォーテーションで囲うとそのまま出力される // エスケープシーケンスを使うときはダブルクォーテーション echo "\n改行を\n入れて出力します\n"; // ヒアドキュメント構文(<<<) // 文字列を区切る方法 echo <<< EOT あああああ いいいいいい ううううううううう ええええええええええええええ EOT; echo "\n □Nowdoc"; // Nowdocはヒアドキュメントと似ているが、シングルクォートで囲んだ文字列として扱われる echo <<< 'EOT' あああああ いいいいいい ううううううううう ええええええええええええええ EOT;
算術演算子
<?php echo 1+2; echo "\n"; echo 10*3; echo "\n"; echo 10/3; echo "\n"; // 乗除算 echo 12%3; echo "\n"; // 累乗 echo 2*3; echo "\n";
変数
<?php $price = 150+80; $price = 10; echo $price; echo "\n"; echo $price*1.1; $x; $number_123; $_123; // $123; // ×数字から始まる // $nuber-123; // ×ハイフン // $_SERVER = 10; // ×予約語 $price = 100; // 文字列連結 . echo '50 + 50 は、' . $price . 'です'; $number = 10; echo "{$number}th anniversary"; // 10th anniversary
定数
<?php // 定数は慣習として大文字で定義する const TAX = 10; // 非推奨 // define(TAX, 10); echo "消費税は、", TAX, "%です";
関数
<?php $sum = 5000 + 150; // number_format 数字を千位毎にグループ化してフォーマットする // number_format ( float $number [, int $decimals = 0 ]) : string // パラメータの中の[]は省略可能という意味 // []を省略した場合は0となる $sum = number_format($sum); echo $sum, '円です';
<?php echo '足される数 >'; // STDIN ユーザのキーボードの入力を受け付ける // fgets ファイルオブストリームから文字列を取得 // trimで改行を取り除く $number = trim(fgets(STDIN)); echo '足す数 >'; $number2 = trim(fgets(STDIN)); echo $number . ' + ' . $number2 . ' = ' . $number + $number2;
制御構造
if構文
<?php echo '数字を入力してください'; $number = trim(fgets(STDIN)); // 10よりも大きいかを判断する if ($number > 10) { echo '10よりも大きいです'; } else { echo '10以下です'; } if ($number > 10): echo '10よりも大きいです'; else: echo '10以下です'; endif;
elseif
if ($number > 10) {
echo '10よりも大きいです';
} elseif ($number == 10) {
echo '10です';
} else {
echo '10以下です';
}
<?php $number = 10; // is_numeric 変数が数字または数値形式の文字列であるかを調べる // 数値または数値形式の文字列である場合にTRUE、それ以外の場合にFALSEを返す // $numeric = is_numeric($number); if (is_numeric($numeric)) { // trueを省略 echo '数字です'; } else { echo '数字ではありません'; } echo '西暦を入力してください'; $year = trim(fgets(STDIN)); if (is_numeric($year) && $year <= date('Y')) { // echo '数字が入力されました'; if ($year >= 2018) { echo '令和です'; } elseif ($year >= 1988) { echo '平成です'; } elseif ($year >= 1925) { echo '昭和です'; } elseif ($year >= 1911) { echo '大正です'; } elseif ($year >= 1867) { echo '明治です'; } else { echo '明治よりも前です'; } } else { echo '今年よりも前の数字を入力してください'; } $sum = 10 + 5; echo $sum; // 変数の再代入 // $sum = $sum + 20; $sum += 20; $sum -= 20; $sum *= 20; $sum /= 20; // インクリメント・デクリメント // $sum += 1; $sum++; // インクリメント $sum--; // デクリメント echo $sum;
データ型と型キャスト
<?php $number1 = 10; // int型 $number2 = '10'; // string型 // === $number が $numberに等しく、同じ型である場合にTRUE // (int)でint型に型キャスト if ($number1 === (int)$number2) { echo '同じです'; }
switch構文
<?php echo '色を選んでください(1.黒, 2.しろ, 3.赤)'; $color = (int)trim(fgets(STDIN)); // if ($color === 1) { // echo '黒が選ばれました'; // } elseif ($color === 2) { // echo 'しろが選ばれました'; // } elseif ($color === 3) { // echo '赤が選ばれました'; // } // switch構文 switch ($color) { case 1: echo '黒が選ばれました'; break; case 2: echo 'しろが選ばれました'; break; case 3: echo '赤が選ばべれました'; break; }
配列
<?php // $black = '黒'; // $white = 'しろ'; // $red = '赤'; // $color[0] = '黒'; // $color[1] = 'しろ'; // $color[2] = '赤'; // 配列 $color = ['黒', 'しろ', '赤']; // $color = array('黒', 'しろ', '赤'); // PHP5以前の書き方 $number = 10; $myfavorite = 2; echo $color[$myfavorite];
多次元配列
<?php // $book[0][0] = 'デザイン入門'; // $book[0][1] = 'デザインの基礎'; // $book[1][0] = 'PHP入門'; // $book[1][1] = '高度なPHP開発'; // $book[1][2] = 'Laravel入門'; // $book[2][0] = 'JavaScript入門'; $book = [ ['デザイン入門', 'デザインの基礎'], ['PHP入門', '高度なPHP開発'] ]; // echo $book[0][1]; // print_r 指定した変数に関する情報をわかりやすく出力する print_r($book);
連想配列
<?php // $pref['hokkaidou'] = '北海道'; // $pref['aomori'] = '青森県'; // $pref['iwate'] = '岩手県'; // 連想配列はindexとなるキーを設定し、関連する値を代入する // $pref = [ // 'hokkaido' => '北海道', // 'aomori' => '青森県', // 'iwate' => '岩手県' // ]; // 連想配列の中に配列を入れる $pref['hokkaido'] = [ '赤平市', '旭川市', '芦別市' ]; $pref['aomori'] = [ '青森市', '鰺ヶ沢町' ]; echo $pref['aomori'][1];
配列 ファンクション
<?php $color = ['黒', 'しろ', '赤']; // count 変数に含まれる全ての要素、あるいはオブジェクトに含まれる何かの数を数える // count ( Countable|array $value [, int $mode = COUNT_NORMAL ] ) : int // 返り値 valueに含まれる要素を返す // $max = count($color); // 3 // echo $color($max-1); // 要素は3のため // 一つ以上の要素を配列の最初に加える // array_unshift($color, '緑', '黄'); // 0] => 緑 [1] => 黄 [2] => 黒 [3] => しろ [4] => 赤 // array_push($color, '緑', '黄'); //[0] => 黒 [1] => しろ [2] => 赤 [3] => 緑 [4] => 黄 // 配列の先頭から要素を一つ取り出して消す $mycolor = array_shift($color); // [0] => しろ [1] => 赤 echo $mycolor; $mycolor = array_shift($color); // [0] => 赤 echo $mycolor; // array_pop 配列の最後の要素を取り出し消す $color = ['黒', 'しろ', '赤']; $mycolor = array_pop($color); // [0] => 黒 [1] しろ echo $mycolor; $mycolor = array_pop($color); // [0] => 黒 echo $mycolor; print_r($color); $color = ['黒', 'しろ', '赤']; $color_string = implode(',', $color); print($color_string); // 黒,しろ,赤 $color_string = implode(' - ', $color); print($color_string); // 黒 - しろ - 赤 $newarray = explode(',', $color_string); print($color_string);
for構文
<?php $color = ['黒', '白', '赤']; for ($i=0; $i<count($color); $i++) { echo $color[$i], "\n"; }
foreach
<?php $pref = [ 'hokkaido' => '北海道', 'aomori' => '青森県', 'iwate' => '岩手県' ]; // 連想配列の値を取り出す(配列でも取得できる) foreach ($pref as $pref_name) { echo ' ・ ', $pref_name, "\n"; } // 連想配列の値とキーを取り出す foreach ($pref as $pref_key => $pref_name) { echo ' ・ ', $pref_key, ':', $pref_name, "\n"; }
while構文
<?php $q1 = 5; $q2 = 10; echo $q1, '+', $q2, 'は? >'; $answer = (int)trim(fgets(STDIN)); while ($answer !== $q1+$q2) { echo 'はずれ。もう一回 >'; $answer = (int)trim(fgets(STDIN)); } echo 'あたり!';
do while
<?php // rand 乱数を生成する // rand ( int $min, int $max ) : int // $dice = rand(1, 6); // while ($dice !== 1) { // echo $dice, "\n"; // $dice = rand(1, 6); // } // echo $dice; do { $dice = rand(1, 6); echo $dice, "\n"; } while ($dice !== 1);
関数
<?php // $pref = [ // 'hokkaido' => '北海道', // 'aomori' => '青森県', // 'iwate' => '岩手県' // ]; // foreach ($pref as $pref_code => $pref_name) { // echo '・', $pref_code, ':', $pref_name, "\n"; // } // $color = [ // 'red' => '赤', // 'blue' => '青', // 'black' => '黒' // ]; // // 上記と同じことをしている // foreach ($color as $color_code => $color_name) { // echo '・', $color_code, ':', $color_name, "\n"; // } // ⬇︎⬇︎ 関数を使うと... // 連想配列を受け取って、リストにして出力する function make_list($list) { foreach ($list as $key => $value) { echo '・', $key, ':', $value, "\n"; } } $pref = [ 'hokkaido' => '北海道', 'aomori' => '青森県', 'iwate' => '岩手県' ]; make_list($pref); $color = [ 'red' => '赤', 'blue' => '青', 'black' => '黒' ]; make_list($color);
返り値
<?php // // number_format numberをフォーマットした結果を返す // $price = number_format(1000); // echo $price; function sum($num1, $num2) { $answer = $num1 + $num2; // return 後の処理は行われない return $answer; } $item_sum = sum(167, 269); echo $item_sum;
可変パラメータ
<?php // // 合計を計算する // function sum($num1, $num2, $num3) { // $answer = $num1 + $num2 + $num3; // return $answer; // } // $item_sum = sum(10, 20, 30); // echo $item_sum; // ⬇︎⬇︎ さらに出力する値を増やしたい場合(可変パラメータの後にパラメータを追加することはできない、パラメータは最後に追加すると使用できる) function sum(...$numbers) { $answer = 0; foreach ($numbers as $num) { $answer += $num; } return $answer; } $item_sum = sum(10, 20, 30, 40, 50, 60, 70, 80, 90, 100); echo $item_sum;
可変パラメータのデフォルト引数
<?php // リストを作る function make_list($list, $head = '・') { foreach($list as $key => $val) { echo $head, $key, ':', $val, "\n"; } } $pref = [ 'hokakid' => '北海道', 'aomori' => '青森県', 'iwate' => '岩手県' ]; make_list($pref); // ・hokakid:北海道 // ・aomori:青森県 // ・iwate:岩手県 make_list($pref, '→'); // →hokakid:北海道 // →aomori:青森県 // →iwate:岩手県
リファレンス(参照)渡し
<?php // $color = ['黒', '赤', '白']; // リファレンス(参照)渡し // array_shift($color); // print_r($color); // 黒がなくなる // Array // ( // [0] => 赤 // [1] => 白 // ) // $price = 10000; // // 値渡し // // number_formatファンクションの返り値を受け取った変数($new_price) // $new_price = number_format($price); // echo $price; // echo "\n"; // echo $new_price; // 配列の先頭に文字を繋げる function add_head(&$target){ for ($i=0; $i<count($target); $i++) { $target[$i] = '●' . $target[$i]; } } $color = ['黒', '赤', '白']; print_r($color); // Array // ( // [0] => 黒 // [1] => 赤 // [2] => 白 // ) add_head($color); print_r($color); // Array // ( // [0] => ●黒 // [1] => ●赤 // [2] => ●白 // )
無名関数(クロージャー)
<?php // function sum(int $a, int $b): int { // return $a + $b; // } // 数字を整形して表示する function echo_price($callback) { echo number_format($callback(1000, 500)), '円'; } // クロージャー・無名関数 // $get_sum = function ($a, $b) { // return $a + $b; // }; $get_sum = fn($a, $b) => $a + $b; // $sum = $get_sum(10, 15); // echo_price($get_sum); echo_price(function($a, $b){ return $a + $b; });
オブジェクト指向
<?php class Item { // プロパティ public string $name; // 商品名 public int $price; // 価格 // メソッドの定義 public function getPrice(string $end = '') { return number_format($this->price) . $end; } } // Itemクラスからphp_basicインスタンスの生成 $php_basic = new Item(); $php_basic->name = "PHP入門"; $php_basic->price = 15000000; echo $php_basic->name, '/', $php_basic->getPrice('円'); $js_basic = new Item(); $js_basic->name = 'JS入門'; $js_basic->price = 2300;
コンストラクター
<?php class Item { // プロパティ public string $name; // 商品名 public int $price; // 価格 // コンストラクター public function __construct(string $name, int $price = 0) { $this->name = $name; $this->price = $price; } // メソッドの定義 public function getPrice(string $end = '') { return number_format($this->price) . $end; } } // デフォルト引数が設定されているため第二引数をしてしなくても実行可能 $php_basic = new Item('PHP入門'); echo $php_basic->name, '/', $php_basic->getPrice('円'); $js_basic = new Item('JS入門', 2300);
継承
<?php class Item { // プロパティ public string $name; // 商品名 private int $price; // 価格 // public int $page; // ページ数 // コンストラクター public function __construct(string $name, int $price = 0) { $this->name = $name; $this->price = $price; } // 価格を設定する public function setPrice(int $price) { if ($price < 0) { return false; } $this->price = $price; return true; } // 価格を取得する public function getPrice(string $end = '') { return number_format($this->price) . $end; } } // Itemクラスを継承 class Book extends Item { public int $page; // 書籍のページ数 } $php_basic = new Book('PHP入門', 2300); $php_basic->page = 100; $php_basic->setPrice(1500); echo $php_basic->name, '(', $php_basic->page, 'ページ/', $php_basic->getPrice('円'); $js_basic = new Item('JS入門', 2300);
メソッドのオーバーライド
<?php class Item { // protected 親クラスと継承したクラスで参照できるプロパティ protected int $uint; // 個数(アクセス修飾子をprivateにすると継承クラスでは使用できない) public function __construct(int $uint) { $this->uint = $uint; } public function getUint(): string { return $this->uint . '個'; } } class Book extends Item { // オーバーライド(親クラスで定義されたメソッドを上書きする) public function getUint(): string { return $this->uint . '冊'; } } $eraser = new Item(10); echo $eraser->getUint(), "\n"; // 10個 $book = new Book(5); echo $book->getUint(); // 5冊
親クラスのメソッド呼び出し parent::
<?php class Item { // protected 親クラスと継承したクラスで参照できるプロパティ protected int $unit; // 個数(アクセス修飾子をprivateにすると継承クラスでは使用できない) public function __construct(int $unit) { $this->unit = $unit; } public function getUnit(): string { return $this->unit . '個'; } } class Book extends Item { private int $page; // ページ数 public function __construct(int $unit, int $page) { // parent:: 自分の親クラス parent::__construct($unit); $this->page = $page; } // オーバーライド(親クラスで定義されたメソッドを上書きする) public function getUnit(): string { return $this->unit . '冊'; } } $eraser = new Item(10); echo $eraser->getUnit(), "\n"; $book = new Book(5, 120); echo $book->getUnit();
インターフェイス
<?php // 今回であればgetPriceメソッドを必ず使用しなければならない interface ItemInterface { public function getPrice(): int; } // ItemInterfaceを実装したBookクラス class Book implements ItemInterface { private int $price; public function getPrice(): int { return $this->price; } } // ItemInterfaceを実装したPencilクラス class Pencil implements ItemInterface { private int $price; public function getPrice(): int { return $this->price; } }
抽象クラス
インターフェイスと同様、約束事を定義できるが
通常のクラスと同じようにプロパティやメソッドを定義できる
<?php // 抽象クラス abstract class Item { private $price; public function getPrice() { return $this->price; } // 抽象メソッド abstract public function getUnit(); } class Book extends Item { public function getUnit() { return '冊'; } } class Pen extends Item { public function getUnit() { return '本'; } } $book = new Book(); $book->getPrice();
トレイト
再利用可能なもの
<?php trait Tax { // 消費税を返す public function getTax(): int { return 10; } } // 外国の消費税を扱う trait ForeignTax { public function getTax(): int { return 20; } } class Item { // TaxとForeginTax2つのトレイトを使用(同じメソッド名の場合) use Tax, ForeignTax { Tax::getTax insteadof ForeignTax; ForeignTax::getTax as getForeginTax; } private int $price; } class Service { use Tax; private int $service_price; } $item = new Item; echo $item->getTax(), "\n"; echo $item->getForeginTax(), "\n"; $service = new Service; echo $service->getTax(), "\n";
名前空間
<?php require_once 'app/Shop/Book.php'; require_once 'app/Review/Book.php'; // path\to\クラス名; use app\Shop\Book; use app\Review\Book as BookReview; $book = new Book(); echo $book->getPrice(), "\n"; $book_review = new BookReview(); echo $book_review->getPoint(), "\n";
静的プロパティ・静的メソッド
<?php class Item { // 静的プロパティ public static int $tax = 10; // 消費税 // 静的メソッド public static function getTax(): int { // 静的メソッドから静的プロパティを呼び出す際はselfを使用する return self::$tax; } // // 静的メソッド内では$thisは使用できない // public static function getTax(): int { // return 15; // } } // $item = new Item(); // echo $item->getTax(); // echo Item::getTax(); echo Item::$tax; Item::$tax = 8; $item02 = new Item(); echo $item02->getTax();
インスタンスの複製(clone)
<?php class Item { public string $name; } $item01 = new Item(); $item01->name = 'PHP入門'; // 参照渡し $item02 = $item01; // 値渡し $item02 = clone($item01); $item01->name = 'JS入門'; echo $item02->name;
React Hooks あれこれ
環境構築
npx create-react-app --scripts-version バージョン アプリ名
バージョン確認方法
npm info create-react-app
構築できたか確認(自動で立ち上がる)
yarn start
基本
import React, { useState } from 'react';
const [count, setCount] = useState(0)
常に2つの要素を返すため、Javascriptの分割代入を使用してそれぞれの要素を受け取る
1つ目の要素(count)は今回であれば0(useStateの状態)が返ってくる
2つ目の要素(setCount)は関数(function)
関数にも名前をつける(キャメルケース)
import React, { useState } from 'react'; const App = () => { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) const decrement = () => setCount(count - 1) return ( <> <div>count: {count}</div> <button onClick={increment}>+1</button> <button onClick={decrement}>-1</button> </> ); } export default App;
<button onClick={increment}>+1</button>
onClickでボタンがクリックされたらイベント発火
今回であればincrement関数が呼び出される
const increment = () => setCount(count + 1)
今回であれば、状態countを+1したい
このcountを変更させるには、setCountに値を与えることでcountの状態が変更される
setCountの引数に値ではなく、関数を渡すこともできる
関数に状態を変えさせることもできる
引数には現時点での状態を返す
その値をもとに状態を変更したい場合に使用する
import React, { useState } from 'react'; const App = () => { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) const decrement = () => setCount(count - 1) const increment2 = () => setCount(previousCount => previousCount + 1) const decrement2 = () => setCount(previousCount => previousCount - 1) const reset = () => setCount(0) const dobule = () => setCount(count * 2) const divide3 = () => setCount(previousCount => previousCount % 3 === 0 ? previousCount / 3 : previousCount ) return ( <> <div>count: {count}</div> <div> <button onClick={increment}>+1</button> <button onClick={decrement}>-1</button> </div> <div> <button onClick={increment2}>+1</button> <button onClick={decrement2}>-1</button> </div> <div> <button onClick={reset}>Reset</button> <button onClick={dobule}>×2</button> <button onClick={divide3}>3の倍数の時だけ3で割る</button> </div> </> ); } export default App;
複数の状態管理
import React, { useState } from 'react'; const App = props => { const [name, setName] = useState(props.name) const [price, setPrice] = useState(props.price) const reset = () => { setPrice(props.price) setName(props.name) } return ( <> <p>現在の{name}は、{price}円です。</p> <button onClick={() => setPrice(price + 1)}>+1</button> <button onClick={() => setPrice(price - 1)}>-1</button> <button onClick={reset}>Reset</button> <input value={name} onChange={e => setName(e.target.value)}/> </> ); } App.defaultProps = { name: '', price: 1000 } export default App;
<input value={name} onChange={e => setName(e.target.value)}/>
onChangeでイベントを拾う(e)
e.target.valueでinputに入力された文字列を拾う
複数の状態を1つのオブジェクトに統合
import React, { useState } from 'react'; const App = props => { const [state, setState] = useState(props) const { name, price } = state return ( <> <p>現在の{name}は、{price}円です。</p> <button onClick={() => setState({...state, price: price + 1})}>+1</button> <button onClick={() => setState({...state, price: price - 1})}>-1</button> <button onClick={() => setState(props)}>Reset</button> <input value={name} onChange={e => setState({...state, name: e.target.value})}/> </> ); } App.defaultProps = { name: '', price: 1000 } export default App;
useEffect
// useEffectをimportする import React, { useEffect, useState } from 'react'; // 第一引数に関数を引き取ることができる // jsxのrenderingの後にuseEffectは実行される(Domのどこかの要素に変更があれば呼び出される) useEffect(() => { console.log('This is like componentDidMount or componentDidUpdate.') }) // 最初のrenderingのみ呼び出すには第二引数に空の配列を持たす useEffect(() => { console.log('This is like componentDidMount') }, []) // 特定のパラメータの描画時または変更時のみ呼び出す時(今回であればnameが描画された時または変更された時に呼び出す) useEffect(() => { console.log('This callback is for name only.') }, [name])
reducer
const events = (state = [], action) => { switch (action.type){ case 'CREATE_EVENT': const event = { title: action.title, body: action.body } const length = state.length const id = length === 0 ? 1 : state[length -1].id + 1 return [...state, { id, ...event }] case 'DELETE_EVENT': case 'DELETE_ALL_EVENTS': return [] default: return state } } export default events
const events = (state = [], action) => {}
reducerは2つの引数を受け取る 1つ目はstate(前回の状態)を受け取る 2つ目はactionを受け取る stateは未定義の場合があるため、state = []で初期化しておく
switch (action.type){ case 'CREATE_EVENT': // データを吸い上げる const event = { title: action.title, body: action.body } // stateの長さを把握する const length = state.length // stateの長さが0であれば1を返す、0でなければ最後のidに+1をして返す const id = length === 0 ? 1 : state[length -1].id + 1 // stateの最後の要素にeventデータを格納する return [...state, { id, ...event }] case 'DELETE_EVENT': case 'DELETE_ALL_EVENTS': return [] default: return state }
actionは常にtypeという属性が渡ってくる 今回であれば、typeは作成する場合、削除される場合、そういったtype以外の場合でswitchさせる
React Firebase 連携
構築
npx create-react-app . --template typescript
npm start
立ち上がっていればOK
必要なライブラリをインストール
npm i @material-ui/core
npm i @material-ui/icons
npm i firebase
npm i react-router-dom @types/react-router-dom
npm start
型をインストールする
yarn add -D @types/react
※reactは typescriptで書かれてない上、
型提供も元パッケージでしていないので型を別でインストールする必要がある
-Dオプション
devDependencies:開発用の依存関係
アローファンクションのテンプレートを使用できる
rafce
Firebaseとの連携
.envファイルに環境変数を設定する
REACT_APP_FIREBASE_APIKEY="" REACT_APP_FIREBASE_DOMAIN="" REACT_APP_FIREBASE_DATABASE="" REACT_APP_FIREBASE_PROJECT_ID="" REACT_APP_FIREBASE_STORAGE_BUCKET="" REACT_APP_FIREBASE_SENDER_ID="" REACT_APP_FIREBASE_APP_ID=""
firebase.tsを作成する
import firebase from "firebase/app"; import "firebase/app"; import "firebase/firestore"; import "firebase/auth"; const firebaseApp = firebase.initializeApp({ apiKey: process.env.REACT_APP_FIREBASE_APIKEY, authDomain: process.env.REACT_APP_FIREBASE_DOMAIN, databaseURL: process.env.REACT_APP_FIREBASE_DATABASE, projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, messageingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID, appId: process.env.REACT_APP_FIREBASE_APP_ID, }); export const db = firebaseApp.firestore(); export const auth = firebase.auth();
Firebaseにてデータベースを作成する
コレクションを開始
ドキュメントIDを自動生成
フィールド入力(titleなど)
タイプを指定(stringなど)
値を入力(任意)
React側で取得する
import { List } from "@material-ui/core"; import React,{useState, useEffect} from 'react'; import './App.css'; import { db } from "./firebase"; const App: React.FC = () => { const [tasks, setTasks] = useState([{id:"", title:""}]); useEffect(()=> { const unSub = db.collection("tasks").onSnapshot((snapshot)=>{ setTasks( snapshot.docs.map((doc)=> ({id: doc.id, title: doc.data().title})) ); }); return ()=> unSub(); },[]); return ( <div className="App"> {tasks.map((task) => ( <h3>{task.title}</h3> ))} </div> ); }; export default App;
firebaseから取得したtasksの内容をReact側でstateとして保持しておきたいためuseStateを使用する
// 空の内容で初期化 const [tasks, setTasks] = useState([{id:"", title:""}]);
アプリケーションが立ち上がった時にfirebaseへアクセスして
存在するデータベースのtasksの内容を取得するためuseEffectを使用する
アプリケーションが起動した最初の1回のみデータを読みに行きたいため
第二引数は空にする
useEffect(()=> {},[]);
実際にデータベースへアクセスする内容
db.collectionで取得したいデータベース名を指定する
onSnapshotでデータベースの内容を取得する
firestoreから取得した内容を(snapshot)の引数にいれて実際の処理を行う
取得したtasksのオブジェクトの一覧をsetTasksを使ってtasksのstateに格納する
snapshotの複数あるdocumentをmapで展開する
useEffect(()=> { const unSub = db.collection("tasks").onSnapshot((snapshot)=>{ setTasks( snapshot.docs.map((doc)=> ({id: doc.id, title: doc.data().title})) ); }); return ()=> unSub(); },[]);
取得した内容をブラウザへ表示する
const App: React.FC = () => { const [tasks, setTasks] = useState([{id:"", title:""}]); useEffect(()=> { const unSub = db.collection("tasks").onSnapshot((snapshot)=>{ setTasks( snapshot.docs.map((doc)=> ({id: doc.id, title: doc.data().title})) ); }); return ()=> unSub(); },[]); return ( <div className="App"> {tasks.map((task) => ( <h3 key={task.id}>{task.title}</h3> ))} </div> ); };
タスクを作る
import { FormControl, TextField } from "@material-ui/core"; import React,{useState, useEffect} from 'react'; import './App.css'; import { db } from "./firebase"; import AddToPhotosIcon from "@material-ui/icons/AddToPhotos"; const App: React.FC = () => { const [tasks, setTasks] = useState([{id:"", title:""}]); const [input, setInput] = useState(""); useEffect(()=> { const unSub = db.collection("tasks").onSnapshot((snapshot)=>{ setTasks( snapshot.docs.map((doc)=> ({id: doc.id, title: doc.data().title})) ); }); return ()=> unSub(); },[]); const newTask = (e: React.MouseEvent<HTMLButtonElement>)=>{ db.collection("tasks").add({title: input}); setInput(""); } return ( <div className="App"> <h1>Todo App by React/Firebase</h1> <FormControl> <TextField InputLabelProps={{ shrink: true, }} label="New task ?" value={input} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInput(e.target.value)} /> </FormControl> <button disabled={!input} onClick={newTask}> <AddToPhotosIcon /> </button> {tasks.map((task) => ( <h3 key={task.id}>{task.title}</h3> ))} </div> ); }; export default App;
ユーザーがタイピングした文字列を保持するためのstateをuseStateを使用して実行する
// 初期値を空にセット const [input, setInput] = useState("");
material-uiを使用してフォームを作成する
import { FormControl, TextField } from "@material-ui/core"; <FormControl> <TextField InputLabelProps={{ shrink: true, }} label="New task ?" value={input} // ユーザーがタイピングするために呼び出される関数を定義 // input stateの内容を上書きするためsetInputを毎回呼び出す(ユーザーがタイピングしたvalueの値を取得している) // typescriptではイベントオブジェクトに型を指定する必要がある onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInput(e.target.value)} /> </FormControl>
送信用のボタンを作成する
import AddToPhotosIcon from "@material-ui/icons/AddToPhotos"; // 入力フォームが空の時はボタンを押せない // ボタンが押された時に呼び出される関数(newTask)を指定する <button disabled={!input} onClick={newTask}> <AddToPhotosIcon /> </button>
ボタンを押した時の呼び出される関数を作成
const newTask = (e: React.MouseEvent<HTMLButtonElement>)=>{ // firebaseのデータベースにタスクを追加するため、db.collectionで追加したいコレクションを指定する(addでオブジェクトを渡す) db.collection("tasks").add({title: input}); // 次の新しいタスクの作成に備えてinput stateを初期化しておく setInput(""); }
※onClickなどのデータ型を知りたい場合は、onClickにカーソルを合わせると表示される