こんばんは!スージーです。
前回書いた記事はこちらです
やりたい事
-
devise_auth_token
を使った認証 -
Material-UI
を使ったスタイル修正
開発環境
Ruby 2.7.1 => 2.7.2
Rails 6.0.4 => 6.1.4
MySQL
node.js 14.8.0 => 16.6.2
React 17.0.2
※開発環境を変更したため、各バージョンが変更になりました。part1から通して作り直してみましたが、新旧バージョンで動作に影響はありません
参考
- Rails API + React + devise_token_authでログイン機能を実装する
- [Rails API + React でマッチングアプリを作ってみた](Rails API + React でマッチングアプリを作ってみた)
認証周りは上記のQiita記事通りに実装させてもらいました。大変分かりやすく、簡単に実装することができました。ありがとうございました。
やらないこと
-
Rails
- メール認証
- SNS認証
- パスワード再発行
-
React
- Material-uiの詳しい説明
Rails側から実装開始
backendはこちらの記事に書かれている通りに実装しています
gem 'devise'
gem 'devise_token_auth'
backkend $ bundle install
rails g devise:install
rails g devise_token_auth:install User auth
rails db:migrate
# app/config/initializers/devise_token_auth.rb
# 以下のブロッグをコメント解除
DeviseTokenAuth.setup do |config|
config.change_headers_on_each_request = false
config.token_lifespan = 2.weeks
config.token_cost = Rails.env.test? ? 4 : 10
config.headers_names = {:'access-token' => 'access-token',
:'client' => 'client',
:'expiry' => 'expiry',
:'uid' => 'uid',
:'token-type' => 'token-type' }
end
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "localhost:5000"
resource "*",
headers: :any,
expose: ["access-token", "expiry", "token-type", "uid", "client"], # 追記
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
コントローラ作成
rails g controller api/v1/auth/registrations
rails g controller api/v1/auth/sessions
# registrations_controller.rb
class Api::V1::Auth::RegistrationsController < DeviseTokenAuth::RegistrationsController
private
def sign_up_params
params.permit(:email, :password, :password_confirmation, :name)
end
end
# sessions_controller.rb
class Api::V1::Auth::SessionsController < ApplicationController
def index
if current_api_v1_user
render json: { is_login: true, data: current_api_v1_user }
else
render json: { is_login: false, message: "ユーザーが存在しません" }
end
end
end
※2021/08/15 ソースのコピペミスで誤ったjsonを返す記述をしていたので修正しました
2021/08/15 追記
ログイン状態で画面を何度かリロードすると、ログイン画面にリダイレクトされる不安定な挙動がありました。症状はcookieにuid
, client
, access_token
が有効なものがあるにも関わらず、ログインが解除されているという事象です。DBの情報を見てもtokens
カラムにはtokenが入っています。
こちらの記事を参考に以下の修正を行ったところ挙動が安定するようになりました。
# devise_auth_token.rb
# 8行目をコメントアウトとtrue => falseに修正
change_headers_on_each_request = false
change_headers_on_each_request
ですが、
デフォルトではそれぞれのリクエストの後にaccess-tokenヘッダが変わります。クライアントは変更されるトークンを追跡し続ける責務があります。ng-token-authとj-tokerの両方でこれが実行されます。これは安全ですが、管理が難しいです。この設定をfalseにすると、リクエスト毎にトークンヘッダが変更されなくなります。
トークンヘッダが変わるため、リロードを何度も繰り返すと追跡がうまくいかない可能性があります。まだちゃんと調査したわけでは無いので、詳しい方いましたら、ご教示お願いいたします。
# applicatio_controller.rb
class ApplicationController < ActionController::Base
include DeviseTokenAuth::Concerns::SetUserByToken
skip_before_action :verify_authenticity_token
helper_method :current_user, :user_signed_in?
end
ルーティング
# routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :posts
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
registrations: 'api/v1/auth/registrations'
}
namespace :auth do
resources :sessions, only: %i[index]
end
end
end
end
動作確認
// ユーザー作成
curl -X POST http://localhost:5000/api/v1/auth -d "[name]=test&[email]=test@example.com&[password]=password&[password_confirmation]=password"
・
・・
・・・
"status": "success",
// サインイン
curl -X POST -v http://localhost:5000/api/v1/auth/sign_in -d "[email]=test@example.com&[password]=password"
・
・・
・・・
< HTTP/1.1 200 OK
成功レスポンスが返ってくればOK
次にReact側を実装
認証周りと一緒にMaterial-UI
を使ってレイアウトも修正していきます
まずAPIを用意します。ついでにMaterial-UI
のライブラリもインストールします
frontend $ npm install js-cookie
npm install @material-ui/core @material-ui/icons @material-ui/lab
touch src/lib/api/auth.js
こちらの記事同様に実装するのでjs-cookie
を追加します
// src/lib/api/auth.js
import client from './client';
import Cookies from 'js-cookie';
export const signUp = (params) => {
return client.post('/auth', params);
};
export const signIn = (params) => {
return client.post('/auth/sign_in', params);
};
export const signOut = () => {
return client.delete('/auth/sign_out', {
headers: {
'access-token': Cookies.get('_access_token'),
client: Cookies.get('_client'),
uid: Cookies.get('_uid'),
},
});
};
export const getCurrentUser = () => {
if (
!Cookies.get('_access_token') ||
!Cookies.get('_client') ||
!Cookies.get('_uid')
)
return;
return client.get('/auth/sessions', {
headers: {
'access-token': Cookies.get('_access_token'),
client: Cookies.get('_client'),
uid: Cookies.get('_uid'),
},
});
};
// App.jsx
import React, { useState, useEffect, createContext } from 'react';
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
} from 'react-router-dom';
// component
import List from './components/List';
import New from './components/New';
import Detail from './components/Detail';
import Edit from './components/Edit';
import Header from './components/commons/Header';
import SignUp from './components/users/SignUp';
import SignIn from './components/users/SignIn';
import MainContainer from './components/layout/MainContainer';
// style
import { CssBaseline } from '@material-ui/core';
import { StylesProvider, ThemeProvider } from '@material-ui/styles';
import { theme } from './styles/theme';
import { getCurrentUser } from './lib/api/auth';
export const AuthContext = createContext();
const App = () => {
const [loading, setLoading] = useState(true);
const [isSignedIn, setIsSignedIn] = useState(false);
const [currentUser, setCurrentUser] = useState();
const handleGetCurrentUser = async () => {
try {
const res = await getCurrentUser();
if (res?.data.isLogin === true) {
setIsSignedIn(true);
setCurrentUser(res?.data.data);
console.log(res?.data.data);
} else {
console.log('no current user');
}
} catch (e) {
console.log(e);
}
setLoading(false);
};
useEffect(() => {
handleGetCurrentUser();
}, [setCurrentUser]);
const Private = ({ children }) => {
if (!loading) {
if (isSignedIn) {
return children;
} else {
return <Redirect to='/signin' />;
}
} else {
return <></>;
}
};
return (
<>
<StylesProvider injectFirst>
<ThemeProvider theme={theme}>
<AuthContext.Provider
value={{
loading,
setLoading,
isSignedIn,
setIsSignedIn,
currentUser,
setCurrentUser,
}}
>
<CssBaseline />
<Router>
<Header />
<MainContainer>
<Switch>
<Route exact path='/signup' component={SignUp} />
<Route exact path='/signin' component={SignIn} />
<Private>
<Route exact path='/' component={List} />
<Route path='/post/:id' component={Detail} />
<Route exact path='/new' component={New} />
<Route path='/edit/:id' component={Edit} />
</Private>
</Switch>
</MainContainer>
</Router>
</AuthContext.Provider>
</ThemeProvider>
</StylesProvider>
</>
);
};
export default App;
Contextを使って認証情報をグローバルに管理しています。<Private></Private>
でラップするコンポーネントには認証中でないとアクセスできないようになっています
Material-UI
で共通で使う色など変更したり、breakpointを変えたり、フォントを変えたり等々する時の為にtheme.js
を作成します
// src/styles/theme.js
import { createTheme } from '@material-ui/core/styles';
export const theme = createTheme({
overrides: {
MuiCssBaseline: {
'@global': {
html: {
WebkitFontSmoothing: 'auto',
},
},
},
},
breakpoints: {
values: {
xs: 0,
sm: 768,
md: 960,
lg: 1280,
xl: 1920,
},
},
props: {
MuiButtonBase: {
disableRipple: false,
},
MuiFilledInput: {
color: 'secondary',
},
},
palette: {
primary: {
main: '#1976d2',
contrastText: '#ffffff',
light: '#4791db',
dark: '#115293',
},
secondary: {
main: '#dc004e',
light: '#e33371',
dark: '#9a0036',
},
error: {
main: '#FF2E2E',
},
background: {
default: '#ffffff',
},
},
typography: {
fontFamily: [
'Noto Sans JP',
'Hiragino Kaku Gothic Pro',
'ヒラギノ角ゴ Pro',
'Yu Gothic Medium',
'游ゴシック Medium',
'YuGothic',
'游ゴシック体',
'メイリオ',
'sans-serif',
].join(','),
},
});
画面真ん中に要素を配置できるように全てのコンポーネント共通で使うレイアウトを作成します
// src/components/layout/MainContainer.jsx
import React from 'react';
// style
import { Container, Grid } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(() => ({
container: {
marginTop: '3rem',
},
}));
const MainContainer = ({ children }) => {
const classes = useStyles();
return (
<>
<main>
<Container maxWidth='lg' className={classes.container}>
<Grid container justifyContent='center'>
<Grid item>{children}</Grid>
</Grid>
</Container>
</main>
</>
);
};
export default MainContainer;
上のコンポーネントをApp.jsx
にimportして<Switch></Switch>
をラップして全てのコンポーネントに適用します
サインアップ、サインイン用のコンポーネントを作成します。フォーム部分は共通で使いまわします
// src/components/users/SignIn.jsx
import React, { useState, useContext } from 'react';
import { useHistory } from 'react-router-dom';
import Cookies from 'js-cookie';
// context
import { AuthContext } from '../../App';
// api
import { signIn } from '../../lib/api/auth';
// component
import SignForm from './SignForm';
const SignIn = () => {
const history = useHistory();
const { setIsSignedIn, setCurrentUser } = useContext(AuthContext);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const signInHandleSubmit = async (e) => {
e.preventDefault();
const params = generateParams();
try {
const res = await signIn(params);
if (res.status === 200) {
Cookies.set('_access_token', res.headers['access-token']);
Cookies.set('_client', res.headers['client']);
Cookies.set('_uid', res.headers['uid']);
setIsSignedIn(true);
setCurrentUser(res.data.data);
history.push('/');
}
} catch (e) {
console.log(e);
}
};
const generateParams = () => {
const signInParams = {
email: email,
password: password,
};
return signInParams;
};
return (
<SignForm
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
handleSubmit={signInHandleSubmit}
signType='signIn'
/>
);
};
export default SignIn;
// src/components/users/SignUp.jsx
import React, { useState, useContext } from 'react';
import { useHistory } from 'react-router-dom';
import Cookies from 'js-cookie';
// context
import { AuthContext } from '../../App';
// api
import { signUp } from '../../lib/api/auth';
// component
import SignForm from './SignForm';
const SignUp = () => {
const history = useHistory();
const { setIsSignedIn, setCurrentUser } = useContext(AuthContext);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirmation, setPasswordConfirmation] = useState('');
const signUpHandleSubmit = async (e) => {
e.preventDefault();
const params = generateParams();
try {
const res = await signUp(params);
console.log(res);
if (res.status === 200) {
Cookies.set('_access_token', res.headers['access-token']);
Cookies.set('_client', res.headers['client']);
Cookies.set('_uid', res.headers['uid']);
setIsSignedIn(true);
setCurrentUser(res.data.data);
history.push('/');
console.log('signed in successfully');
}
} catch (e) {
console.log(e);
}
};
const generateParams = () => {
const signUpParams = {
name: name,
email: email,
password: password,
passwordConfirmation: passwordConfirmation,
};
return signUpParams;
};
return (
<SignForm
name={name}
setName={setName}
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
passwordConfirmation={passwordConfirmation}
setPasswordConfirmation={setPasswordConfirmation}
handleSubmit={signUpHandleSubmit}
signType='signUp'
/>
);
};
export default SignUp;
// src/components/users/SignForm.jsx
import React from 'react';
import { Link } from 'react-router-dom';
// style
import { makeStyles } from '@material-ui/core/styles';
import {
Typography,
TextField,
Card,
CardContent,
CardHeader,
Button,
Box,
} from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
container: {
marginTop: theme.spacing(6),
},
submitBtn: {
marginTop: theme.spacing(2),
flexGrow: 1,
textTransform: 'none',
},
header: {
textAlign: 'center',
},
card: {
padding: theme.spacing(2),
maxWidth: 400,
},
box: {
marginTop: '2rem',
},
link: {
textDecoration: 'none',
},
}));
const SignForm = (props) => {
const {
email,
setEmail,
password,
setPassword,
handleSubmit,
signType,
name,
setName,
passwordConfirmation,
setPasswordConfirmation,
} = props;
const classes = useStyles();
return (
<>
<form noValidate autoComplete='off'>
<Card className={classes.card}>
<CardHeader className={classes.header} title={signType} />
<CardContent>
{signType === 'signUp' && (
<TextField
variant='outlined'
required
fullWidth
label='Name'
value={name}
margin='dense'
onChange={(event) => setName(event.target.value)}
/>
)}
<TextField
variant='outlined'
required
fullWidth
label='Email'
value={email}
margin='dense'
onChange={(event) => setEmail(event.target.value)}
/>
<TextField
variant='outlined'
required
fullWidth
label='Password'
type='password'
placeholder='At least 6 characters'
value={password}
margin='dense'
autoComplete='current-password'
onChange={(event) => setPassword(event.target.value)}
/>
{signType === 'signUp' && (
<TextField
variant='outlined'
required
fullWidth
label='Password Confirmation'
type='password'
value={passwordConfirmation}
margin='dense'
autoComplete='current-password'
onChange={(event) =>
setPasswordConfirmation(event.target.value)
}
/>
)}
<Button
type='submit'
variant='contained'
size='large'
fullWidth
color='default'
disabled={!email || !password ? true : false}
className={classes.submitBtn}
onClick={handleSubmit}
>
Submit
</Button>
{signType === 'signIn' && (
<Box textAlign='center' className={classes.box}>
<Typography variant='body2'>
Don't have an account?
<Link to='/signup' className={classes.link}>
Sign Up now!
</Link>
</Typography>
</Box>
)}
</CardContent>
</Card>
</form>
</>
);
};
export default SignForm;
propsで受け取ったsignType
がsignIn
なのかsignUp
なのかで、name
とpasswordConfirmation
のテキストフィールドの出し分け、signup
へのリンクの出し分けを行っています
各画面のスタイルを修正
前回の記事で作成した各コンポーネントにMaterial-UI
でスタイルを当てていきます
ヘッダー
ログイン・サインアップ・ログアウトの各種ボタンとハンバーガーメニューを作成します
// components/commons/Header.jsx
import React, { useContext, useState } from 'react';
import { useHistory, Link } from 'react-router-dom';
import Cookies from 'js-cookie';
// style
import { createStyles, makeStyles } from '@material-ui/core/styles';
import {
AppBar,
Toolbar,
Typography,
Button,
IconButton,
} from '@material-ui/core';
import MenuIcon from '@material-ui/icons/Menu';
// api
import { signOut } from '../../lib/api/auth';
// context
import { AuthContext } from '../../App';
// component
import HeaderDrawer from './HeaderDrawer';
const useStyles = makeStyles((theme) =>
createStyles({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
linkBtn: {
textTransform: 'none',
},
})
);
const drawerItem = [
{ label: '一覧へ戻る', path: '/' },
{ label: '新規作成', path: '/new' },
{ label: '自分の投稿', path: '#' },
];
const Header = () => {
const { loading, isSignedIn, setIsSignedIn, currentUser } =
useContext(AuthContext);
const classes = useStyles();
const history = useHistory();
const handleSignOut = async (e) => {
try {
const res = await signOut();
if (res.data.success === true) {
Cookies.remove('_access_token');
Cookies.remove('_client');
Cookies.remove('_uid');
setIsSignedIn(false);
history.push('/signin');
console.log('succeeded in sign out');
} else {
console.log('failed in sign out');
}
} catch (e) {
console.log(e);
}
};
const AuthButtons = () => {
if (!loading) {
if (isSignedIn) {
return (
<Button
color='inherit'
className={classes.linkBtn}
onClick={handleSignOut}
>
Sign out
</Button>
);
} else {
return (
<>
<Button
component={Link}
to='/signin'
color='inherit'
className={classes.linkBtn}
>
Sign in
</Button>
<Button
component={Link}
to='/signup'
color='inherit'
className={classes.linkBtn}
>
Sign Up
</Button>
</>
);
}
} else {
return <></>;
}
};
const [open, setOpen] = useState(false);
const handleDrawerToggle = () => {
setOpen(!open);
};
return (
<>
<div className={classes.root}>
<AppBar position='static'>
<Toolbar>
{isSignedIn && currentUser && (
<IconButton
edge='start'
className={classes.menuButton}
color='inherit'
aria-label='menu'
onClick={handleDrawerToggle}
>
<MenuIcon />
</IconButton>
)}
<Typography variant='h6' className={classes.title}>
React Rails API Practice
</Typography>
<AuthButtons />
</Toolbar>
</AppBar>
</div>
<HeaderDrawer
open={open}
handleDrawerToggle={handleDrawerToggle}
drawerItem={drawerItem}
/>
</>
);
};
export default Header;
未ログイン時はSign In
、Sign Up
ボタンが表示されます。ログイン中はSign Out
ボタンが表示されます。またハンバーガーメニューはログイン中しか表示されないように制御しています
ドロワーは別コンポーネントで作成します
// components/commons/HeaderDrawer.jsx
import React from 'react';
import { Link } from 'react-router-dom';
import { createStyles, makeStyles, useTheme } from '@material-ui/core/styles';
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import {
IconButton,
Drawer,
List,
Divider,
ListItem,
ListItemText,
} from '@material-ui/core';
const drawerWidth = 240;
const useStyles = makeStyles((theme) =>
createStyles({
drawer: {
width: drawerWidth,
flexShrink: 0,
},
drawerPaper: {
width: drawerWidth,
},
drawerHeader: {
display: 'flex',
alignItems: 'center',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
justifyContent: 'flex-end',
},
})
);
const HeaderDrawer = (props) => {
const { open, handleDrawerToggle, drawerItem } = props;
const classes = useStyles();
const theme = useTheme();
return (
<Drawer
className={classes.drawer}
variant='temporary'
anchor='left'
open={open}
classes={{ paper: classes.drawerPaper }}
>
<div className={classes.drawerHeader}>
<IconButton onClick={handleDrawerToggle}>
{theme.direction === 'ltr' ? (
<ChevronLeftIcon />
) : (
<ChevronRightIcon />
)}
</IconButton>
</div>
<Divider />
<List>
{drawerItem.map((item, index) => (
<ListItem
component={Link}
to={item.path}
key={index}
onClick={handleDrawerToggle}
>
<ListItemText primary={item.label} />
</ListItem>
))}
</List>
</Drawer>
);
};
export default HeaderDrawer;
HOME画面
// List.jsx
import React, { useEffect, useState } from 'react';
import { getList, deletePost } from '../lib/api/post';
import { useHistory, Link } from 'react-router-dom';
import SpaceRow from './commons/SpaceRow';
// style
import {
Button,
TableContainer,
Table,
TableBody,
TableCell,
TableRow,
TableHead,
Paper,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
// functions
import { subString } from '../functions/functions';
const useStyles = makeStyles({
table: {
minWidth: 650,
},
fontWeight: {
fontWeight: 900,
},
});
const List = () => {
const classes = useStyles();
const [dataList, setDataList] = useState([]);
useEffect(() => {
handleGetList();
}, []);
const handleGetList = async () => {
try {
const res = await getList();
console.log(res.data);
setDataList(res.data);
} catch (e) {
console.log(e);
}
};
const history = useHistory();
const handleDelete = async (item) => {
console.log('click', item.id);
try {
const res = await deletePost(item.id);
console.log(res.data);
handleGetList();
} catch (e) {
console.log(e);
}
};
return (
<>
<h1>HOME</h1>
<Button
variant='contained'
color='primary'
onClick={() => history.push('/new')}
>
新規作成
</Button>
<SpaceRow height={20} />
<TableContainer component={Paper}>
<Table className={classes.table} aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell align='center' className={classes.fontWeight}>
名前
</TableCell>
<TableCell align='center' className={classes.fontWeight}>
猫種
</TableCell>
<TableCell align='center' className={classes.fontWeight}>
好きな食べ物
</TableCell>
<TableCell align='center' className={classes.fontWeight}>
好きなおもちゃ
</TableCell>
<TableCell align='center'></TableCell>
<TableCell align='center'></TableCell>
<TableCell align='center'></TableCell>
</TableRow>
</TableHead>
<TableBody>
{dataList.map((item, index) => (
<TableRow key={index}>
<TableCell align='center'>{subString(item.name, 15)}</TableCell>
<TableCell align='center'>
{subString(item.nekoType, 15)}
</TableCell>
<TableCell align='center'>
{subString(item.detailInfo.favoriteFood, 10)}
</TableCell>
<TableCell align='center'>
{subString(item.detailInfo.favoriteToy, 10)}
</TableCell>
<TableCell align='center'>
<Link to={`/edit/${item.id}`}>更新</Link>
</TableCell>
<TableCell align='center'>
<Link to={`/post/${item.id}`}>詳細へ</Link>
</TableCell>
<TableCell align='center'>
<Button
variant='contained'
color='secondary'
onClick={() => handleDelete(item)}
>
削除
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
);
};
export default List;
長い文字列の時にテーブルレイアウトが崩れてしまうので、一定文字数以上は「...」に置き換える関数subString
を用意します
// functions/functions.js
export function subString(string, num) {
const name = string;
if (name.length > num) {
const splitName = name.substring(0, num);
return splitName + '...';
} else {
return name;
}
}
引数にstring
とnum
を渡してあげます
// こんな感じで使います
{subString(item.nekoType, 15)}
要素にmarginを入れる為にstyle
タグやclassName
をつけるのが手間なのでmarginコンポーネント作ります
// components/commons/SpaceRow.jsx
import React from 'react';
const SpaceRow = (props) => {
return <div style={{ marginTop: props.height, width: '100%' }} />;
};
export default SpaceRow;
// こんな感じで使います
<SpaceRow height={20} />
詳細画面
// Detail.jsx
import React, { useEffect, useState } from 'react';
import { getDetail } from '../lib/api/post';
import { useHistory, useParams } from 'react-router-dom';
import SpaceRow from './commons/SpaceRow';
// style
import {
Button,
TableContainer,
Table,
TableBody,
TableCell,
TableRow,
Paper,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles({
table: {
minWidth: 300,
},
fontWeight: {
fontWeight: 900,
},
});
const Detail = () => {
const classes = useStyles();
const [data, setData] = useState({
name: '',
neko_type: '',
detailInfo: {
favorite_food: '',
favorite_toy: '',
},
});
const query = useParams();
console.log(query.id);
const history = useHistory();
useEffect(() => {
handleGetDetail(query);
}, [query]);
const handleGetDetail = async (query) => {
try {
const res = await getDetail(query.id);
console.log(res.data);
setData(res.data);
} catch (e) {
console.log(e);
}
};
return (
<>
<h1>DETAIL</h1>
<Button
variant='contained'
color='primary'
onClick={() => history.push('/')}
>
戻る
</Button>
<SpaceRow height={20} />
<TableContainer component={Paper} className={classes.table}>
<Table className={classes.table} aria-label='simple table'>
<TableBody>
<TableRow>
<TableCell align='right' className={classes.fontWeight}>
ID:
</TableCell>
<TableCell align='center'>{data.id}</TableCell>
</TableRow>
<TableRow>
<TableCell align='right' className={classes.fontWeight}>
名前:
</TableCell>
<TableCell align='center'>{data.name}</TableCell>
</TableRow>
<TableRow>
<TableCell align='right' className={classes.fontWeight}>
猫種:
</TableCell>
<TableCell align='center'>{data.nekoType}</TableCell>
</TableRow>
<TableRow>
<TableCell align='right' className={classes.fontWeight}>
好きな食べ物:
</TableCell>
<TableCell align='center'>
{data.detailInfo.favoriteFood}
</TableCell>
</TableRow>
<TableRow>
<TableCell align='right' className={classes.fontWeight}>
好きなおもちゃ:
</TableCell>
<TableCell align='center'>
{data.detailInfo.favoriteToy}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</>
);
};
export default Detail;
新規登録画面と編集画面
// Form.jsx
import React from 'react';
import { useHistory } from 'react-router-dom';
import SpaceRow from './commons/SpaceRow';
import { makeStyles } from '@material-ui/core/styles';
import { Button, TextField } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
'& > *': {
margin: theme.spacing(1),
width: '25ch',
},
},
}));
const Form = (props) => {
const classes = useStyles();
const history = useHistory();
const { handleChange, handleSubmit, value, buttonType } = props;
return (
<>
<Button
type='submit'
variant='contained'
color='primary'
onClick={(e) => handleSubmit(e)}
style={{ marginRight: 10 }}
>
{buttonType}
</Button>
<Button variant='contained' onClick={() => history.push('/')}>
戻る
</Button>
<SpaceRow height={20} />
<form className={classes.root} noValidate autoComplete='off'>
<TextField
id='name'
label='猫の名前'
type='text'
name='name'
onChange={(e) => handleChange(e)}
value={value.name}
/>
<TextField
id='nekoType'
label='猫種'
type='text'
name='nekoType'
onChange={(e) => handleChange(e)}
value={value.nekoType}
/>
<TextField
id='favoriteFood'
label='好きな食べ物'
type='text'
name='favoriteFood'
onChange={(e) => handleChange(e)}
value={value.favoriteFood}
/>
<TextField
id='favoriteToy'
label='好きなおもちゃ'
type='text'
name='favoriteToy'
onChange={(e) => handleChange(e)}
value={value.favoriteToy}
/>
</form>
</>
);
};
export default Form;
これで全てのページのレイアウトをある程度整えることができました。
キャプチャのようなレイアウトになっていればOK
まとめ
今回は認証周りとレイアウトの修正を行いました。次はUserとPostの関連付けを行っていこうと思います