先來切個版
對之前的 todolist 先整理資料夾結構
- src 裡面會有 components 的資料夾,舉例針對 App.js, App.test.js, App.css 在 components 裡面再建一個資料夾 App 給他、再建立另一個資料夾 MessageBoard
- src 裡面有 constants 的資料夾,裡面有 style.css
re-export 目的是為了讓資料夾結構與檔案名稱乾淨一點
// App.js 同個資料夾內建立 index.js
import App from './App'
export default App;
// 寫法二
// export default App;
先用個簡單的版型
(想一下 component 怎麼下,接著開始切)
import React from 'react'
import styled from 'styled-components'
const Page = styled.div`
width: 300px;
margin: 0 auto;
`
const Title = styled.h1`
color: #333;
`
const MessageForm = styled.form`
margin-top: 16px;
`
const MessageTextArea = styled.textarea`
display: block;
width: 100%;
`
const SubmitButton = styled.button`
margin-top: 8px;
`
const MessageList = styled.div`
margin-top: 16px;
`
const Message = styled.div`
border: 1px solid black;
`
function App() {
return (
<Page>
<Title>留言板</Title>
<MessageForm>
<MessageTextArea rows={10} cols={20}/>
<SubmitButton>送出留言</SubmitButton>
</MessageForm>
<MessageList>
<Message>1234</Message>
</MessageList>
</Page>
);
}
export default App;
底下 message 是一個 component,將 props 開好
import React from 'react'
import styled from 'styled-components'
const Page = styled.div`
width: 300px;
margin: 0 auto;
`
const Title = styled.h1`
color: #333;
`
const MessageForm = styled.form`
margin-top: 16px;
`
const MessageTextArea = styled.textarea`
display: block;
width: 100%;
`
const SubmitButton = styled.button`
margin-top: 8px;
`
const MessageList = styled.div`
margin-top: 16px;
`
const MessageContainer = styled.div`
border: 1px solid black;
padding: 8px 16px;
border-radius: 8px;
`
const MessageHead = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`
const MessageAuthor = styled.div`
color: rgba(23, 78, 55, 0.3);
font-size: 14px;
`
const MessageTime = styled.div`
`
const MessageBody = styled.div`
font-size: 16px;
margin-top: 16px;
`
function Message({author, time, children}) { // children 表內容
return (
<MessageContainer>
<MessageHead>
<MessageAuthor>{author}</MessageAuthor>
<MessageTime>{time}</MessageTime>
</MessageHead>
<MessageBody>{children}</MessageBody>
</MessageContainer>
)
}
function App() {
return (
<Page>
<Title>留言板</Title>
<MessageForm>
<MessageTextArea rows={10} cols={20}/>
<SubmitButton>送出留言</SubmitButton>
</MessageForm>
<MessageList>
<Message author={'huli'} time="2021/9/27 10:10:11">我的留言</Message>
</MessageList>
</Page>
);
}
export default App;
來拿資料吧!把 API 串上去
- 用 state 存資料
- 做事情:useEffect / eventHandler。因為要在 component render 完之後拿資料,所以要使用 useEffect。dependency array 參數為空陣列,才是 component mount 才做一次
import React, {useEffect, useState} from 'react'
import styled from 'styled-components'
const API_ENDPOINT = 'https://student-json-api.lidemy.me/comments?_sort=createdAt&_order=desc'
const Page = styled.div`
width: 300px;
margin: 0 auto;
`
const Title = styled.h1`
color: #333;
`
const MessageForm = styled.form`
margin-top: 16px;
`
const MessageTextArea = styled.textarea`
display: block;
width: 100%;
`
const SubmitButton = styled.button`
margin-top: 8px;
`
const MessageList = styled.div`
margin-top: 16px;
`
const MessageContainer = styled.div`
border: 1px solid black;
padding: 8px 16px;
border-radius: 8px;
&:not(:first-child) {
margin-top: 8px;
}
`
const MessageHead = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`
const MessageAuthor = styled.div`
color: rgba(23, 78, 55, 0.3);
font-size: 14px;
`
const MessageTime = styled.div`
`
const MessageBody = styled.div`
font-size: 16px;
margin-top: 16px;
`
function Message({author, time, children}) { // children 表內容
return (
<MessageContainer>
<MessageHead>
<MessageAuthor>{author}</MessageAuthor>
<MessageTime>{time}</MessageTime>
</MessageHead>
<MessageBody>{children}</MessageBody>
</MessageContainer>
)
}
function App() {
const [messages, setMessages] = useState([])
const [apiError, setApiError] = useState(null)
useEffect(() => {
fetch(API_ENDPOINT)
.then(res => res.json())
.then((data) => {
setMessages(data)
})
.catch(err => {
setApiError(err.message)
})
}, [])
return (
<Page>
<Title>留言板</Title>
<MessageForm>
<MessageTextArea rows={10} cols={20}/>
<SubmitButton>送出留言</SubmitButton>
</MessageForm>
<MessageList>
{messages.map(message => (
<Message key={message.id} author={message.nickname} time={message.createdAt}>
{message.body}
</Message>
))}
</MessageList>
</Page>
);
}
export default App;
進一步優化:防呆設計
- 狀況一、api url 打錯:.catch 抓錯誤
- 狀況二、message 是空的錯誤處理
import React, {useEffect, useState} from 'react'
import styled from 'styled-components'
import PropTypes from 'prop-types'
const API_ENDPOINT = 'https://student-json-api.lidemy.me/comments?_sort=createdAt&_order=desc'
const Page = styled.div`
width: 360px;
margin: 0 auto;
`
const Title = styled.h1`
color: #333;
`
const MessageForm = styled.form`
margin-top: 16px;
`
const MessageTextArea = styled.textarea`
display: block;
width: 100%;
`
const SubmitButton = styled.button`
margin-top: 8px;
`
const MessageList = styled.div`
margin-top: 16px;
`
const MessageContainer = styled.div`
border: 1px solid black;
padding: 8px 16px;
border-radius: 8px;
&:not(:first-child) {
margin-top: 8px;
}
`
const MessageHead = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
padding-bottom: 4px;
`
const MessageAuthor = styled.div`
color: rgba(23, 78, 55, 0.3);
font-size: 14px;
`
const MessageTime = styled.div`
`
const MessageBody = styled.div`
font-size: 16px;
margin-top: 16px;
`
const ErrorMessage = styled.div`
margin-top: 16px;
color: red;
`
function Message({author, time, children}) { // children 表內容
return (
<MessageContainer>
<MessageHead>
<MessageAuthor>{author}</MessageAuthor>
<MessageTime>{time}</MessageTime>
</MessageHead>
<MessageBody>{children}</MessageBody>
</MessageContainer>
)
}
Message.propTypes = {
author: PropTypes.string,
time: PropTypes.string,
children: PropTypes.node
}
function App() {
const [messages, setMessages] = useState(null)
const [apiError, setApiError] = useState(null)
useEffect(() => {
fetch(API_ENDPOINT)
.then(res => res.json())
.then((data) => {
setMessages(data)
})
.catch(err => {
setApiError(err.message)
})
}, [])
return (
<Page>
<Title>留言板</Title>
<MessageForm>
<MessageTextArea rows={10} cols={20}/>
<SubmitButton>送出留言</SubmitButton>
</MessageForm>
{apiError && (
<ErrorMessage>
Something went wrong. {apiError.toString()/*render 物件會出問題要轉字串*/}
</ErrorMessage>
)}
{messages && messages.length === 0 && <div>No Message!</div>}
<MessageList>
{messages && messages.map(message => (
<Message key={message.id} author={message.nickname} time={new Date(message.createdAt).toLocaleString()}>
{message.body}
</Message>
))}
</MessageList>
</Page>
);
}
export default App;
實作新增留言功能
- textArea 要做成 unconrolled component 還是 controlled component => 決定:controlled component
- 送出留言的部分,表單按了會重新整理。因為是原生預設送出表單行為。所以事件要綁在表單
- 新增留言成功,應該再拿一次 API 的資料
- 錯誤訊息處理
- 送出留言按鈕狂按就會打 API 出去,需要一個是不是送出留言的 state
- loading components
import React, {useEffect, useState} from 'react'
import styled from 'styled-components'
import PropTypes from 'prop-types'
const API_ENDPOINT = 'https://student-json-api.lidemy.me/comments?_sort=createdAt&_order=desc'
const Page = styled.div`
width: 360px;
margin: 0 auto;
`
const Title = styled.h1`
color: #333;
`
const MessageForm = styled.form`
margin-top: 16px;
`
const MessageTextArea = styled.textarea`
display: block;
width: 100%;
`
const SubmitButton = styled.button`
margin-top: 8px;
`
const MessageList = styled.div`
margin-top: 16px;
`
const MessageContainer = styled.div`
border: 1px solid black;
padding: 8px 16px;
border-radius: 8px;
&:not(:first-child) {
margin-top: 8px;
}
`
const MessageHead = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
padding-bottom: 4px;
`
const MessageAuthor = styled.div`
color: rgba(23, 78, 55, 0.3);
font-size: 14px;
`
const MessageTime = styled.div`
`
const MessageBody = styled.div`
font-size: 16px;
margin-top: 16px;
`
const ErrorMessage = styled.div`
margin-top: 16px;
color: red;
`
const Loading = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
color: white;
font-size: 30px;
display: flex;
align-items: center;
justify-content: center;
`
function Message({author, time, children}) { // children 表內容
return (
<MessageContainer>
<MessageHead>
<MessageAuthor>{author}</MessageAuthor>
<MessageTime>{time}</MessageTime>
</MessageHead>
<MessageBody>{children}</MessageBody>
</MessageContainer>
)
}
Message.propTypes = {
author: PropTypes.string,
time: PropTypes.string,
children: PropTypes.node
}
function App() {
const [messages, setMessages] = useState(null)
const [messageApiError, setMessageApiError] = useState(null)
const [value, setValue] = useState()
const [postMessageError, setPostMessageError] = useState()
const [isLoadingPostMessage, setIsLoadingPostMessage] = useState(false)
const fetchMessages = () => {
return fetch(API_ENDPOINT)
.then(res => res.json())
.then((data) => {
setMessages(data)
})
.catch(err => {
setMessageApiError(err.message)
})
}
const handleTextareaChange = e => {
setValue(e.target.value)
}
const handleTextareaFocus = () => {
setPostMessageError(null)
}
const handleFormSubmit = e => {
// 先阻止表單發送的行為
e.preventDefault()
if (isLoadingPostMessage) {
return
}
setIsLoadingPostMessage(true)
fetch('https://student-json-api.lidemy.me/comments', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
nickname: 'hello', // 新增 nickname 輸入框,這邊先統一隨便留
body: value
})
})
.then(res => res.json())
.then(data => {
setIsLoadingPostMessage(false)
if (data.ok === 0) {
setPostMessageError(data.message)
return
}
// 新增留言成功
setValue('')
fetchMessages()
}).catch(err => {
setIsLoadingPostMessage(false)
setPostMessageError(err.message)
})
}
useEffect(() => {
fetchMessages()
}, [])
return (
<Page>
{isLoadingPostMessage && <Loading>Loading ...</Loading>}
<Title>留言板</Title>
<MessageForm onSubmit={handleFormSubmit}>
<MessageTextArea
rows={10}
cols={20}
value={value}
onChange={handleTextareaChange}
onFocus={handleTextareaFocus}
/>
<SubmitButton>送出留言</SubmitButton>
{postMessageError && <ErrorMessage>{postMessageError}</ErrorMessage>}
</MessageForm>
{messageApiError && (
<ErrorMessage>
Something went wrong. {messageApiError.toString()/*render 物件會出問題要轉字串*/}
</ErrorMessage>
)}
{messages && messages.length === 0 && <div>No Message!</div>}
<MessageList>
{messages && messages.map(message => (
<Message key={message.id} author={message.nickname} time={new Date(message.createdAt).toLocaleString()}>
{message.body}
</Message>
))}
</MessageList>
</Page>
);
}
export default App;