初探 state
畫面永遠由 state 產生,UI 只是 state 的 mapping。ie UI = f(state),只要關注畫面長甚麼樣子就可以了。
使用 state 的方式
- import React from 'react'
- 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
}
總結
- 用 useState 後面傳狀態的初始值。使用前需要先
import {useState} from 'react'
。 - useState 會回給你一個陣列,第一個參數是 state 的值、第二個參數是 setter function 透過他去 set 你的 state
- 改變 state 時 call setter function,裡面傳新的 state。
- 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 裡。所以這個東西也要我自己進行控制。
const [value, setValue] = useState('')
- 將 input 的值跟 state 做掛勾
<input type="text" placeholder="todo" value={value}/>
- 不管怎麼打字,畫面都不會變,因為我的 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)
}
成功地順利打字!!可喜可賀 ~
- 第一次 render 時,value 是空字串
- 當我按 a 他觸發 handleInputChange 這個事件,他的 e.target.value 是 a 所以 setValue(e.target.value) === setValue(a),我的 value 就是 a
- state 改變就會重新 render,所以 re-render 的時候 input 的 value 變成 a
- 當我再打 b,setValue(e.target.value) === setValue(ab),所以 const [value, setValue] 的 value 就是 ab
- 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
<input type="text" placeholder="todo"/>
- 拿到 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 中場總結
- Component (類似切版)
- Props (Component 可以傳自訂的 Props 進去,像是自訂的 html 元素,就可以在子層接收到這些東西)
- Style (css / inline-style / styled-component 寫法)
- Event Handler (加上 onClick / onSubmit / onMouseDown)
- JSX (render 簡單的 component、傳 js 用 { } 沒有迴圈跟 if-else,所以要用短路跟三元運算子寫、render 一系列 list 會用 map 變成陣列,只是 render 陣列要提供 key)
- State (用 useState 放初始值、用 setState 改變 state、immutable 的概念)
新增時會用一個新的陣列
刪除用 filter 產生新的陣列
編輯用 map 產生新的陣列