[FE302] React 基礎 - hooks 版本:state


Posted by s103071049 on 2021-09-06

初探 state

畫面永遠由 state 產生,UI 只是 state 的 mapping。ie UI = f(state),只要關注畫面長甚麼樣子就可以了。

使用 state 的方式

  1. import React from 'react'
  2. React.useState() 會回傳一個陣列,透過解構語法使我們不用 renaming
function useState() {
  return [123, 456]
}
const [a, b] = useState()

// 通常會用解構的寫法表示 
import {useState} from 'react'
const [todos, setTodos] = useState([
    123, 456, 555 // todos 的初始值
  ])

useState is a hook,透過這樣的方式在 function component 裡面使用 state。

function App() {
  const [counter, setCounter] = React.useState(0) // 初始值是 0
  const handleButtonClick = () => {
    setCounter(counter + 1)
  } 
  return (
    <div className="App">
      counter: {counter}
      <button onClick={handleButtonClick}>increment</button>
      <TodoItem content={21}/>
      <BlackTodoItem content={212} size="XL" />
    </div>
  );
}

React 裡面有個動作是 render,意思是他執行 app 這個 function, react 會將 return 的東西放到畫面上,放到畫面上這個動作是 mount,就是把東西放到 dom 上去。mount 完後再更新 state,他就會 re-render,re-render 就是再呼叫一次 app 這個 function 的意思。

加上 console 看得更清楚

function App() {
  const [counter, setCounter] = React.useState(0) // 初始值是 0
  console.log('render', counter)
  const handleButtonClick = () => {
    console.log('button click')
    setCounter(counter + 1)
  } 
  return (
    <div className="App">
      counter: {counter}
      <button onClick={handleButtonClick}>increment</button>
      <TodoItem content={21}/>
      <BlackTodoItem content={212} size="XL" />
    </div>
  );
}

jsx 裡沒有迴圈,所以只能透過 functional 的方式做,

render 一系列資料:透過 map 的方式將 array 裡的每個東西都 map 成一個 component,他就會變成很多 component 的意思。

兩個寫法等價

  const [todos, setTodos] = React.useState([
    123, 456, 555
  ])

  {// render 多個 component 可以傳陣列
    [<TodoItem content={21}/>, <TodoItem content={212}/>]
  }

  { 
    todos.map(todo => <TodoItem content={todo} />)
  }

console 出現錯誤訊息 : Warning: Each child in a list should have a unique "key" prop.

render 一個陣列要幫他加上一個 key,key 會幫 react 辨別他是哪一個 item。一般來說不推薦用 index 當 key,但我們暫時先用 index 當 key。

  {
    todos.map((todo, index) => <TodoItem key={index} content={todo} />)
  }

注意一:改 state 要用 setter 的方式改,不是用 push 方式亂改。

注意二:因為 .push 會改變原本的 todos,所以 todos.push(123) 後我的 todos 變成 [123, 456, 555, 123],而 setTodos(todos) 的 todos 也是 [123, 456, 555, 123],當 react 判定舊的 state 與新的 state 一樣,他就部會做事情。因為更新成一樣的 state 就等於不更新。

  const [todos, setTodos] = React.useState([
    123, 456, 555
  ]) 
  const handleButtonClick = () => {
    todos.push(123)
    setTodos(todos) // 裡面傳新的 todos 
  }

state is immutable in react,所以對於新增來說你會用解構語法這麼寫

前面是將 todos 的值複製過來,後面是新增的東西。

  const handleButtonClick = () => {
    setTodos([...todos,  Math.random()]) // 裡面傳新的 todos 
  }

總結

  1. 用 useState 後面傳狀態的初始值。使用前需要先 import {useState} from 'react'
  2. useState 會回給你一個陣列,第一個參數是 state 的值、第二個參數是 setter function 透過他去 set 你的 state
  3. 改變 state 時 call setter function,裡面傳新的 state。
  4. react state is immutable,也就是 state 基本上不變,不變指的是不能直接改他,ex:todos[1] = 100。所以要改 state 時要產生出一個新的 state 出來,怎麼產生會用一些慣用的 pattern。

再探 state 之新增 todo

jsx 裡面沒有內容一定要加上 slash,< />

加上 todo 功能

function App() {
  const [todos, setTodos] = useState([
    123, 456, 555
  ]) // 初始值是 0
  const handleButtonClick = () => {
    setTodos([...todos, Math.random()]) // 裡面傳新的 todos 
  } 
  return (
    <div className="App">
      <input type="text" placeholder="todo"/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo, index) => <TodoItem key={index} content={todo} />)
      }
    </div>
  );
}

react component 有兩種類型,以有無被 react 控制進行劃分。

一、controller component
二、uncontrollered component

一、controller component

在 react 裡面幾乎所有會動的東西都有它的狀態,當我在畫面上打字時,打的字也會在 react 的 state 裡。所以這個東西也要我自己進行控制。

  1. const [value, setValue] = useState('')
  2. 將 input 的值跟 state 做掛勾 <input type="text" placeholder="todo" value={value}/>
  3. 不管怎麼打字,畫面都不會變,因為我的 state 沒有改變。可以在 input 上聽一個 onChange 事件,命名慣例會用 handle 開頭,<input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
  const handleInputChange = (e) => {
    console.log(e.target.value) // 會將打的字的值給印出來,所以
    setValue(e.target.value)
  }

成功地順利打字!!可喜可賀 ~

  1. 第一次 render 時,value 是空字串
  2. 當我按 a 他觸發 handleInputChange 這個事件,他的 e.target.value 是 a 所以 setValue(e.target.value) === setValue(a),我的 value 就是 a
  3. state 改變就會重新 render,所以 re-render 的時候 input 的 value 變成 a
  4. 當我再打 b,setValue(e.target.value) === setValue(ab),所以 const [value, setValue] 的 value 就是 ab
  5. state 改變就會重新 render,所以 re-render 的時候 input 的 value 變成 ab,<input type="text" placeholder="todo" value={ab} onChange={handleInputChange}/>

透過這樣的操作,就可以將 input 的 state 的值給放到 state 裡面去。這是方法一、controller component,我的value 是有放在 state 裡面

二、uncontrollered component

  1. <input type="text" placeholder="todo"/>
  2. 拿到 input value 的方式:一、使用 className,透過 document.querySelector('.input-todo').value 拿值;二、透過 ref 存取 input dom 的元素,需要 import ref 的 hook:import {useState, useRef} from 'react'<input ref={inputRef} className="input-todo" type="text" placeholder="todo"/>
const inputRef = useRef()
const handleButtonClick = () => {
console.log(inputRef) // inputRef 是 obj 會有 current 這個 key
console.log(inputRef.current) // 他的 dom 元素
console.log(inputRef.current.value) // 輸入的東西
setTodos([...todos, Math.random()])
}

小結

input 的值有放到 state 裡面就是 controlled,沒有就是 uncontrolled
參考:官方文件 forms 裡面 controlled components,官方文件都是提供 class component 的用法,但可以透過 google 關鍵字 controlled components hooks / function components

controlled components 的方式完成新增 todo-list

import './App.css';
import styled from 'styled-components'
import {useState} from 'react'
import TodoItem from './TodoItem';

function App() {
  const [todos, setTodos] = useState([
    123
  ]) // 初始值是 0
  const [value, setValue] = useState('')
  const handleButtonClick = () => {
    setTodos([value, ...todos])
    setValue('') // 將 todo 清空
  }
  const handleInputChange = (e) => {
    setValue(e.target.value)
  }
  return (
    <div className="App">
      <input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo, index) => <TodoItem key={index} content={todo} />)
      }
    </div>
  );
}
export default App;

刪除、編輯有 todo 的 id 會比較好做事。此外也要儲存 todo 的其他狀態,

現在加上 id 的功能

import './App.css';
import styled from 'styled-components'
import {useState} from 'react'
import TodoItem from './TodoItem';
let id = 2 // 每次 render 都會重新呼叫 App 所以 id 要放外面
function App() {
  const [todos, setTodos] = useState([
    {id: 1, content: 'abc'}
  ])
  const [value, setValue] = useState('')
  const handleButtonClick = () => {
    setTodos([{
      id, content: value
    }, ...todos])
    setValue('') // 將 todo 清空
    id ++
  }
  const handleInputChange = (e) => {
    setValue(e.target.value)
  }
  return (
    <div className="App">
      <input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo) => <TodoItem key={todo.id} content={todo.content} />)
      }
    </div>
  );
}
export default App;

將 id 用 useState 的方式表達也可以。但因為 id 沒有在畫面上出現,所以 id 改變不用重新渲染。react 的原則是 state 改變就會重新渲染,所以我們會不希望存成 state。

function App() {
  const [todos, setTodos] = useState([
    {id: 1, content: 'abc'}
  ])
  const [value, setValue] = useState('')
  const [id, SetId] = useState(2)
  const handleButtonClick = () => {
    setTodos([{
      id, content: value
    }, ...todos])
    setValue('') // 將 todo 清空
    //id ++
    SetId(id + 1)
  }

我們也可以用 userRef 的方式寫,const id = useRef(2) // 他的初始值,useRef 比較神奇的點在為了讓值給以保存,在 component re-render 不會變,除了可以當 state 用也可以直接操作,他會維持原本的東西。使用 useRef 他會回傳一個物件,裡面的 current 值是你設定的初始值。

之所以會有 current 是因為物件指向的問題,

function App() {
  const [todos, setTodos] = useState([
    {id: 1, content: 'abc'}
  ])
  const [value, setValue] = useState('')
  const id = useRef(2) // 他的初始值
  const handleButtonClick = () => {
    setTodos([{
      id: id.current, 
      content: value
    }, ...todos])
    setValue('') // 將 todo 清空
    //id ++
    id.current ++
  }
  下面略

為了確定有無成功,可以這麼做,todos.map((todo) => <TodoItem key={todo.id} todo={todo} />),將整個 todo 傳到 TodoItem,

function TodoItem ({className, size, todo}) {
  return (
    <div className="App">
      <TodoItemWrapper className={className} data-todo-id={todo.id}>
        <TodoContent size={size}>{todo.content}</TodoContent>
        <TodoButtonWrapper>
          <Button>completed</Button>
          <GreenButton>deleted</GreenButton>
        </TodoButtonWrapper>
      </TodoItemWrapper>
    </div>
  )
}

react 裡面使用表單相關,就要處理事件,

加上刪除 todo

App 是父親它 render TodoItem 小孩,所以我要怎麼在 TodoItem 改到 App ?

把這個 function 當作 props 傳給 TodoItem。將要做事情的 function 寫在 parent 然後傳給 child,child 再呼叫這個 function 就可以在 parent 這邊處理這個資訊。

// TodoItem
function TodoItem ({className, size, todo, handleDeleteTodo}) {
  return (
    <div className="App">
      <TodoItemWrapper className={className} data-todo-id={todo.id}>
        <TodoContent size={size}>{todo.content}</TodoContent>
        <TodoButtonWrapper>
          <Button>completed</Button>
          <GreenButton onClick= {() => {
            handleDeleteTodo(todo.id)
          }}>deleted</GreenButton>
        </TodoButtonWrapper>
      </TodoItemWrapper>
    </div>
  )
}

刪除不會改原本的陣列,會產生新的陣列。用 filter

// App
上略
  const handleDeleteTodo = id => {
    setTodos(todos.filter(todo => todo.id !== id)) // todo.id 不等於要刪除的 id 就會留下來
  }
  return (
    <div className="App">
      <input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo) => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo}/>)
      }
    </div>
  );
}

加上編輯 todo 功能

先檢視資料結構:現在只有 id, content

  const [todos, setTodos] = useState([
    {id: 1, content: 'abc'}
  ])

再加上 isDone,然後我們先調整介面

function App() {
  const [todos, setTodos] = useState([
    {id: 1, content: 'done', isDone: true},
    {id:2, content: 'not done', isDone: false}
  ])
  const [value, setValue] = useState('')
  const id = useRef(3) // 他的初始值

對 TodoItem 來說如果已完成他的 isDone 是 true,這時就要顯示未完成,反之 fase 已完成。

jsx 沒有 if-else,所以可以用短路或三元運算子去寫。然後加上刪除線的功能。

傳進去的 props 除了會給 styled-component 外,也會架在 dom 上去,傳的變數前面加上 $ 字號,她就不會被傳到 dom 上去。參考:transient props

const TodoContent = styled.div`
  color: ${props => props.theme.colors.red_300};
  font-size: 24px;
  ${props => props.size === 'XL' && `font-size: 20px;`};
  ${props => props.$isDone && `text-decoration: line-through`}
`
        <TodoContent $isDone={todo.isDone} size={size}>{todo.content}</TodoContent>
        <TodoButtonWrapper>
          <Button>
            {todo.isDone ? '未完成' : '已完成'}
          </Button>

處理 mark 的部份,用 map
基本上新增用解構的語法、刪除用 filter、修改用 map ,這是固定用法

// App
  const hadnleToggleIsDone = id => {
    setTodos(todos.map(todo => {
      if (todo.id !== id) return todo // 這個 id 不是我要修改的 id 我就將原本的 todo 回傳
      return {
        ...todo, // todo 原本的東西
        isDone: !todo.isDone // 我要修改的屬性
      }
    }))
  }
  return (
    <div className="App">
      <input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo) => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} hadnleToggleIsDone={hadnleToggleIsDone}/>)
      }
    </div>
  );

將它拆出來,可讀性會比 inline function 高很多。

function TodoItem ({className, size, todo, handleDeleteTodo, hadnleToggleIsDone}) {
  const handleToggleClick = () => {
    hadnleToggleIsDone(todo.id)
  }
  return (
    <div className="App">
      <TodoItemWrapper className={className} data-todo-id={todo.id}>
        <TodoContent $isDone={todo.isDone} size={size}>{todo.content}</TodoContent>
        <TodoButtonWrapper>
          <Button onClick={handleToggleClick}>
            {todo.isDone ? '未完成' : '已完成'}
          </Button>
          <GreenButton onClick= {() => {
            handleDeleteTodo(todo.id)
          }}>deleted</GreenButton>
        </TodoButtonWrapper>
      </TodoItemWrapper>
    </div>
  )
}

todo list 中場總結

  1. Component (類似切版)
  2. Props (Component 可以傳自訂的 Props 進去,像是自訂的 html 元素,就可以在子層接收到這些東西)
  3. Style (css / inline-style / styled-component 寫法)
  4. Event Handler (加上 onClick / onSubmit / onMouseDown)
  5. JSX (render 簡單的 component、傳 js 用 { } 沒有迴圈跟 if-else,所以要用短路跟三元運算子寫、render 一系列 list 會用 map 變成陣列,只是 render 陣列要提供 key)
  6. State (用 useState 放初始值、用 setState 改變 state、immutable 的概念)

新增時會用一個新的陣列
刪除用 filter 產生新的陣列
編輯用 map 產生新的陣列










Related Posts

F2E合作社|清單群組|Bootstrap 5網頁框架開發入門

F2E合作社|清單群組|Bootstrap 5網頁框架開發入門

[Note] React: Component props

[Note] React: Component props

Express 框架 - debug 補

Express 框架 - debug 補


Comments