ðReact + TypeScript + Firebaseã§èªèšŒä»ãã®ç°¡åãªæ²ç€ºæ¿ãäœãã
ã¯ããã«
ã€ã¶ããããã§ããªãWebã¢ããªãäœããŸãããReactã§WebãµãŒãã¹ãäœããã人ã¯ãã®ãã¥ãŒããªã¢ã«ãèªã¿é²ãã足ããªãæ©èœãäœã£ãŠã¿ããšè¯ãã§ãããã
Demo: https://single-board-3c001.web.app/ Code: https://github.com/shuent/single-board
Webã¢ããªã®æ©èœ
- ãã°ã€ã³
- Googleã¢ã«ãŠã³ããã¡ãŒã«ã¢ãã¬ã¹ã§èªèšŒã
- ã€ã¶ããäžèŠ§
- ã³ã¡ã³ãæçš¿
- èªèšŒãããŠãŒã¶ãŒã®ã¿æçš¿ã§ãã
䜿çšããæè¡ã»ã©ã€ãã©ãªãªã©
- React
- Hooks
- useReducer
- ContextAPI
- Hooks
- TypeScript
- Create React App
- React Router
- Firebase
- Auth
- Firestore
- Hosting
- Chakra UI
Hooks
ä»ã©ããªã®ã§ãé¢æ°ã³ã³ããŒãã³ããšHooksã䜿ããŸããçè ã¯é¢æ°ã³ã³ããŒãã³ããåºãŠããReactãå匷ããã®ã§ãClassæ代ã®Reactãæžããããšããªãã
TypeScript
- ã³ã³ãã€ã«æãšã©ãŒãåºããŠããã
- IDEãã³ãŒãè£å®ãããã
ãšããã¡ãªãããããã®ã§å©çšããŠããŸããåå¿è ã«ãšã£ãŠã³ãŒããæžãéãå€ããªããšãããã¡ãªãããå·®ãåŒããŠããã¡ãªãããäœããããŸãã
ç¶æ 管ç
ç¶æ 管çã«ã¯Hooksã®useReducerãš Context APIãå©çšããŠãFluxã®ææ³ãåãå ¥ããŸããReduxã¯äœ¿ããŸããããå®è£ ã®æµããšããŠã¯äžç·ãªã®ã§äœ¿ãæ¹ã¯äžåºŠèŠãŠãããšè¯ãããã§ãã
UIã©ã€ãã©ãª: Chakra UI
Chakra UI 㯠TailwindCSS ã®ãããªãŠãŒãã£ãªãã£ããŒã¹ãªComponentãæäŸããUIã©ã€ãã©ãªã§ããReactã³ã³ããŒãã³ãã«ãªã£ãŠããã®ã§ãTailwindãã䜿ãããããMaterial UIãªã©ã®UIãã¬ãŒã ã¯ãŒã¯ããã¯èªç±åºŠãããã®ã§å¥œãã§ãã
Firebase
Firestore
ä»åã¯ããŒã¿æ§é ãç°¡åãªãããNoSQLã§ããFirestoreãå©çšããŸããNoSQLã¯DBèšèšã«æ£è§£ããªãã®ã§é£ãããè€éãªãªã¬ãŒã·ã§ã³ã匵ãã«ã¯åããŠãŸãããåé¢ãããã¯ãšã³ããèŠããæ軜ã«å©çšã§ããã®ã§ãå°èŠæš¡ã§åçŽãªããŒã¿æ§é ã®ã¢ããªã«ã¯äœ¿ããããã§ãã
Firebase Authentication
èªèšŒã«ã¯Firebase Authentication ãå©çšããŸããtokenã®ç®¡çãªã©ãè£ã§ãã£ãŠãããã®ã§ããšãŠã楜ã§ããããã«Firebase UIã䜿ãããã°ã€ã³ç»é¢ãã»ãŒã³ãŒãæžããã«æžã¿ãŸããã
Hosting
ã³ãã³ãäžã€ã§ãããã€ãurlãçºè¡ããŠãããŸããä»åã¯ããã䜿ããŸã
ããã³ããšã³ãã®ãã¹ãã£ã³ã°ãµãŒãã¹ã¯ä»ã«ãããããåºãŠããŸããVercel, Netlify, Amplify. ã©ããç°¡åã«ãããã€ã§ããã®ã§ãè©ŠããŠã¿ãŠãã ããã
å®è£ æé
- ç»é¢èšèšã»æ©èœãæžãåºã
- ããŒã¿ã¢ãã«ã®åãæžãåºã
- ã³ã³ããŒãã³ãæ§é ãèãã
- æ§é åããŠåãã©ã«ãã»ãã¡ã€ã«ãäœã
- ç»é¢ãå®è£ ããã衚瀺ããã®ã¯ãããŒããŒã¿
- ãŠãŒã¶ãŒèªèšŒãå®è£
- Flux(useReducer + Context)ã§ã€ã¶ããã®ç¶æ 管çãå®è£
- firebaseãã èªã¿åããæžã蟌ã¿ãã§ããããã«ãã
- firestoreã®ã«ãŒã«ãå®è£ ãã
- ãããã€ãã
æ¬¡ç« ããããã³ãºãªã³åœ¢åŒã§ãã¥ãŒããªã¢ã«ãæžããŠãããŸããã³ãŒããå šãŠæžããŠããããã§ã¯ãªãã®ã§ã説æã足ããªãéšåã¯Githubãªããžããªãåç §ããŠãã ãããããããããªãéšåãããã°ã質åããŠããã ãããšèšäºã®æ¹åã«ã€ãªãããŸãã
ç»é¢èšèšã»æ©èœã®æžãåºã
ã©ããªã¢ããªãäœãã«ããŠãèšèšã倧äºã§ãããŸãäžèšã§äœãäœããã決ããŸãã
- ãã¡ãã£ãšèŠãç®ã«æ°ã䜿ã£ãã·ã³ãã«ãªã€ã¶ããæçš¿ã¢ããª: Single Boardã
æ©èœäžèŠ§
次ã«ã¢ããªã«æ¬²ããæ©èœã決ããŸããSingle Boardã¯äžã«åºãæèããªãã£ãã®ã§ãæäœéãšç·Žç¿ãããæ©èœãã€ããããšã«ããŸããã
- æçš¿äžèŠ§ãèŠãã
- æçš¿ã«ã¯ãŠãŒã¶ãŒæ å ±ãšäœææ¥æãã€ã¶ããå 容ãèŒãã
- ãã°ã€ã³ããªããšæçš¿ã§ããªã
- googleãšã¡ãŒã«ã¢ãã¬ã¹ã§ãã°ã€ã³ã§ãã
ç»é¢èšèš
çŽã§ãUIããŒã«ã§ããã¯ãã§ããã©ãã§è¯ãã®ã§ãç»é¢ãäœããŸããå®éäœã£ãã®ããã®ãããã©ããç¬ããããããŒãžãšãæçš¿ã³ã³ããŒãã³ããæããŠããŸãã
ã©ããUIã©ã€ãã©ãªã«äŸåããããšã«ãªãã®ã§ãäžå¯§ã«SketchãFigmaã䜿ã£ãŠãã¶ã€ã³ããå¿
èŠã¯ãªãã§ããç»é¢æ°ãå€ãå Žåãéã«ãããã¿ã€ããšããŠFigmaããªããã§äœã£ãŠã¿ãã®ã¯ã¢ãªã
å®è£
ããããã¯å®è£ ããŠãããŸãããŸããcreate-react-app(CRA)ã§ãããžã§ã¯ããäœæããŸãã
npx create-react-app single-board --template typescript
CRAã§ã¯å¿ èŠãªã©ã€ãã©ãªãå šéšå ¥ã£ãŠããã®ã§ãèšå®ãªãã«ã³ãŒããæžãå§ããããŸããeslintãå ¥ã£ãŠããŸãã
ãã ãã³ãŒããã©ãŒãããããŒã«ã®prettierã¯ãããŸãããèªåã§ã³ãŒãã綺éºã«æŽåœ¢ããã人ã¯ã€ã³ã¹ããŒã«ããŸããããvscodeã䜿ã£ãŠãã人ã¯ãprettierã®æ¡åŒµæ©èœãã€ã³ã¹ããŒã«ããã°npm installããå¿
èŠããããŸããã.prettierrc
ã§èªåã®èšå®ã§ã³ãŒããã©ãŒãããããŠãããŸãã
åè:
https://create-react-app.dev/docs/setting-up-your-editor/#formatting-code-automatically
https://www.digitalocean.com/community/tutorials/how-to-format-code-with-prettier-in-visual-studio-code-ja
æå ã§ã§ããç»é¢ã確èªããŠã¿ãŠãã ãããããã©ã«ãã®ç»é¢ãç«ã¡äžãããŸãã
npm start
ãããžã§ã¯ããã©ã«ãã®äžã®ãåºæ¬çã«ã¯src/
ã®äžã«ã³ãŒããæžããŠããããšã«ãªããŸãã
ããããã®åæãã¡ã€ã«ã®èª¬æã¯å
¬åŒããŒãžãèªãã§ã¿ãŠãã ããã
https://www.digitalocean.com/community/tutorials/how-to-format-code-with-prettier-in-visual-studio-code-ja
ããŒã¿ã¢ãã«ã®åãæžãåºã
ãããžã§ã¯ããäœã£ãæãäœããæžããŠããã°è¯ããè¿·ããŸããããããŒã¿ã¢ãã«ããæžãããšã«ãã£ãŠãããŒã¿äžå¿ã«ã¢ããªãäœã£ãŠãããã®ã§ããããã§ããä»åã¯ã
- ã€ã¶ããäžèŠ§ã§ãŠãŒã¶ãŒåãšã€ã¶ããã®ããŒã¿ã衚瀺ããã
- ã€ã¶ãããæçš¿ãã
ãšããæ©èœã«å¿
èŠãªããŒã¿ã¢ãã«ãå®çŸ©ããŸããmodels.ts
ãšãããã¡ã€ã«ãäœæããŸãã
.
âââ src/
âââ models.ts
export type IUser = {
displayName: string | null | undefined
photoURL: string | null | undefined
}
export type IComment = {
user: IUser
content: string
createdAt: Date
id: string
}
export type ICommentAdd = {
user: IUser
content: string
}
衚瀺ã«äœ¿ãå±æ§ã ãå®çŸ©ããŸãã
ã³ã³ããŒãã³ãæ§é ãèãã
次ã«ViewãèŠãç®ã®éšåãäœã£ãŠãããŸããReactã§éçºããäžã§å€§äºãªèãæ¹ããã³ã³ããŒãã³ãå¿åã§ããç»é¢ãé©åãªåœ¹å²ããšã«ã³ã³ããŒãã³ãã§åãåããŠå®è£ ããããšã§å¯èªæ§ãä¿å®æ§ãäžãããŸãã
ã³ã³ããŒãã³ãã®çš®é¡ã«ã¯2çš®é¡ãããŸãã
- APIãšéä¿¡ããããç¶æ 管çã³ãŒããåŒãã ããç¶æ ãæã£ãŠãããããšããå¯äœçšãæã£ãå®äœã³ã³ããŒãã³ã
- åãåã£ãpropsã衚瀺ããçŽç²ãªé¢æ°ã³ã³ããŒãã³ã
- (Hooksã䜿ã£ãŠããã®ã³ã³ããŒãã³ãå ã§éããŠãããã®ãå«ã)
ç§ãä»åã¢ããªãäœã£ãŠãããšãã«ã¯ã
- ãŸãç»é¢ããã£ããåè ã®å®äœã³ã³ããŒãã³ãïŒãšåä»ããŠã¿ãïŒã§åããŠã¿ãã
- å®äœã³ã³ããŒãã³ããå®è£ ããäžã§å ±éåã§ããããªãã®ã¯é¢æ°ã³ã³ããŒãã³ãã«åããŠã¿ã
ãšãã颚ã«äœã£ãŠãããŸããã
- App
- Home
- Header
- Editor
- CommentList
- Comment
- UserAvatar
- Content
- Footer
- Login
- Header
- Form
èãæ¹ãšããŠã¯ãAtomic Designãåèã«ãç°¡æåããŠããŸããå®äœã³ã³ããŒãã³ããé¢æ°ã³ã³ããŒãã³ãã¯ãããã Organism, molecules ã«å¯Ÿå¿ããããšæããŸãã
倧äºãªã®ã¯ãé£ããèããã ãããã§åãåããŠããšã§å ±éåããããšããããšã§ããæåããDRYã§ããã®ã¯æªæã§ãã
å®è£ ããŠãããŸãã
ãã©ã«ãã»ãã¡ã€ã«ãæ§é åããŠäœæããã
å ã«å¿ èŠã«ãªããããªãã¡ã€ã«ãå šéšäœã£ãŠãããŸãã
.
âââ package-lock.json
âââ package.json
âââ public
âââ src
â âââ App.tsx # åã³ã³ããŒãã³ããåŒã³åºã
â âââ api # firestoreã®ã€ã³ã¿ãŒãã§ãŒã¹
â â âââ commentsApi.ts
â âââ components
â â âââ CommentList.tsx
â â âââ Editor.tsx
â â âââ Footer.tsx
â â âââ Header.tsx
â â âââ Home.tsx
â â âââ Login.tsx
â â âââ MainVisual.tsx
â â âââ UserAvatar.tsx
â âââ contexts
â â âââ authContext.tsx # ãŠãŒã¶ãŒèªèšŒç¶æ
管ç
â â âââ commentsContext.tsx #ãã€ã¶ããã®ç¶æ
管ç
â âââ reducers
â â âââ commentsReducer.ts #ãã€ã¶ããã®Flux (ããšã§è§£èª¬)
â âââ firebase.ts
â âââ index.tsx # App.tsxãåŒã³åºããŠããã ã
â âââ models.ts # ããŒã¿ã¢ãã«
â âââ theme.ts # å
šäœUIã®èšå®
âââ tsconfig.json
Viewãäœã
UIã©ã€ãã©ãªã®Chakra UIãã€ã³ã¹ããŒã«ããŸãã
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4
react-routerã䜿ããurlã«ãã£ãŠããããç»é¢ãšãã°ã€ã³ç»é¢ãåºãåããŸããå ¬åŒããã¥ã¡ã³ãã§ã¯ãµã³ãã«ãåãããã®ã§ããã¡ãããããããã§ãã
npm i react-router-dom
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import { Header } from './components/Header'
import { Login } from './components/Login'
import { Home } from './components/Home'
function App() {
return (
<Router>
<Header />
<Switch>
<Route exact path='/'>
<Home />
</Route>
<Route path='/login'>
<Login />
</Route>
</Switch>
</Router>
)
}
export default App
import { CommentList } from './CommentList'
import { MainVisual } from './MainVisual'
import { Editor } from './Editor'
import { Footer } from './Footer'
export const Home = () => (
<>
<MainVisual /> // äžçªäžã®ã¡ã€ã³ããžã¥ã¢ã«
<Editor /> // ã€ã¶ããç·šéãã©ãŒã
<CommentList /> // ã€ã¶ãããªã¹ã
<Footer /> // ããã¿ãŒ
</>
)
ãããŒããŒã¿ãäœãããšãããã衚瀺ããã®ç»é¢ãäœã£ãŠãããŸãã
import { HStack, Box, Avatar, Heading, Text } from '@chakra-ui/react'
import { IComment, IUser } from '../models'
// ãããŒããŒã¿
const user1: IUser = { displayName: 'testuser1', photoURL: 'sample.jpg' }
const dcomments: IComment[] = [
{
user: user1,
content:
'first comment ss',
createdAt: new Date(),
id: 'comment1id',
},
{
user: user1,
content: 'å
æ°ã§ãã',
createdAt: new Date(),
id: 'comment2id',
},
export const CommentList = () => {
return (
<>
<Heading>
Posted Comments
</Heading>
<ul>
{comments === [] ? (
<p>No Post</p>
) : (
// Comment å®è£
ã¯çç¥
comments.map((comment) => (
<Comment key={comment.id} comment={comment} />
))
)}
</ul>
</>
)
}
çç¥ããã³ã³ããŒãã³ãã¯ã¬ããžããªãã¿ãŠã¿ãŠãã ããã
èªèšŒç»é¢ã»æ©èœãäœæ
ãã°ã€ã³ç»é¢ãäœã£ãŠãããŸããFirebase AuthenticationãšFirebaseUIã䜿ãããšã§ç°¡åã«å®è£ ã§ããŸãã
firebaseã®èšå®
firebase consoleã§ãããžã§ã¯ããäœæããŸãã https://console.firebase.google.com/u/0/?hl=ja
äœæãããããããžã§ã¯ãã®èšå® > Firebase SDK snippet ãååŸããŸãã
CRAã¯å
ã
ã®èšå®ã§ãREACT_APP_
ããå§ãŸãç°å¢å€æ°åã.env
ãã¡ã€ã«ããã¢ããªã«çµã¿èŸŒãã§ãããŸãããããŠããã«ãæã«å€ãåã蟌ãã§ãããŸããããã§å€ã«å€æ°ãæŒããããšã¯ãããŸããã
https://create-react-app.dev/docs/adding-custom-environment-variables/
å
ã»ã©ååŸããå€ãå€æ°ãšããŠ.local.env
ãã¡ã€ã«ã«å®£èšãããããžã§ã¯ãã®ã«ãŒãã«ãããŸãã
REACT_APP_APIKEY=xxxxxx
REACT_APP_AUTHDOMAIN=xxxxxx
REACT_APP_PROJECTID=xxxxxx
REACT_APP_STORAGEBUCKET=xxxxxx
REACT_APP_MESSAGINGSENDERID=xxxxxx
REACT_APP_APPID=xxxxxx
REACT_APP_MEASUREMENTID=xxxxxx
ãããžã§ã¯ãå ã§ã¯ãfirebaseãæ±ããã¡ã€ã«ãäœããç°å¢å€æ°ãåããŸããã€ãã§ã«Firebaseã®èªèšŒãšããŒã¿ããŒã¹ã«firestoreã䜿ãã®ã§ãexportããŠãããŸãã
import firebase from 'firebase'
const fireConfig = {
apiKey: process.env.REACT_APP_APIKEY,
authDomain: process.env.REACT_APP_AUTHDOMAIN,
projectId: process.env.REACT_APP_PROJECTID,
storageBucket: process.env.REACT_APP_STORAGEBUCKET,
messagingSenderId: process.env.REACT_APP_MESSAGINGSENDERID,
appId: process.env.REACT_APP_APPID,
measurementId: process.env.REACT_APP_MEASUREMENTID,
}
firebase.initializeApp(fireConfig)
const auth = firebase.auth()
const firedb = firebase.firestore()
export { firebase, auth, firedb }
firebaseUIãå°å ¥
ãã°ã€ã³ç»é¢ãäœã£ãŠãããŸãã firebaseUIã®Reactçšã©ã€ãã©ãªãããã®ã§ã€ã³ã¹ããŒã«ããŸãã
ïŒå ¬åŒã®éçºè ãã¹ãããããŠããã¿ãããªã®ã§ãcanaryçã䜿ããŸãã https://github.com/firebase/firebaseui-web-react/pull/122
npm install react-firebaseui@canary
ãã°ã€ã³ã³ã³ããŒãã³ããäœããŸãããã°ã€ã³ãã©ãŒã ã®æåã¯uiConfig
å€æ°ã§èšå®ããŸãã
import { Center, Heading, VStack } from '@chakra-ui/layout'
import { primaryTextColor } from '../theme'
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth'
import { firebase, auth } from '../firebase'
const uiConfig = {
signInFlow: 'popup',
signInSuccessUrl: '/',
signInOptions: [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID,
],
}
export const Login = () => {
return (
<Center mt={8}>
<VStack>
<Heading size='md' color={primaryTextColor}>
Sign In
</Heading>
<StyledFirebaseAuth uiConfig={uiConfig} firebaseAuth={auth} />
</VStack>
</Center>
)
<StyledFirebaseAuth firebaseAuth={auth} />
ã§ãããžã§ã¯ãã®firebaseã€ã³ã¹ã¿ã³ã¹ãšUIãã€ãªããŠããŸãã
AuthContextã§èªèšŒç¶æ 管ç
ãã°ã€ã³ã»ç»é²ãã§ããããã«ãªã£ãã®ã§ãã»ãã·ã§ã³æ å ±:(ããã°ã€ã³ããŠãããã©ããããšããã°ã€ã³ããŠãããŠãŒã¶ãŒæ å ±ã)ãã¢ããªå ã§äœ¿ããããã«ããŸã
ãŠãŒã¶ãŒèªèšŒã®ç¶æ 管çã«ã¯ãContext APIã䜿çšããŸããæµããšããŠã¯ãContextãäœæããProviderã§ç¶æ ãä¿åããuseContextã§äœ¿ããŸãã å ¬åŒ: https://ja.reactjs.org/docs/context.html
èªèšŒçšã®Contextãæ±ããauthContext.ts
ãäœæããŸãã
import React, { createContext, useContext, useState, useEffect } from 'react'
import { firebase, auth } from '../firebase'
type AuthContextProps = {
user: firebase.User | null
}
const AuthContext = createContext<AuthContextProps>({
user: null,
})
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<firebase.User | any>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setUser(user)
setLoading(false)
})
return unsubscribe
}, [])
return (
<AuthContext.Provider value={{ user }}>
{!loading && children}
</AuthContext.Provider>
)
}
export const useAuth = () => {
return useContext(AuthContext)
}
auth.onAuthStateChanged
ã§ã¯ãŠãŒã¶ãŒã®èªèšŒç¶æ
ãç£èŠããŠããã°ã€ã³ããã°ã¢ãŠãæããšèªèšŒæ
å ±ãå€ãã床ã«åŒæ°ã«æž¡ããŠããã³ãŒã«ããã¯é¢æ°ãå®è¡ããŸãã
Provider
ãUnmountããæã«ç£èŠãæšãŠãå¿
èŠãããã®ã§ãuseEffectã®è¿ãå€ã«èšå®ããŠãŸãã
https://firebase.google.com/docs/auth/web/manage-users?hl=ja
ã»ãã·ã§ã³ã䜿çšãã
ä»å®çŸ©ããé¢æ°ã䜿ããã¢ããªäžã§ã»ãã·ã§ã³ãååŸã§ããããã«ããŸããããã¢ããªå
šäœãAuthProvider
ã§å²ã¿ãŸããããã§å²ãã ã©ã®ã³ã³ããŒãã³ãå
ã§ãuseAuth()
ã䜿ããããšã«ãªããŸãã
...
import { AuthProvider } from './contexts/authContext'
function App() {
return (
+ <AuthProvider>
<ChakraProvider theme={theme}>
<Router>
<Header />
<Switch>
<Route exact path='/'>
<Home />
</Route>
<Route path='/login'>
<Login />
</Route>
</Switch>
</Router>
</ChakraProvider>
+ </AuthProvider>
)
}
ããããŒã§ãã°ã€ã³ããŠããæã¯ãã°ã¢ãŠããã¿ã³ããã°ã€ã³ããŠããªããšãã¯ãã°ã€ã³ãªã³ã¯ã衚瀺ããŸãã
import { Link } from 'react-router-dom'
import { useAuth } from '../contexts/authContext'
import { auth } from '../firebase'
export const Header = () => {
const { user } = useAuth()
return (
<>
// ...çç¥
{user ? (
<Text as='button' onClick={() => auth.signOut()}>
Log Out
</Text>
) : (
<Link to='/login'>
<Text color='white'> Sign In</Text>
</Link>
)}
// ...
</>
)
ãããç»é¢ã®ãšãã£ã¿ãŒã§ããã°ã€ã³ããŠããæã®ã¿æçš¿ã§ããããã«ããŸãã
export const Editor = () => {
const { user } = useAuth()
const [content, setContent] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (content !== '' && user) {
// post content to server
} else if (!user) {
alert('Sign in first')
}
setContent('')
}
const handleChange = (e: React.FormEvent<HTMLTextAreaElement>) => {
setContent(e.currentTarget.value)
}
return (
<div>
<VStack
as='form'
onSubmit={handleSubmit}
>
<Textarea
name='content'
value={content}
onChange={handleChange}
placeholder="What's on your mind?"
/>
<Button type='submit' colorScheme='orange'>
post
</Button>
</VStack>
</div>
)
}
ããã§èªèšŒç¶æ 管çã¯çµããã§ãã
ã€ã¶ããã®ç¶æ 管ç
ã€ã¶ããã®ç¶æ 管çã§ãContextAPIã䜿ããç¶æ ãä¿æã§ããããã«ããŸããå ããŠãuseReducerãšããHookã䜿ãFluxã¢ãŒããã¯ãã£ã§ã®ç¶æ 管çãè¡ããŸããContextã ãã§ã管çã§ããªãããšã¯ãªãã§ãããç¶æ ãå€æŽããæ©èœãå€ããªã£ãŠããæã«åãããããã§ãã
reducerããå®çŸ©ããŠãããŸãã
import { IComment } from '../models'
export type CommentsAction =
| { type: 'SET_COMMENTS'; comments: IComment[] }
| { type: 'ADD_COMMENT'; comment: IComment }
export type CommentsState = {
comments: IComment[]
}
export const initialState: CommentsState = {
comments: [],
}
export const commentsReducer = (
state: CommentsState,
action: CommentsAction
): CommentsState => {
switch (action.type) {
case 'SET_COMMENTS':
return { comments: action.comments }
case 'ADD_COMMENT':
return { comments: [action.comment, ...state.comments] }
default:
return state
}
åŸã
firestoreã«ã€ã¶ãããæçš¿ããã¿ã€ãã³ã°ã§ãŸãã€ã¶ãããªã¹ããååŸãããã¹ããªãŒãã³ã°ããã°ææ°ã®ç¶æ
ã«ãªãã®ã§ãADD_COMMENT
ã¯ãã£ãŠããªããŠãè¯ãã®ã§ãããæ¯åAPIãåŒã°ãªããŠãè¯ãããã«ãšãç·Žç¿ã®ããã«äœã£ãŠããŸãã
ContextãäœããŸãã
import {
createContext,
Dispatch,
ReactNode,
useReducer,
useContext,
} from 'react'
import {
CommentsAction,
commentsReducer,
CommentsState,
initialState,
} from '../reducers/commentsReducer'
type CommentsContextProps = {
state: CommentsState
dispatch: Dispatch<CommentsAction>
}
const CommentsContext = createContext<CommentsContextProps>({
state: initialState,
dispatch: () => initialState,
})
export const CommentsProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(commentsReducer, initialState)
return (
<CommentsContext.Provider value={{ state, dispatch }}>
{children}
</CommentsContext.Provider>
)
}
export const useComments = () => useContext(CommentsContext)
EditorãšCommentListã§äœ¿ãã®ã§ãããããå«ãHomeã³ã³ããŒãã³ãã§å²ãã§ãããŸãã
export const Home = () => (
<>
+ <CommentsProvider>
<MainVisual />
<Editor />
<CommentList />
<Footer />
+ </CommentsProvider>
<>
)
ãã©ãŒã éä¿¡æã«DispatchããŸãã
export const Editor = () => {
const { user } = useAuth()
const { dispatch } = useComments()
const [content, setContent] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (content !== '' && user) {
const toPost: ICommentAdd = {
user: { displayName: user.displayName, photoURL: user.photoURL },
content,
}
dispatch({
type: 'ADD_COMMENT',
comment: {
...toPost,
createdAt: new Date(),
id: Date(),
},
})
} else if (!user) {
alert('Sign in first')
}
setContent('')
}
return (...)
}
useEffect
ã§ã³ã³ããŒãã³ããèªã¿èŸŒãã¿ã€ãã³ã°ã§DispatchããŸãã
export const CommentList = () => {
const { state, dispatch } = useComments()
const dcomments: IComment[] = [
{
user: user1,
content:
'first comment',
createdAt: new Date(),
id: 'comment1id',
},
{
user: user1,
content: 'å
æ°ã§ãã',
createdAt: new Date(),
id: 'comment2id',
},
]
useEffect(() => {
let unmount = false
if (!unmount) {
console.log('set comments called')
dispatch({ type: 'SET_COMMENTS', comments: dcomments })
}
return () => {
unmount = true
}
}, [dispatch])
return (...)
}
ããã§ã€ã¶ããïŒã³ã¡ã³ãïŒã®ç¶æ 管çã¯çµããã§ãã
firestore ãžã® read/write
firestoreäžã§ããŒã¿ã管çã§ããããã«ããŸãã
firebaseã³ã³ãœãŒã«ã§ firestoreãæå¹ã«ããŸãã
firestoreãžã®ã€ã³ã¿ãŒãã§ãŒã¹ãå®è£ ããŸããããã§å®è£ ããããšã§ãå°æ¥å¥ã®APIã䜿ã£ãæã«ããé¢æ°åãåŒæ°ãè¿ãå€ãåãã«ããããšã§ViewåŽãå€æŽããªããŠãè¯ãããã«ãççµåã«å®è£ ããŸãããã£ãšå³å¯ã«ãããªãinterfaceãå®çŸ©ããããDipendency Injectionãããããšã«ãªããŸãã
import { firedb, firebase } from '../firebase'
import { IComment, ICommentAdd } from '../models'
export const getComments = async () => {
const snapShot = await firedb
.collection('comments')
.orderBy('createdAt', 'desc')
.get()
const data = snapShot.docs.map<IComment>((doc) => ({
user: doc.data().user,
content: doc.data().content,
createdAt: doc.data().createdAt.toDate(),
id: doc.id,
}))
return data
}
export const addComment = async (comment: ICommentAdd) => {
return firedb.collection('comments').add({
user: comment.user,
content: comment.content,
createdAt: firebase.firestore.Timestamp.now(),
})
䜿çšæã«ã¯ãäž»ã«DispatchåŒã³åºãåã«ãããçµæãDispatchã«æž¡ããŸãã
ã³ã¡ã³ããªã¹ã
export const CommentList = () => {
const {
state: { comments },
dispatch,
} = useComments()
useEffect(() => {
+ getComments().then((data) => {
+ dispatch({ type: 'SET_COMMENTS', comments: data })
+ })
}, [dispatch])
return (...)
}
ãšãã£ã¿ãŒ
export const Editor = () => {
const { user } = useAuth()
const { dispatch } = useComments()
const [content, setContent] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (content !== '' && user) {
const toPost: ICommentAdd = {
user: { displayName: user.displayName, photoURL: user.photoURL },
content,
}
+ addComment({ ...toPost })
dispatch({
type: 'ADD_COMMENT',
comment: {
...toPost,
createdAt: new Date(),
id: Date(),
},
})
} else if (!user) {
alert('Sign in first')
}
setContent('')
}
ãã©ãŠã¶ã§ã€ã¶ãããŠã¿ããšãfirestoreã«ãããŒã¿ãè¿œå ãããŠããã®ãåãããŸã
firestore rule
Editor Component
ã§ããŠãŒã¶ãŒã§ã¯ãªãå Žåæçš¿ã§ããªãããã«ããŸããããçŽæ¥APIãç¥ãããŠããŸã£ãå Žåãæçš¿ã§ããŠããŸããŸããããã«ä»ã®ãŸãŸã ãšæçš¿ãããŠãŒã¶ãŒåãåœè£
ããŠãæ¬äººä»¥å€ã®åãéšãæçš¿ã§ããŠããŸããŸãã
ãã®ãããªããšããªãããã«ãã³ã³ãœãŒã«ã§rule
ãæžãããšã§ãã»ãã¥ãªãã£ãå®ããŸããããŒã«ã«ç°å¢ã§æžããŠãããã€ããããšãå¯èœã§ãããããã§ã¯ã³ã³ãœãŒã«ã«çŽæ¥æžããŠãŸãã
å·Šäžã®ã«ãŒã«ãã¬ã€ã°ã©ãŠã³ãã§ã¯ããããããªæ¡ä»¶ã§ã«ãŒã«ããã¹ãã§ããã®ã§ãè©ŠããŠã¿ããšè¯ãã§ãã
ä»åã®ã«ãŒã«ã¯ãã¡ã
- âcommentsâ以å€ã®ãªãœãŒã¹ã«ã¢ã¯ã»ã¹ã§ããªã
- (ãã°ã€ã³ããŠãªããŠã)誰ã§ãèªããããã«ãã
- ãŠãŒã¶ãŒåãšäžèŽããæçš¿ã®ã¿åãä»ããã
- æŽæ°ãåé€ã¯åãä»ããªã
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
match /comments/{comment} {
allow read: if true;
allow create: if request.auth.token.name == request.resource.data.user.displayName
}
}
}
}
å ¬åŒ: https://firebase.google.com/docs/rules/basics?hl=ja
Firebase Hosting ãžãããã€
ãããŸã§ã§ã¢ããªãå®æãããFirebase HostingãµãŒãã¹ã«ãããã€ããŸãã
ã³ã³ãœãŒã«ããHostingãæå¹ã«ããŸãã
# firebase cliãã€ã³ã¹ããŒã«ããŠãdeployã³ãã³ãã䜿ããããã«ããŸãã
npm install -g firebase-tools
# èªèšŒããŠã³ã³ãœãŒã«ã§äœã£ããããžã§ã¯ããéžæããŸãã
firebase login
firebaseã®ãã¡ã€ã«ãäœæããŸãã
firebase init
ããããèãããŸããHostingã ãéžæãã
What do you want to use as your public directory? ã«ã¯ build
ãæå®ããŸãã
ããšã¯å¥œããªãã®ãéžãã§ãã ããã
æçµçã«ãããªãã¡ã€ã«ãã§ããŠããã°å€§äžå€«ã§ãã
{
"hosting": {
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
Githubãšé£æºããŠCICDããã«ãªã¯ã§ãããã€ããŠãããã®ã§ããããã®èšå®ã¯æ±ããŸããã調ã¹ãŠã¿ãŠãã ããã
ãããžã§ã¯ãããã«ãåŸããããã€ããŸãïŒ
npm run build
firebase deploy --only hosting
ããŸãããã°ãã¿ãŒããã«ã«åºãŠããurlããããã€å ã§ãïŒïŒïŒ
çµããã«
ä»åäœã£ãã¢ããªã«ã¯ã€ã¶ããã®åé€ããŠãŒã¶ãŒèšå®ããŠãŒã¶ãŒããŒãžãªã©æ©èœã足ããŸããããããŸã§èªãã§ãããæ¹ã¯ãããçºå±ãããŠãæ¹é ãããŠãé¢çœããã®ãäœã£ãŠã¿ãŠãã ãããããäœã£ãæã¯ã³ã¡ã³ãããå ±åããŠããããšå¬ããã§ãã
åããŠå æ¬çãªèšäºãæžããã®ã§è¶³ããªããšããã¯ãããšæããŸããã楜ããã§ããã ãããäœããã®ã¢ã¯ã·ã§ã³ãããŠããããšå¬ããã§ãããããŸã§èªãã§ãããŠããããšãããããŸããã次åã¯ããã¡ãã£ãšé«åºŠãªããšããã³ã³ããŒãã³ãèšèšã«é¢ããããšãæžããããšæããŸãã