日本一わかりやすいReact-Redux入門をやる
やる時間が無いので終了
せっかくのGWなので、仕事以外の事もしたいと思うので下記の動画をやってみる
動画の内容以外にやること
- git(非公開リポジトリ)
- すべてTypeScriptで記述(
"strict": true
)
自分のレベル感
- ほぼ独学
- JavaScript歴約2年(2019-06~)
- TypeScript歴1年5ヶ月(2020-01~)
- React歴1年ぐらい(趣味でたまに触るぐらい)
- React実務経験無し
- Redux初めて触る
- Firebase触るの3回目
動作環境
- OS: macOS Big Sur11.2.3
- Node.js: v14.16.1
- npm: 7.9.0
- yarn: 1.22.5(今回は使っていないけど)
- react: 17.0.2
- react-scripts(create-react-app): 4.0.3
最終目標
- testを実装してみたい
実は0時から動画見始めてもう6まで終わっている😂
6までで動画以外でやったこと
serve
を追加
1. npm scriptにnpm run allとserveを使いbuild後の結果を見れるようコマンドに追加
{
"scripts": {
"serve": "run-s build _serve",
"_serve": "serve -s build -p 9000",
}
}
alias
を追加
2. importの相対パスを書きたくなかったのでaliasを追加
{
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"serve": "run-s build _serve",
"test": "react-app-rewired test",
"_serve": "serve -s build -p 9000",
"eject": "react-app-rewired eject"
}
}
{
"extends": "./tsconfig.paths.json",
}
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@src/*": ["src/*"]
}
}
}
const path = require('path');
module.exports = function override(config) {
config.resolve = {
...config.resolve,
alias: { '@src': path.resolve(__dirname, 'src') },
};
return config;
};
Blaze
プランに変更
3. Firebaseの2020/06/22の仕様変更で、Cloud Functionsを使うためにFirebaseのプロジェクトをBlazeプランに変更する必要があるらしいので下記の記事を見ながらプランを変更
一応$1超えるとアラートが来るように設定ちなみに具体的なファイルの中身は下記の通り😂
型はノリで付けた!動いてるからヨシ👷
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from "react-redux";
import {createStore} from "@src/reducks/store/store";
import App from './App';
import reportWebVitals from './reportWebVitals';
export const store = createStore();
ReactDOM.render(
// <React.StrictMode>
<Provider store={store}>
<App/>
</Provider>,
// </React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import React from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootStateType} from "@src/reducks/type";
import {signInAction} from "@src/reducks/users/actions";
function App() {
const dispatch = useDispatch();
const selector = useSelector((state:RootStateType) => state);
console.log(selector)
return (
<div>
<button type="button" onClick={() => dispatch(signInAction({uid: "ssssss",username: 'test'}))}>
Sign In
</button>
</div>
);
}
export default App;
import {RootStateType} from "@src/reducks/type";
//初期値
export const initialState: RootStateType = {
users: {
isSignedIn: true,
uid: "",
username: ""
}
}
import {createStore as reduxCreateStore, combineReducers} from "redux";
import {UsersReducer} from "@src/reducks/users/reducers";
export const createStore = () => {
return reduxCreateStore(
combineReducers({
users: UsersReducer
})
)
}
import {UsersType} from "@src/reducks/type";
//Userのアクションタイプの定数
export const ACTION_TYPE = {
SIGN_IN: "SIGN_IN",
SIGN_OUT: "SIGN_OUT"
} as const;
//Userがサインインする時の関数
export const signInAction = (userState: Omit<UsersType['state'], 'isSignedIn'>): UsersType['action'] => {
return {
type: ACTION_TYPE.SIGN_IN,
payload: {
isSignedIn: true,
uid: userState.uid,
username: userState.username
}
}
}
//Userがサインアウトする時の関数
export const signOutAction = (): UsersType['action'] => {
return {
type: ACTION_TYPE.SIGN_OUT,
payload: {
isSignedIn: false,
uid: "",
username: ""
}
}
}
import {initialState} from "@src/reducks/store/initialState";
import {UsersType} from "@src/reducks/type";
//userのアクションを呼ぶReducer
export const UsersReducer = (
state = initialState.users,
action: UsersType["action"]
) => {
switch (action.type) {
case "SIGN_IN":
return {
...state,
...action.payload
}
case "SIGN_OUT":
return {
...state,
...action.payload
}
default:
// console.error(`type is ${Object.values(ACTION_TYPE).join(' or ')}`);
// throw new Error(`type is ${Object.values(ACTION_TYPE).join(' or ')}`);
return state;
}
}
import {ACTION_TYPE} from "@src/reducks/users/actions";
//storeのstateの型
export type RootStateType = {
users: UsersType['state']
}
//Usersの型をまとめた型
export type UsersType = {
state: usersStateType
action: UsersActionType
}
//Usersのstateの型
type usersStateType = {
isSignedIn: boolean,
uid: string,
username: string
}
//Usersのactionの型
type UsersActionType = {
type: keyof typeof ACTION_TYPE;
payload: usersStateType;
}
redux typescriptで検索したら下記の記事が引っかかった
方法は違うけど今の所問題無さそうなので、このまま続けることにする
7
コメントに
historyのバージョンが5.0.0でpushメソッドを使うと以下のエラーが起きるみたいです。
Uncaught Could not find router reducer in state tree, it must be mounted under "router"
以下のコマンドでhistoryのバージョンを4.10.1でインストールし直してみてください。
npm install --save history@4.10.1
と書いてあって震えながら公式のマイグレーションガイド開いていたけど大丈夫だった(4.10.1だった)
あれ?何故かhistoryが4.10.1になってるな🤔
インストール時にversion指定したっけ?
-import React from 'react';
-import {useDispatch, useSelector} from "react-redux";
-import {RootStateType} from "@src/reducks/type";
-import {signInAction} from "@src/reducks/users/actions";
-
-function App() {
- const dispatch = useDispatch();
- const selector = useSelector((state:RootStateType) => state);
- console.log(selector)
+import React from "react";
+import {Router} from "@src/Router";
+export const App: React.VFC = () => {
return (
- <div>
- <button type="button" onClick={() => dispatch(signInAction({uid: "ssssss",username: 'test'}))}>
- Sign In
- </button>
- </div>
- );
-}
-
-export default App;
+ <main>
+ <Router/>
+ </main>
+ )
+}
import ReactDOM from 'react-dom';
import {Provider} from "react-redux";
import {createStore} from "@src/reducks/store/store";
-import App from './App';
+import {App} from './App';
import reportWebVitals from './reportWebVitals';
+import {createBrowserHistory} from "history";
+import {ConnectedRouter} from "connected-react-router";
-export const store = createStore();
+const history = createBrowserHistory();
+export const store = createStore(history);
ReactDOM.render(
// <React.StrictMode>
<Provider store={store}>
+ <ConnectedRouter history={history}>
<App/>
+ </ConnectedRouter>
</Provider>,
// </React.StrictMode>,
document.getElementById('root')
history
の型がわからなかったけどconnectRouter
とrouterMiddleware
の型がHistory<unknown>
なので同じにしてみた
-import {createStore as reduxCreateStore, combineReducers} from "redux";
+import {createStore as reduxCreateStore, combineReducers, applyMiddleware} from "redux";
+import {connectRouter, routerMiddleware} from 'connected-react-router'
import {UsersReducer} from "@src/reducks/users/reducers";
+import {History} from "history";
-export const createStore = () => {
+export const createStore = (history: History) => {
return reduxCreateStore(
combineReducers({
+ router: connectRouter(history),
users: UsersReducer
- })
+ }),
+ applyMiddleware(
+ routerMiddleware(history),
+ ),
)
}
import React from "react";
export const Home:React.VFC = () => {
return (
<>
this is home
</>
)
}
import React from "react";
import {useDispatch, useSelector} from "react-redux";
import {push} from "connected-react-router";
export const Login:React.VFC = () => {
const dispatch = useDispatch();
const selector = useSelector(state => state);
console.log(selector);
return (
<>
<h2>ログイン</h2>
<button type="button" onClick={() => dispatch(push('/'))}>
ログインする
</button>
</>
)
}
export {Login} from "@src/pages/Login";
export {Home} from "@src/pages/Home";
とりあえずここまで
再開!
useSelectorの型補完が出ない!
const selector = useSelector<RootStateType, RootStateType>(state => state);
ジェネリクスで対応しようと思ったけど毎回書くのめんどくさいので何かいい方法はないか調べたら下記の記事が見つかった😀
-const selector = useSelector<RootStateType, RootStateType>(state => state);
+const selector = useSelector(state => state);
// 参考記事
// https://qiita.com/Takepepe/items/6addcb1b0facb8c6ff1f#ambient-module-%E5%AE%A3%E8%A8%80%E3%81%A7-overload-%E3%81%99%E3%82%8B
import 'react-redux'
import {RootStateType} from "@src/reducks/type";
// ______________________________________________________
//
// react-reduxのDefaultRootStateをRootStateTypeでオーバーライドする
declare module 'react-redux' {
interface DefaultRootState extends RootStateType {
}
}
8の全体コード
import React from "react";
import {getUserId} from "@src/reducks/users/selector";
import {useSelector} from "react-redux";
export const Home: React.VFC = () => {
const selector = useSelector(state => state);
const uid = getUserId(selector);
return (
<>
this is home
<p>{uid}</p>
</>
)
}
import React from "react";
import {useDispatch, useSelector} from "react-redux";
import {push} from "connected-react-router";
import {signInAction} from "@src/reducks/users/actions";
export const Login:React.VFC = () => {
const dispatch = useDispatch();
const selector = useSelector(state => state);
console.log(selector);
return (
<>
<h2>ログイン</h2>
<button type="button" onClick={() => {
dispatch(signInAction({uid: "00001", username: "test"}))
dispatch(push('/'))
}}>
ログインする
</button>
</>
)
}
// 参考記事
// https://qiita.com/Takepepe/items/6addcb1b0facb8c6ff1f#ambient-module-%E5%AE%A3%E8%A8%80%E3%81%A7-overload-%E3%81%99%E3%82%8B
import 'react-redux'
import {RootStateType} from "@src/reducks/type";
// ______________________________________________________
//
// react-reduxのDefaultRootStateをRootStateTypeでオーバーライドする
declare module 'react-redux' {
interface DefaultRootState extends RootStateType {
}
}
import {createSelector} from "reselect";
import {RootStateType} from "@src/reducks/type";
const usersSelector = (state:RootStateType) => state.users;
export const getUserId = createSelector(
[usersSelector],
state => state.uid
)
signInのdispatchの型がわからない
useDispatch()
の返り値の型がDispatch<any>(export type React.Dispatch<A>)
なのでReactからインポートされた型なのが分かる
onClick={() => dispatch(signIn())}しても動かない
signInにconsole.logでクリックが走ってるか見た所動いていたので、isSignedInの値も見たら初期値をtrue
にしていた😂
import {RootStateType} from "@src/reducks/type";
//初期値
export const initialState: RootStateType = {
users: {
isSignedIn: false,
uid: "",
username: ""
}
}
8全体のコード
import {createStore as reduxCreateStore, combineReducers, applyMiddleware} from "redux";
import {connectRouter, routerMiddleware} from 'connected-react-router'
import {UsersReducer} from "@src/reducks/users/reducers";
import {History} from "history";
import thunk from "redux-thunk";
export const createStore = (history: History) => {
return reduxCreateStore(
combineReducers({
router: connectRouter(history),
users: UsersReducer
}),
applyMiddleware(
routerMiddleware(history),
thunk
),
)
}
githubのjsonの型はとりあえずダミー
import React from "react";
import {signInAction} from "@src/reducks/users/actions";
import {push} from "connected-react-router";
import {RootStateType} from "@src/reducks/type";
export const signIn = () => {
return async (dispatch: React.Dispatch<unknown>, getState: () => RootStateType) => {
const {users} = getState();
const isSignedIn = users.isSignedIn;
if (isSignedIn) return;
const url = "https://api.github.com/users/hisho";
const response: {
login: string
} = await fetch(url).then(res => res.json()).catch(() => null);
const username = response.login;
dispatch(signInAction({
uid: "00001",
username
}));
dispatch(push('/'));
}
}
import {createSelector} from "reselect";
import {RootStateType} from "@src/reducks/type";
const usersSelector = (state: RootStateType) => state.users;
export const getUserId = createSelector(
[usersSelector],
state => state.uid
)
export const getUsername = createSelector(
[usersSelector],
state => state.username
)
import React from "react";
import {useDispatch} from "react-redux";
import {signIn} from "@src/reducks/users/operations";
export const Login:React.VFC = () => {
const dispatch = useDispatch();
return (
<>
<h2>ログイン</h2>
<button type="button" onClick={() => dispatch(signIn())}>
ログインする
</button>
</>
)
}
import React from "react";
import {getUserId, getUsername} from "@src/reducks/users/selector";
import {useSelector} from "react-redux";
export const Home: React.VFC = () => {
const selector = useSelector(state => state);
const uid = getUserId(selector);
const username = getUsername(selector);
return (
<>
this is home
<p>ユーザーID:{uid}</p>
<p>ユーザー名:{username}</p>
</>
)
}
コメントアウトを書くのを忘れていたので簡単なコメントアウトを追記していく
import {createStore as reduxCreateStore, combineReducers, applyMiddleware} from "redux";
import {connectRouter, routerMiddleware} from 'connected-react-router'
import {UsersReducer} from "@src/reducks/users/reducers";
import {History} from "history";
import thunk from "redux-thunk";
//ストアを作成する
export const createStore = (history: History) => {
return reduxCreateStore(
//管理するステート
combineReducers({
router: connectRouter(history),
users: UsersReducer
}),
//ミドルウェアの設定
applyMiddleware(
routerMiddleware(history),
thunk
),
)
}
import React from "react";
import {signInAction} from "@src/reducks/users/actions";
import {push} from "connected-react-router";
import {RootStateType} from "@src/reducks/type";
//サインインする時に実行する関数
export const signIn = () => {
return async (dispatch: React.Dispatch<unknown>, getState: () => RootStateType) => {
//現在のユーザーの値を取得
const {users} = getState();
//現在ユーザーがサインインしているかどうか
const isSignedIn = users.isSignedIn;
//サインインしている場合は早期リターン
if (isSignedIn) return;
//ダミー
const url = "https://api.github.com/users/hisho";
const response: {
login: string
} = await fetch(url).then(res => res.json()).catch(() => null);
const username = response.login;
//uidとusernameを変更してsignInActionを実行
//signInAction中でisSignedInをtrueに設定しているためisSignedInは不要
dispatch(signInAction({
uid: "00001",
username
}));
//サインイン後にトップページへリダイレクト
dispatch(push('/'));
}
}
import {createSelector} from "reselect";
import {RootStateType} from "@src/reducks/type";
const usersSelector = (state: RootStateType) => state.users;
//selectorを受け取り現在のユーザーidを返す関数
export const getUserId = createSelector(
[usersSelector],
state => state.uid
)
//selectorを受け取り現在のユーザー名を返す関数
export const getUsername = createSelector(
[usersSelector],
state => state.username
)
selector.ts
が単数形になっていたためselectors.ts
にリネーム
10
今回はClassを使わないので視聴のみ
実践編#1
視聴のみ
実践編#2
見終わった!
今からまとめる!
拡張入れたらシンタックスハイライトは効いた
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{usersId} {
allow read: if request.auth.uid != null;
allow create;
allow update: if request.auth.uid == userId;
allow delete: if request.auth.uid == userId;
}
}
}
gitからassetsフォルダを落としてcssを読み込ませた
import React from "react";
import {Router} from "@src/Router";
import './assets/reset.css';
import './assets/style.css';
export const App: React.VFC = () => {
return (
<main>
<Router/>
</main>
)
}
import React, {useCallback, useState, ChangeEvent} from "react";
import {PrimaryButton, TextInput} from "@src/components/UIkit";
import {signIn} from "@src/reducks/users/operations";
import {useDispatch} from "react-redux";
export const SignIn = () => {
const dispatch = useDispatch()
//emailのstate
const [email, setEmail] = useState("");
//passwordのstate
const [password, setPassword] = useState("");
//emailのinputがchangeしたら走るイベント
const inputEmail = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをemailにセットする
setEmail(event.target.value);
}, [setEmail]);
//passwordのinputがchangeしたら走るイベント
const inputPassword = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをpasswordにセットする
setPassword(event.target.value);
}, [setPassword]);
return (
<div className="c-section-container">
<h2 className="u-text__headline u-text-center">サインイン</h2>
<div className="module-spacer--medium" aria-hidden="true"/>
<TextInput
label="メールアドレス"
value={email}
type="email"
onChange={inputEmail}
/>
<TextInput
label="パスワード"
value={password}
type="password"
onChange={inputPassword}
/>
<div className="module-spacer--medium" aria-hidden="true"/>
<div className="center">
<PrimaryButton label="サインイン" onClick={() => dispatch(signIn(email, password))}/>
</div>
</div>
)
}
import React, {useCallback, useState, ChangeEvent} from "react";
import {PrimaryButton, TextInput} from "@src/components/UIkit";
import {signUp} from "@src/reducks/users/operations";
import {useDispatch} from "react-redux";
export const SignUp = () => {
const dispatch = useDispatch()
//usernameのstate
const [username, setUsername] = useState("");
//emailのstate
const [email, setEmail] = useState("");
//passwordのstate
const [password, setPassword] = useState("");
//confirmPasswordのstate
const [confirmPassword, setConfirmPassword] = useState("");
//usernameのinputがchangeしたら走るイベント
const inputUsername = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをusernameにセットする
setUsername(event.target.value);
}, [setUsername]);
//emailのinputがchangeしたら走るイベント
const inputEmail = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをemailにセットする
setEmail(event.target.value);
}, [setEmail]);
//passwordのinputがchangeしたら走るイベント
const inputPassword = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをpasswordにセットする
setPassword(event.target.value);
}, [setPassword]);
//confirmPasswordのinputがchangeしたら走るイベント
const inputConfirmPassword = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをconfirmPasswordにセットする
setConfirmPassword(event.target.value);
}, [setConfirmPassword]);
return (
<div className="c-section-container">
<h2 className="u-text__headline u-text-center">アカウント登録</h2>
<div className="module-spacer--medium" aria-hidden="true"/>
<TextInput
label="ユーザー名"
value={username}
onChange={inputUsername}
/>
<TextInput
label="メールアドレス"
value={email}
type="email"
onChange={inputEmail}
/>
<TextInput
label="パスワード"
value={password}
type="password"
onChange={inputPassword}
/>
<TextInput
label="パスワード確認用"
value={confirmPassword}
type="password"
onChange={inputConfirmPassword}
/>
<div className="module-spacer--medium" aria-hidden="true"/>
<div className="center">
<PrimaryButton label="アカウントを登録する" onClick={() => dispatch(signUp(username, email, password, confirmPassword))}/>
</div>
</div>
)
}
Loginは削除した
export {Home} from "@src/pages/Home";
export {SignUp} from "@src/pages/SignUp";
export {SignIn} from "@src/pages/SignIn";
import React from "react";
import {Route, Switch} from "react-router";
import {Home, SignIn, SignUp} from "@src/pages";
export const Router: React.VFC = () => {
return (
<Switch>
<Route exact path="/signin" component={SignIn}/>
<Route exact path="/signup" component={SignUp}/>
<Route exact path="(/)?" component={Home}/>
</Switch>
)
}
import React from "react";
import {TextField, TextFieldProps} from "@material-ui/core";
type TextInputPropsType = Omit<TextFieldProps, 'margin' | 'autoComplete'>;
export const TextInput: React.VFC<TextInputPropsType> = (
{
fullWidth = true,
multiline = false,
required = true,
rows = 1,
type = "text",
...others
}) => {
return (
<TextField
autoComplete="off"
fullWidth={fullWidth}
multiline={multiline}
rows={rows}
required={required}
type={type}
margin="dense"
{...others}
/>
)
}
import React from "react";
import {Button, ButtonProps} from "@material-ui/core";
import {makeStyles} from "@material-ui/styles";
type PrimaryButtonPropsType = Omit<ButtonProps, 'variant'> & {
label: string
}
const useStyles = makeStyles({
"button": {
backgroundColor: "#4dd0e1",
color: "#333",
fontSize: 16,
height: 48,
marginBottom: 16,
width: 256
}
})
export const PrimaryButton: React.VFC<PrimaryButtonPropsType> = (
{
label,
...others
}) => {
const classes = useStyles();
return (
<Button className={classes.button} variant="contained" {...others}>
{label}
</Button>
)
}
export {TextInput} from '@src/components/UIkit/TextInput';
export {PrimaryButton} from "@src/components/UIkit/PrimaryButton";
roleを追加した
import {RootStateType} from "@src/reducks/type";
//初期値
export const initialState: RootStateType = {
users: {
isSignedIn: false,
role: "",
uid: "",
username: ""
}
}
これもroleを追加した
import {ACTION_TYPE} from "@src/reducks/users/actions";
//storeのstateの型
export type RootStateType = {
users: UsersType['state']
}
//Usersの型をまとめた型
export type UsersType = {
state: usersStateType
action: UsersActionType
}
//Usersのstateの型
type usersStateType = {
isSignedIn: boolean;
role: string;
uid: string;
username: string;
}
//Usersのactionの型
type UsersActionType = {
type: keyof typeof ACTION_TYPE;
payload: usersStateType;
}
これもroleを追加した
import {UsersType} from "@src/reducks/type";
//Userのアクションタイプの定数
export const ACTION_TYPE = {
SIGN_IN: "SIGN_IN",
SIGN_OUT: "SIGN_OUT"
} as const;
//Userがサインインする時の関数
export const signInAction = (userState: Omit<UsersType['state'], 'isSignedIn'>): UsersType['action'] => {
return {
type: ACTION_TYPE.SIGN_IN,
payload: {
isSignedIn: true,
role: userState.role,
uid: userState.uid,
username: userState.username
}
}
}
//Userがサインアウトする時の関数
export const signOutAction = (): UsersType['action'] => {
return {
type: ACTION_TYPE.SIGN_OUT,
payload: {
isSignedIn: false,
role: "",
uid: "",
username: ""
}
}
}
import React from "react";
import {push} from "connected-react-router";
import {auth, db, FirebaseTimestamp} from "@src/firebase";
import {signInAction} from "@src/reducks/users/actions";
import {RootStateType} from "@src/reducks/type";
//サインインする時に実行する関数
export const signIn = (email: string, password: string) => {
return async (dispatch: React.Dispatch<unknown>) => {
//バリデーション
//emailとpasswordが空の場合alertを出す
if (email === "" || password === '') {
alert("必須項目が未入力です");
return false;
}
//メアドとパスワードで認証する
auth.signInWithEmailAndPassword(email, password)
.then(result => {
const user = result.user;
if (user) {
const uid = user.uid;
//firebaseのusersからuidを検索してgetする
db.collection('users').doc(uid).get()
.then((snapshot) => {
//snapshotは返った来たユーザーのdata
//TODO asを無くす
const data = snapshot.data() as Omit<RootStateType["users"], 'isSignedIn'>;
//ユーザーの認証情報をセットする
dispatch(signInAction({
role: data.role,
uid: data.uid,
username: data.username
}))
//Homeに遷移させる
dispatch(push('/'))
})
}
})
}
}
export const signUp = (username: string, email: string, password: string, confirmPassword: string) => {
return async (dispatch: React.Dispatch<unknown>) => {
//バリデーション
//usernameとemailとpasswordとconfirmPasswordが空の場合alertを出す
if (username === "" || email === "" || password === '' || confirmPassword === "") {
alert("必須項目が未入力です");
return false;
}
//passwordとconfirmPasswordが一致しない場合alertを出す
if (password !== confirmPassword) {
alert("パスワードが一致しません。");
return false;
}
//emailとpasswordでユーザーを作成する
return auth.createUserWithEmailAndPassword(email, password)
.then(result => {
const user = result.user;
//アカウント作成が成功していたら処理を続ける
if (user) {
const uid = user.uid;
//現在の時間を設定する
const timestamp = FirebaseTimestamp.now();
//ユーザーのデータの雛形に当てはめる
const userInitialData = {
created_at: timestamp,
email,
role: "customer",
uid,
updated_at: timestamp,
username
}
//firebaseのusersのuidが一致すれば保存しhomeに遷移させる
db.collection('users').doc(uid).set(userInitialData)
.then(() => {
dispatch(push('/'));
})
}
})
}
}
snapshot.data()
にasを使っているのでいつか直したい!
バリデーションもちゃんと実装したい
一旦休憩
再開!
結構ハマった
const Auth = ({children}) => {
return children
}
これできないのね
必ずReact.Fragment
で加工必要があるっぽい
実践編#3
import React from "react";
import {push} from "connected-react-router";
import {auth, db, FirebaseTimestamp} from "@src/firebase";
import {signInAction, signOutAction} from "@src/reducks/users/actions";
import {RootStateType} from "@src/reducks/type";
import firebase from "firebase/app";
//firebaseのUserとdispatchを受け取りサインインする関数
const setFirebaseUserData = async (user: firebase.User, dispatch: React.Dispatch<unknown>) => {
const uid = user.uid;
//firebaseのusersからuidを検索してgetする
db.collection('users').doc(uid).get()
.then((snapshot) => {
//snapshotは返った来たユーザーのdata
//TODO asを無くす
const data = snapshot.data() as Omit<RootStateType["users"], 'isSignedIn'>;
//ユーザーの認証情報をセットする
dispatch(signInAction({
role: data.role,
uid: data.uid,
username: data.username
}))
})
}
//サインインしているかどうか監視し返す関数
export const listenAuthState = () => {
return async (dispatch: React.Dispatch<unknown>) => {
return auth.onAuthStateChanged((user) => {
//サインインしている場合
if (user) {
setFirebaseUserData(user, dispatch);
//ユーザーが存在しない場合
} else {
//signinページに遷移させる
dispatch(push('/signin'))
}
})
}
}
//サインインする時に実行する関数
export const signIn = (email: string, password: string) => {
return async (dispatch: React.Dispatch<unknown>) => {
//バリデーション
//emailとpasswordが空の場合alertを出す
//TODO バリデーションをちゃんとする
if (email === "" || password === '') {
alert("必須項目が未入力です");
return false;
}
//メアドとパスワードで認証する
auth.signInWithEmailAndPassword(email, password)
.then(result => {
const user = result.user;
//サインインしている場合
if (user) {
setFirebaseUserData(user, dispatch);
//Homeに遷移させる
dispatch(push('/'));
}
})
}
}
export const signUp = (username: string, email: string, password: string, confirmPassword: string) => {
return async (dispatch: React.Dispatch<unknown>) => {
//バリデーション
//usernameとemailとpasswordとconfirmPasswordが空の場合alertを出す
//TODO バリデーションをちゃんとする
if (username === "" || email === "" || password === '' || confirmPassword === "") {
alert("必須項目が未入力です");
return false;
}
//passwordとconfirmPasswordが一致しない場合alertを出す
//TODO バリデーションをちゃんとする
if (password !== confirmPassword) {
alert("パスワードが一致しません。");
return false;
}
//emailとpasswordでユーザーを作成する
return auth.createUserWithEmailAndPassword(email, password)
.then(result => {
const user = result.user;
//アカウント作成が成功していたら処理を続ける
if (user) {
const uid = user.uid;
//現在の時間を設定する
const timestamp = FirebaseTimestamp.now();
//ユーザーのデータの雛形に当てはめる
const userInitialData = {
created_at: timestamp,
email,
role: "customer",
uid,
updated_at: timestamp,
username
}
//firebaseのusersのuidが一致すれば保存しhomeに遷移させる
db.collection('users').doc(uid).set(userInitialData)
.then(() => {
dispatch(push('/'));
})
}
})
}
}
//サインアウトする時に実行する関数
export const signOut = () => {
return async (dispatch: React.Dispatch<unknown>) => {
auth.signOut()
.then(() => {
dispatch(signOutAction());
dispatch(push('/signin'))
})
}
}
//パスワードをリセットする時に実行する関数
export const resetPassword = (email: string) => {
return async (dispatch: React.Dispatch<unknown>) => {
//メールアドレスに入力がない場合
if (email === '') {
alert('メールアドレスが空です。');
return false;
} else {
return auth.sendPasswordResetEmail(email)
.then(() => {
alert('パスワードのリセットメールを送信しました。');
dispatch(push('/signin'));
}).catch(() => {
alert('パスワードリセットに失敗しました。');
})
}
}
}
import {createSelector} from "reselect";
import {RootStateType} from "@src/reducks/type";
const usersSelector = (state: RootStateType) => state.users;
//selectorを受け取り現在ユーザーがサインインしているかどうがを返す関数
export const getIsSignedIn = createSelector(
[usersSelector],
state => state.isSignedIn
)
//selectorを受け取り現在のユーザーidを返す関数
export const getUserId = createSelector(
[usersSelector],
state => state.uid
)
//selectorを受け取り現在のユーザー名を返す関数
export const getUsername = createSelector(
[usersSelector],
state => state.username
)
import React from "react";
import {getUserId, getUsername} from "@src/reducks/users/selectors";
import {useDispatch, useSelector} from "react-redux";
import {signOut} from "@src/reducks/users/operations";
export const Home: React.VFC = () => {
const dispatch = useDispatch();
const selector = useSelector(state => state);
const uid = getUserId(selector);
const username = getUsername(selector);
return (
<>
this is home
<p>ユーザーID:{uid}</p>
<p>ユーザー名:{username}</p>
<button type="button" onClick={() => dispatch(signOut())}>サインアウト</button>
</>
)
}
import React, {useCallback, useState, ChangeEvent} from "react";
import {PrimaryButton, TextInput} from "@src/components/UIkit";
import {resetPassword} from "@src/reducks/users/operations";
import {useDispatch} from "react-redux";
import {push} from "connected-react-router";
export const Reset = () => {
const dispatch = useDispatch()
//emailのstate
const [email, setEmail] = useState("");
//emailのinputがchangeしたら走るイベント
const inputEmail = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをemailにセットする
setEmail(event.target.value);
}, [setEmail]);
return (
<div className="c-section-container">
<h2 className="u-text__headline u-text-center">リセット</h2>
<div className="module-spacer--medium" aria-hidden="true"/>
<TextInput
label="メールアドレス"
value={email}
type="email"
onChange={inputEmail}
/>
<div className="module-spacer--medium" aria-hidden="true"/>
<div className="center">
<PrimaryButton label="Reset Password" onClick={() => dispatch(resetPassword(email))}/>
<div className="module-spacer--medium" aria-hidden="true"/>
<button type="button" onClick={() => dispatch(push('/signin'))}>アカウントをお持ちの方はこちら</button>
<button type="button" onClick={() => dispatch(push('/signup'))}>アカウントをお持ちではない方はこちら</button>
</div>
</div>
)
}
import React, {useCallback, useState, ChangeEvent} from "react";
import {PrimaryButton, TextInput} from "@src/components/UIkit";
import {signIn} from "@src/reducks/users/operations";
import {useDispatch} from "react-redux";
import {push} from "connected-react-router";
export const SignIn = () => {
const dispatch = useDispatch()
//emailのstate
const [email, setEmail] = useState("");
//passwordのstate
const [password, setPassword] = useState("");
//emailのinputがchangeしたら走るイベント
const inputEmail = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをemailにセットする
setEmail(event.target.value);
}, [setEmail]);
//passwordのinputがchangeしたら走るイベント
const inputPassword = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをpasswordにセットする
setPassword(event.target.value);
}, [setPassword]);
return (
<div className="c-section-container">
<h2 className="u-text__headline u-text-center">サインイン</h2>
<div className="module-spacer--medium" aria-hidden="true"/>
<TextInput
label="メールアドレス"
value={email}
type="email"
onChange={inputEmail}
/>
<TextInput
label="パスワード"
value={password}
type="password"
onChange={inputPassword}
/>
<div className="module-spacer--medium" aria-hidden="true"/>
<div className="center">
<PrimaryButton label="サインイン" onClick={() => dispatch(signIn(email, password))}/>
<div className="module-spacer--medium" aria-hidden="true"/>
<button type="button" onClick={() => dispatch(push('/signup'))}>アカウントをお持ちでない方はこちら</button>
<button type="button" onClick={() => dispatch(push('/signin/reset'))}>パスワードを忘れた方はこちら</button>
</div>
</div>
)
}
import React, {useCallback, useState, ChangeEvent} from "react";
import {PrimaryButton, TextInput} from "@src/components/UIkit";
import {signUp} from "@src/reducks/users/operations";
import {useDispatch} from "react-redux";
import {push} from "connected-react-router";
export const SignUp = () => {
const dispatch = useDispatch()
//usernameのstate
const [username, setUsername] = useState("");
//emailのstate
const [email, setEmail] = useState("");
//passwordのstate
const [password, setPassword] = useState("");
//confirmPasswordのstate
const [confirmPassword, setConfirmPassword] = useState("");
//usernameのinputがchangeしたら走るイベント
const inputUsername = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをusernameにセットする
setUsername(event.target.value);
}, [setUsername]);
//emailのinputがchangeしたら走るイベント
const inputEmail = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをemailにセットする
setEmail(event.target.value);
}, [setEmail]);
//passwordのinputがchangeしたら走るイベント
const inputPassword = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをpasswordにセットする
setPassword(event.target.value);
}, [setPassword]);
//confirmPasswordのinputがchangeしたら走るイベント
const inputConfirmPassword = useCallback((event: ChangeEvent<HTMLInputElement>) => {
//現在のvalueをconfirmPasswordにセットする
setConfirmPassword(event.target.value);
}, [setConfirmPassword]);
return (
<div className="c-section-container">
<h2 className="u-text__headline u-text-center">アカウント登録</h2>
<div className="module-spacer--medium" aria-hidden="true"/>
<TextInput
label="ユーザー名"
value={username}
onChange={inputUsername}
/>
<TextInput
label="メールアドレス"
value={email}
type="email"
onChange={inputEmail}
/>
<TextInput
label="パスワード"
value={password}
type="password"
onChange={inputPassword}
/>
<TextInput
label="パスワード確認用"
value={confirmPassword}
type="password"
onChange={inputConfirmPassword}
/>
<div className="module-spacer--medium" aria-hidden="true"/>
<div className="center">
<PrimaryButton label="アカウントを登録する" onClick={() => dispatch(signUp(username, email, password, confirmPassword))}/>
<div className="module-spacer--medium" aria-hidden="true"/>
<button type="button" onClick={() => dispatch(push('/signin'))}>アカウントをお持ちの方はこちら</button>
<button type="button" onClick={() => dispatch(push('/signin/reset'))}>パスワードを忘れた方はこちら</button>
</div>
</div>
)
}
export {Home} from "@src/pages/Home";
export {SignUp} from "@src/pages/SignUp";
export {SignIn} from "@src/pages/SignIn";
export {Reset} from "@src/pages/Reset";
import React, {useEffect} from "react";
import {useDispatch, useSelector} from "react-redux";
import {getIsSignedIn} from "@src/reducks/users/selectors";
import {listenAuthState} from "@src/reducks/users/operations";
type AuthPropsType = {
children: React.ReactNode
}
export const Auth: React.VFC<AuthPropsType> = ({children}) => {
const dispatch = useDispatch();
const selector = useSelector(state => state);
const isSignedIn = getIsSignedIn(selector);
useEffect(() => {
if (!isSignedIn) {
dispatch(listenAuthState());
}
}, [])
return <>{isSignedIn && children}</>
}
import React from "react";
import {Route, Switch} from "react-router";
import {Home, SignIn, SignUp, Reset} from "@src/pages";
import {Auth} from "@src/Auth";
export const Router: React.VFC = () => {
return (
<Switch>
<Route exact path="/signin" component={SignIn}/>
<Route exact path="/signin/reset" component={Reset}/>
<Route exact path="/signup" component={SignUp}/>
<Auth>
<Route exact path="(/)?" component={Home}/>
</Auth>
</Switch>
)
}
実践編#4
とりあえず、アクションはダミー
import {RootStateType} from "@src/reducks/type";
//初期値
export const initialState: RootStateType = {
products: {
list: []
},
users: {
isSignedIn: false,
role: "",
uid: "",
username: ""
}
}
Redux loggerを追加
import {createStore as reduxCreateStore, combineReducers, applyMiddleware} from "redux";
import {connectRouter, routerMiddleware} from 'connected-react-router'
import {UsersReducer} from "@src/reducks/users/reducers";
import {ProductsReducer} from "@src/reducks/products/reducers";
import {History} from "history";
import thunk from "redux-thunk";
import {createLogger} from "redux-logger";
//ストアを作成する
export const createStore = (history: History) => {
const logger = createLogger({
collapsed: true,
diff: true
})
return reduxCreateStore(
//管理するステート
combineReducers({
router: connectRouter(history),
users: UsersReducer,
products: ProductsReducer
}),
//ミドルウェアの設定
applyMiddleware(
logger,
routerMiddleware(history),
thunk
),
)
}
//Productsのアクションタイプの定数
export const ACTION_TYPE = {
SIGN_IN: "SIGN_IN",
} as const;
import firebase from "firebase/app";
import 'firebase/auth';
import 'firebase/firestore';
import 'firebase/storage';
import 'firebase/functions';
import {firebaseConfig} from "@src/firebase/config";
//firebaseの初期化
firebase.initializeApp(firebaseConfig);
//firebase関連を予めexportしておく
export const auth = firebase.auth();
export const db = firebase.firestore();
export const storage = firebase.storage();
export const functions = firebase.functions();
export const FirebaseTimestamp = firebase.firestore.Timestamp;
export type FirebaseTimestampType = firebase.firestore.Timestamp;
productDataTypeがしょぼいので今後書き直す
import React from "react";
import {db, FirebaseTimestamp, FirebaseTimestampType} from "@src/firebase";
import {push} from "connected-react-router";
//firebaseのdbのproductsを一旦変数に代入
const productsRef = db.collection('products');
//TODO idとcreate_atは必須なのでpartialを無くす
//productのdataの型
type productDataType = {
id?: string;
name: string;
description: string;
category: string;
gender: string;
price: number;
update_at: FirebaseTimestampType;
created_at?: FirebaseTimestampType;
}
//商品を追加、編集する時に実行される関数
export const saveProducts = (name: string, description: string, category: string, gender: string, price: string) => {
return async (dispatch: React.Dispatch<unknown>) => {
//現在の時刻
const timestamp = FirebaseTimestamp.now();
//productのdata(更新用)
const data: productDataType = {
name,
description,
category,
gender,
price: parseInt(price, 10),
update_at: timestamp,
}
//firebaseが自動生成するidを取得する
const {id} = productsRef.doc();
//productのidにfirebaseが自動生成したidをセットする
data.id = id;
//productのcreate_atに作った時刻をセットする
data.created_at = timestamp;
//firebaseのproductのidに上記のdataをセットする
return productsRef.doc(id).set(data)
.then(() => {
dispatch(push('/'))
}).catch((e) => {
throw new Error(e);
})
}
}
気がついたけど、エラーはいらないかも😂
import {initialState} from "@src/reducks/store/initialState";
import {ProductsType} from "@src/reducks/type";
//productsのアクションを呼ぶReducer
export const ProductsReducer = (
state = initialState.users,
action: ProductsType["action"]
) => {
switch (action.type) {
default:
// console.error(`type is ${Object.values(ACTION_TYPE).join(' or ')}`);
// throw new Error(`type is ${Object.values(ACTION_TYPE).join(' or ')}`);
return state;
}
}
import {createSelector} from "reselect";
import {RootStateType} from "@src/reducks/type";
const productsSelector = (state: RootStateType) => state.products;
//selectorを受け取り現在プロダクトのリストを返す関数
export const getList = createSelector(
[productsSelector],
state => state.list
)
productsが増えるのでACTION_TYPEに名前空間を追加
import * as Users from "@src/reducks/users/actions";
//storeのstateの型
export type RootStateType = {
products: ProductsType["state"]
users: UsersType['state']
}
//Productsの型をまとめた型
export type ProductsType = {
state: ProductsStateType
action: ProductsActionType
}
//Usersの型をまとめた型
export type UsersType = {
state: UsersStateType
action: UsersActionType
}
//Productsのstateの型
type ProductsStateType = {
list: string[]
}
//Productsのactionの型
type ProductsActionType = {
//TODO Productsのアクションを作る
type: keyof typeof Users.ACTION_TYPE;
payload: UsersStateType;
}
//Usersのstateの型
type UsersStateType = {
isSignedIn: boolean;
role: string;
uid: string;
username: string;
}
//Usersのactionの型
type UsersActionType = {
type: keyof typeof Users.ACTION_TYPE;
payload: UsersStateType;
}
material-uiのSelectにeventの実体がselecetのChangeEventでは無いらしいのでevent.target.value as string
にはas
を使用
コメントはこの投稿終わったら書く
import React from "react";
import {InputLabel, MenuItem, FormControl, Select, makeStyles} from "@material-ui/core";
type SelectBoxPropsType = {
label: string;
required?: boolean;
value: string;
select: (value: string) => void;
options: {
id: string
name: string
}[]
}
const useStyles = makeStyles({
formControl: {
marginBottom: 16,
minWidth: 128,
width: '100%'
}
})
export const SelectBox: React.VFC<SelectBoxPropsType> = (
{
label,
required = true,
value,
select,
options
}) => {
const classes = useStyles();
return (
<FormControl className={classes.formControl}>
<InputLabel>{label}</InputLabel>
<Select required={required} value={value} onChange={(event) => select(event.target.value as string)}>
{options.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
)
}
export {TextInput} from '@src/components/UIkit/TextInput';
export {PrimaryButton} from "@src/components/UIkit/PrimaryButton";
export {SelectBox} from "@src/components/UIkit/SelectBox";
import React from "react";
import {Route, Switch} from "react-router";
import {Home, SignIn, SignUp, Reset,ProductEdit} from "@src/pages";
import {Auth} from "@src/Auth";
export const Router: React.VFC = () => {
return (
<Switch>
<Route exact path="/signin" component={SignIn}/>
<Route exact path="/signin/reset" component={Reset}/>
<Route exact path="/signup" component={SignUp}/>
<Auth>
<Route exact path="(/)?" component={Home}/>
<Route exact path="(/product/edit)" component={ProductEdit}/>
</Auth>
</Switch>
)
}
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read: if request.auth.uid != null;
allow create;
allow update: if request.auth.uid == userId;
allow delete: if request.auth.uid == userId;
}
match /products/{productId} {
allow read: if request.auth.uid != null;
allow write: if request.auth.uid != null;
}
}
}
カテゴリーとジェンダーは外部切り出ししても良いかも
import React, {ChangeEvent, useCallback, useState} from "react";
import {PrimaryButton, SelectBox, TextInput} from "@src/components/UIkit";
import {useDispatch} from "react-redux";
import {saveProducts} from "@src/reducks/products/operations";
const categories = [
{
id: "tops",
name: "トップス"
},
{
id: "shirt",
name: "シャツ"
},
{
id: "pants",
name: "パンツ"
}
];
const genders = [
{
id: "all",
name: "すべて"
},
{
id: "male",
name: "男性"
},
{
id: "female",
name: "女性"
}
];
export const ProductEdit: React.VFC = () => {
const dispatch = useDispatch();
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [category, setCategory] = useState('');
const [gender, setGender] = useState('');
const [price, setPrice] = useState('');
const inputName = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
}, [setName]);
const inputDescription = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setDescription(event.target.value);
}, [setDescription]);
const inputPrice = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setPrice(event.target.value);
}, [setPrice]);
return (
<section>
<h2 className="u-text__headline u-text-center">商品の登録・編集</h2>
<div className="c-section-container">
<TextInput label="商品名" value={name} onChange={inputName}/>
<TextInput label="商品説明" value={description} onChange={inputDescription} multiline={true} rows={5}/>
<SelectBox label="カテゴリー" select={setCategory} value={category} options={categories} />
<SelectBox label="性別" select={setGender} value={gender} options={genders} />
<TextInput label="価格" value={price} onChange={inputPrice} type="number"/>
<div className="module-spacer--medium"/>
<div className="center">
<PrimaryButton label="商品情報を保存" onClick={() => dispatch(saveProducts(name,description,category,gender,price))} />
</div>
</div>
</section>
)
}
export {Home} from "@src/pages/Home";
export {SignUp} from "@src/pages/SignUp";
export {SignIn} from "@src/pages/SignIn";
export {Reset} from "@src/pages/Reset";
export {ProductEdit} from "@src/pages/ProductEdit";
コメント無いやつは今からつける
カテゴリーと性別はstringではないので切り出して型を変更する
categoriesとgendersを切り出してas constするとSelectBoxPropsTypeのoptionsの型をTにしないといけないけど、idとnameを確実に持っているって型の書き方がわからないので甘えたstringで続けます😂
実践編#5
import React, {useCallback} from "react";
import {IconButton} from "@material-ui/core";
import {AddPhotoAlternate} from "@material-ui/icons";
import {makeStyles} from '@material-ui/styles';
import {storage} from "@src/firebase";
import {imageType} from "@src/components/products/types";
import {ImagePreview} from "@src/components/products/ImagePreview";
type ImageAreaPropsType = {
images: imageType[];
setImages: React.Dispatch<React.SetStateAction<imageType[]>>
}
const useStyles = makeStyles({
icon: {
height: 48,
width: 48
}
})
function randomString(length: number = 16): string {
const strings = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return Array.from(crypto.getRandomValues(new Uint8Array(length))).map((n) => {
return strings[n % strings.length];
}).join('');
}
export const ImageArea: React.VFC<ImageAreaPropsType> = (
{
images,
setImages
}) => {
const classes = useStyles();
const uploadImages = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files as unknown as BlobPart[];
const blob = new Blob(file, {type: 'image/png'});
const fileName = randomString();
const uploadRef = storage.ref('images').child(fileName);
const uploadTask = uploadRef.put(blob);
uploadTask.then(() => {
uploadTask.snapshot.ref.getDownloadURL()
.then((downloadURL) => {
if (!(typeof downloadURL === 'string')) return;
const newImage = {
id: fileName,
path: downloadURL
};
setImages((prevState) => ([...prevState, newImage]));
})
})
}, [setImages])
const deleteImages = useCallback(async (id: imageType['id']) => {
const ret = window.confirm('この画像を削除しますか?');
if (!ret) return false;
const newImages = images.filter((image) => image.id !== id);
setImages(newImages);
return storage.ref('images').child(id).delete();
}, [images]);
return (
<div>
<div className="p-grid__list-images">
{images.length > 0 && (
images.map((image) => (
<ImagePreview onDelete={deleteImages} image={image} key={image.id}/>
))
)}
</div>
<div className="u-text-right">
<span>商品を登録する</span>
<IconButton className={classes.icon}>
<label>
<AddPhotoAlternate/>
<input className="u-display-none" type="file" id="image" onChange={(event) => uploadImages(event)}/>
</label>
</IconButton>
</div>
</div>
)
}
import React from "react";
import {imageType} from "@src/components/products/types";
type ImagePreviewPropsType = {
onDelete: (id: imageType["id"]) => Promise<unknown>;
image: imageType
}
export const ImagePreview:React.VFC<ImagePreviewPropsType> = (
{
onDelete,
image
}) => {
return (
<button type="button" className="p-media__thumb" onClick={() => onDelete(image.id)}>
<img src={image.path} alt=""/>
</button>
)
}
export type imageType = {
id: string;
path: string;
};
import React, {ChangeEvent, useCallback, useState} from "react";
import {PrimaryButton, SelectBox, TextInput} from "@src/components/UIkit";
import {useDispatch} from "react-redux";
import {saveProducts} from "@src/reducks/products/operations";
import {categories,genders} from "@src/reducks/products/type";
import {ImageArea} from "@src/components/products/ImageArea";
import {imageType} from "@src/components/products/types";
//商品を追加・編集するページ
export const ProductEdit: React.VFC = () => {
const dispatch = useDispatch();
//画像を管理するstate
const [images, setImages] = useState<imageType[]>([]);
//商品名を管理するstate
const [name, setName] = useState('');
//商品の説明を管理するstate
const [description, setDescription] = useState('');
//商品のカテゴリーを管理するstate
const [category, setCategory] = useState('');
//商品の性別を管理するstate
const [gender, setGender] = useState('');
//商品の価格を管理するstate
const [price, setPrice] = useState('');
//nameのinputがchangeしたら走るイベント
const inputName = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
}, [setName]);
//descriptionのinputがchangeしたら走るイベント
const inputDescription = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setDescription(event.target.value);
}, [setDescription]);
//priceのinputがchangeしたら走るイベント
const inputPrice = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setPrice(event.target.value);
}, [setPrice]);
return (
<section>
<h2 className="u-text__headline u-text-center">商品の登録・編集</h2>
<div className="c-section-container">
<ImageArea images={images} setImages={setImages} />
<TextInput label="商品名" value={name} onChange={inputName}/>
<TextInput label="商品説明" value={description} onChange={inputDescription} multiline={true} rows={5}/>
<SelectBox label="カテゴリー" select={setCategory} value={category} options={categories} />
<SelectBox label="性別" select={setGender} value={gender} options={genders} />
<TextInput label="価格" value={price} onChange={inputPrice} type="number"/>
<div className="module-spacer--medium"/>
<div className="center">
<PrimaryButton label="商品情報を保存" onClick={() => dispatch(saveProducts(images,name,description,category,gender,price))} />
</div>
</div>
</section>
)
}
import React from "react";
import {db, FirebaseTimestamp, FirebaseTimestampType} from "@src/firebase";
import {push} from "connected-react-router";
import {imageType} from "@src/components/products/types";
//firebaseのdbのproductsを一旦変数に代入
const productsRef = db.collection('products');
//TODO idとcreate_atは必須なのでpartialを無くす
//productのdataの型
type productDataType = {
images: imageType[];
id?: string;
name: string;
description: string;
category: string;
gender: string;
price: number;
update_at: FirebaseTimestampType;
created_at?: FirebaseTimestampType;
}
//商品を追加、編集する時に実行される関数
export const saveProducts = (images: imageType[], name: string, description: string, category: string, gender: string, price: string) => {
return async (dispatch: React.Dispatch<unknown>) => {
//現在の時刻
const timestamp = FirebaseTimestamp.now();
//productのdata(更新用)
const data: productDataType = {
images,
name,
description,
category,
gender,
price: parseInt(price, 10),
update_at: timestamp,
}
//firebaseが自動生成するidを取得する
const {id} = productsRef.doc();
//productのidにfirebaseが自動生成したidをセットする
data.id = id;
//productのcreate_atに作った時刻をセットする
data.created_at = timestamp;
//firebaseのproductのidに上記のdataをセットする
return productsRef.doc(id).set(data)
.then(() => {
dispatch(push('/'))
}).catch((e) => {
throw new Error(e);
})
}
}
const file = event.target.files as unknown as BlobPart[];
const blob = new Blob(file, {type: 'image/png'});
この部分無理矢理キャストしたんだけど、正攻法がわからない😥
new Blob
の第一引数がBlobPart[]
なんだけど、event.target.files
の返り値がFileList
でそのまま代入しようとすると
Argument of type 'FileList' is not assignable to parameter of type 'BlobPart[]'. Type 'FileList' is missing the following properties from type 'BlobPart[]': pop, push, concat, join, and 26 more
って怒られますね😂
new Blobは不要とあるのでなしにしてみてもだめっぽい😂
まぁまた今度リファクタリングする時にまた調べよう
const file = event.target.files;
if(!file) return;
const blob = new Blob(file as unknown as BlobPart[], {type: 'image/jpeg'});
こっちの方が安全かな?
一応null対策もしとこう
コメント追記してまた夜か明日続きやろう
実践編#6
import React, {ChangeEvent, useCallback, useEffect, useState} from "react";
import {PrimaryButton, SelectBox, TextInput} from "@src/components/UIkit";
import {useDispatch} from "react-redux";
import {productDataType, saveProducts} from "@src/reducks/products/operations";
import {categories, genders} from "@src/reducks/products/type";
import {ImageArea} from "@src/components/products/ImageArea";
import {imageType, sizeType} from "@src/components/products/types";
import {db} from "@src/firebase";
import {SetSizeArea} from "@src/components/products";
//商品を追加・編集するページ
export const ProductEdit: React.VFC = () => {
const dispatch = useDispatch();
/**
* URLから商品情報のIDを取得する関数
* /product/edit/商品ID
* から/product/edit/を削除する
* /product/edit/ページの場合は ''
* その他は商品IDが入る
*/
const id = window.location.pathname.replace(/\/product\/edit(\/)?/, '');
//画像を管理するstate
const [images, setImages] = useState<imageType[]>([]);
//商品名を管理するstate
const [name, setName] = useState('');
//商品の説明を管理するstate
const [description, setDescription] = useState('');
//商品のカテゴリーを管理するstate
const [category, setCategory] = useState('');
//商品の性別を管理するstate
const [gender, setGender] = useState('');
//商品の価格を管理するstate
const [price, setPrice] = useState('');
const [sizes, setSizes] = useState<sizeType[]>([]);
//nameのinputがchangeしたら走るイベント
const inputName = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
}, [setName]);
//descriptionのinputがchangeしたら走るイベント
const inputDescription = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setDescription(event.target.value);
}, [setDescription]);
//priceのinputがchangeしたら走るイベント
const inputPrice = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setPrice(event.target.value);
}, [setPrice]);
useEffect(() => {
//idが空の場合は早期リターン
if (!id) return;
//現在のfirebaseからidのproductsを取得する
db.collection('products').doc(id).get()
.then((snapshot) => {
//TODO 型キャストをやめる
const data = snapshot.data() as productDataType;
//data.nameをnameに代入する
setName(data.name);
//data.descriptionをdescriptionに代入する
setDescription(data.description);
//data.categoryをcategoryに代入する
setCategory(data.category);
//data.genderをgenderに代入する
setGender(data.gender);
//data.priceをpriceに代入する
setPrice(String(data.price));
//data.priceをpriceに代入する
setPrice(String(data.price));
//data.sizesをsizesに代入する
setSizes(data.sizes);
})
}, [id]);
return (
<section>
<h2 className="u-text__headline u-text-center">商品の登録・編集</h2>
<div className="c-section-container">
<ImageArea images={images} setImages={setImages}/>
<TextInput label="商品名" value={name} onChange={inputName}/>
<TextInput label="商品説明" value={description} onChange={inputDescription} multiline={true} rows={5}/>
<SelectBox label="カテゴリー" select={setCategory} value={category} options={categories}/>
<SelectBox label="性別" select={setGender} value={gender} options={genders}/>
<TextInput label="価格" value={price} onChange={inputPrice} type="number"/>
<div className="module-spacer--small"/>
<SetSizeArea sizes={sizes} setSizes={setSizes}/>
<div className="module-spacer--medium"/>
<div className="center">
<PrimaryButton
label="商品情報を保存"
onClick={() => dispatch(saveProducts(id, images, name, description, category, gender, price, sizes))}/>
</div>
</div>
</section>
)
}
import React, {useCallback, useMemo, useState} from "react";
import {
TableContainer,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
IconButton,
makeStyles, Paper
} from "@material-ui/core";
import {CheckCircle, Delete, Edit} from "@material-ui/icons";
import {TextInput} from "@src/components/UIkit";
import {sizeType} from "@src/components/products/types";
const useStyles = makeStyles({
checkIcon: {
float: 'right'
},
iconCell: {
height: 48,
width: 48
}
})
type changeSize = (index: number, {size, quantity}: sizeType) => void;
type SetSizeAreaPropsType = {
sizes: sizeType[];
setSizes: React.Dispatch<React.SetStateAction<sizeType[]>>
}
export const SetSizeArea: React.VFC<SetSizeAreaPropsType> = (
{
sizes,
setSizes
}) => {
const classes = useStyles();
//商品のサイズと数量のセット数を管理するstate
const [index, setIndex] = useState(0);
//サイズを管理するstate
const [size, setSize] = useState("");
//数量を管理するstate
const [quantity, setQuantity] = useState(0);
//sizeのinputがchangeしたら走るイベント
const inputSize = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setSize(event.target.value);
}, [setSize]);
//quantityのinputがchangeしたら走るイベント
const inputQuantity = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
if (!event.target.value) return;
setQuantity(parseInt(event.target.value));
}, [setQuantity]);
//商品のサイズと数量のセットを増やす時の関数
const addSize: changeSize = (index, {size, quantity}) => {
//sizeかquantityどちらか一つでも空の場合早期リターン
if (!size || !quantity) return;
//最新(新規)なら追加する
if (index === sizes.length) {
//前の値とマージする
setSizes((prevState) => [...prevState, {size, quantity}]);
//indexを+1する
setIndex((prevState => prevState + 1));
//既存の場合
} else {
//sizeを一旦変数に代入
const newSizes = sizes;
//sizeのindex番(選択した既存の番号)のsizeとquantityを上書きする
newSizes[index] = {size, quantity};
//setSizesでstateに保存する
setSizes(newSizes);
//indexをnewSizes.length(最新の長さ)にする
setIndex(newSizes.length);
}
//共通の設定
//値を初期化する
setSize("");
setQuantity(0);
}
//現在選択されている商品のサイズと数量のセットを編集する関数
const editSize: changeSize = (index, {size, quantity}) => {
setIndex(index);
setSize(size);
setQuantity(quantity);
}
//現在選択されている商品のサイズと数量のセットを削除する関数
const deleteSize = (index: number) => {
const newSizes = sizes.filter((_, i) => i !== index);
setSizes(newSizes);
}
//indexをmemo化
useMemo(() => {
setIndex(sizes.length);
}, [sizes.length])
return (
<div>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>サイズ</TableCell>
<TableCell>数量</TableCell>
<TableCell className={classes.iconCell}/>
<TableCell className={classes.iconCell}/>
</TableRow>
</TableHead>
<TableBody>
{sizes.length > 0 && (
sizes.map((item, i) => (
<TableRow key={item.size}>
<TableCell>{item.size}</TableCell>
<TableCell>{item.quantity}</TableCell>
<TableCell>
<IconButton
className={classes.iconCell}
onClick={() => editSize(i, {size: item.size, quantity: item.quantity})}>
<Edit/>
</IconButton>
</TableCell>
<TableCell>
<IconButton className={classes.iconCell} onClick={() => deleteSize(i)}>
<Delete/>
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
<div>
<TextInput fullWidth={false} label="サイズ" value={size} onChange={inputSize}/>
<TextInput fullWidth={false} label="数量" value={quantity} onChange={inputQuantity} type="number"/>
<IconButton className={classes.checkIcon} onClick={() => addSize(index, {size, quantity})}>
<CheckCircle/>
</IconButton>
</div>
</TableContainer>
</div>
)
}
//productのfirebaseに登録する画像の型
export type imageType = {
id: string;
path: string;
};
//productのサイズの型
export type sizeType = {
size: string;
quantity: number
}
export {ImageArea} from "@src/components/products/ImageArea";
export {ImagePreview} from "@src/components/products/ImagePreview";
export {SetSizeArea} from "@src/components/products/SetSizeArea";
import React from "react";
import {Route, Switch} from "react-router";
import {Home, SignIn, SignUp, Reset,ProductEdit} from "@src/pages";
import {Auth} from "@src/Auth";
export const Router: React.VFC = () => {
return (
<Switch>
<Route exact path="/signin" component={SignIn}/>
<Route exact path="/signin/reset" component={Reset}/>
<Route exact path="/signup" component={SignUp}/>
<Auth>
<Route exact path="(/)?" component={Home}/>
<Route path="/product/edit(/:id)?" component={ProductEdit}/>
</Auth>
</Switch>
)
}
import React from "react";
import {db, FirebaseTimestamp, FirebaseTimestampType} from "@src/firebase";
import {push} from "connected-react-router";
import {imageType, sizeType} from "@src/components/products/types";
//firebaseのdbのproductsを一旦変数に代入
const productsRef = db.collection('products');
//TODO idとcreate_atは必須なのでpartialを無くす
//productのdataの型
//どこかに移動させる
export type productDataType = {
images: imageType[];
id?: string;
name: string;
description: string;
category: string;
gender: string;
price: number;
sizes: sizeType[];
update_at: FirebaseTimestampType;
created_at?: FirebaseTimestampType;
}
//商品を追加、編集する時に実行される関数
export const saveProducts = (
id: string,
images: imageType[],
name: string,
description: string,
category: string,
gender: string,
price: string,
sizes: sizeType[]
) => {
return async (dispatch: React.Dispatch<unknown>) => {
//現在の時刻
const timestamp = FirebaseTimestamp.now();
//productのdata(更新用)
const data: productDataType = {
images,
name,
description,
category,
gender,
price: parseInt(price, 10),
sizes,
update_at: timestamp,
}
//idが空の場合(Productを新規作成の場合)
if (id === '') {
//firebaseが自動生成するidを取得する
const {id: currentId} = productsRef.doc();
//productのidにfirebaseが自動生成したidをセットする
data.id = currentId;
//productのcreate_atに作った時刻をセットする
data.created_at = timestamp;
}
//idが空の場合は上記の新規作成したIDを代入する
const productId = id || productsRef.doc().id;
//firebaseのproductのidに上記のdataをセットする
return productsRef.doc(productId).set(data, {merge: true})
.then(() => {
dispatch(push('/'))
}).catch((e) => {
throw new Error(e);
})
}
}
変えた部分
正規表現に変更
/**
* URLから商品情報のIDを取得する関数
* /product/edit/商品ID
* から/product/edit/を削除する
* /product/edit/ページの場合は ''
* その他は商品IDが入る
*/
const id = window.location.pathname.replace(/\/product\/edit(\/)?/, '');
良く考えたらconst [price, setPrice] = useState('');
の部分number
型の方が良いな?
というかエラーハンドリングしてない!
TODO
- form関連のバリデーションをかける(react-hook-form & Yupが候補)