[FE302] React 基礎 - hooks 版本:React 實戰篇 - 留言板


Posted by s103071049 on 2021-09-30

先來切個版

對之前的 todolist 先整理資料夾結構

  1. src 裡面會有 components 的資料夾,舉例針對 App.js, App.test.js, App.css 在 components 裡面再建一個資料夾 App 給他、再建立另一個資料夾 MessageBoard
  2. 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 串上去

API:Lidemy 學生專用 API Server

  1. 用 state 存資料
  2. 做事情: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;

進一步優化:防呆設計

  1. 狀況一、api url 打錯:.catch 抓錯誤
  2. 狀況二、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;

實作新增留言功能

  1. textArea 要做成 unconrolled component 還是 controlled component => 決定:controlled component
  2. 送出留言的部分,表單按了會重新整理。因為是原生預設送出表單行為。所以事件要綁在表單
  3. 新增留言成功,應該再拿一次 API 的資料
  4. 錯誤訊息處理
  5. 送出留言按鈕狂按就會打 API 出去,需要一個是不是送出留言的 state
  6. 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;









Related Posts

使用 node.js 寫出串接 API 的程式

使用 node.js 寫出串接 API 的程式

【Day00】系列介紹

【Day00】系列介紹

Day 63 - SQLAlchemy & Bookshelf Project

Day 63 - SQLAlchemy & Bookshelf Project


Comments