在 React JSX 的語法裡面,沒有「迴圈」的概念,因此如果要 render 陣列(一系列的資料)的話,一定要用 .map()
這種 functional 的方式去做
因為沒有迴圈,所以 .forEach()
也不會有東西,所以會用 .map()
用 .map()
的好處是:可以把陣列裡面的每一個東西,都 map 成一個 component,如下程式碼
這時的 todos
陣列就會是 [123, 456]
.map()
會產生出一個陣列
function App() {
const [todos, setTodos] = React.useState([
123, 456
]);
const handleButtonClick = () => {
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem content={todo} />)
}
</div>
);
}
上面用 .map()
的寫法,就等同於是下面這樣寫:
用大括號包住一個陣列,陣列裡面傳入我要 render 的很多 components
function App() {
const [todos, setTodos] = React.useState([
123, 456
]);
const handleButtonClick = () => {
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
[<TodoItem content={123} />, <TodoItem content={456} />]
}
</div>
);
}
在 console 會看到一個 warning 寫說:「Warning: Each child in a list should have a unique "key" prop.」
用 JSX 來 render 一個陣列時,要幫每個 component 加上一個 key
的 prop,讓 React 可以辨別陣列裡面的每一個 item
key
是不能重複的
這裡,會先用 index
當作 key(但其實是不建議用 index
當作 key)
建議是用 todo.id
當作 key (請看 範例 - 新增 todo)
setTodos
的錯誤寫法
function App() {
const [todos, setTodos] = React.useState([
123, 456
]);
const handleButtonClick = () => {
todos.push(777); // 這時的 todos 會是 [123, 456, 777]
setTodos(todos); // setTodos 裡面的 todos 也是 [123, 456, 777]
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
上面這樣寫,會發現
點擊「Add todo 按鈕」不會有任何反應,原因為:
todos.push(777)
之後,這時的 todos
會是 [123, 456, 777]
(這是舊的 state)
setTodos(todos)
裡面的 todos 也是 [123, 456, 777]
(這是新的 state)
當 React 判定「舊的 state」跟「新的 state」是一樣時,就不會做任何事情
所以,我不能「直接改變原本的 state」,而是要「在 setTodos
產生一個新的 state」
原本的 todos
是「不會變的」
setTodos
的正確寫法
在 React 的 state 是「immutable 不可改變的」
因此,如果是要「新增」的話,要這樣寫:
setTodos([...todos, 777]);
在 setTodos()
裡面,建立一個新的陣列,用解構的語法把 todos
原本的值複製過來,後面再加上我要新增的東西
import styled from 'styled-components';
import TodoItem from './TodoItem';
import { useState } from 'react'; // 用解構的語法
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
const [todos, setTodos] = useState([
123, 456
]);
const handleButtonClick = () => {
setTodos([...todos, 777]);
}
return (
<div className="App">
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
export default App;
新增 todo
React 的 component 有分兩種:
第一種:controlled component
controlled component 就是:value 有放在 state 裡面
範例:
把 input
的 value
(也就是 input
會顯示在畫面上的值) 放到一個 state 裡面
每當 state 改變,就會 re-render
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
123, 456
]);
const handleButtonClick = () => {
setTodos([...todos, 777]);
}
const handleInputChange = (e) => {
setValue(e.target.value);
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
第二種:uncontrolled component
uncontrolled component 就是:value 沒有放在 state 裡面
要拿出 input
的 value 有幾種方法
方法一:
在 input
裡面不加上 value={value} onChange={handleInputChange}
這兩個
當我要拿出 input
的 value 時我再用 document.querySelector('.input-todo').value
拿出來
function App() {
const [todos, setTodos] = useState([
123, 456
]);
const handleButtonClick = () => {
const value = document.querySelector('.input-todo').value;
setTodos([...todos, 777]);
}
return (
<div className="App">
<input className="input-todo" type="text" placeholder="add todo" />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
方法二:
先在上方引入 useRef
這個 function
在 input
傳入一個參數 ref
來存取到這個 DOM 元素
import styled from 'styled-components';
import TodoItem from './TodoItem';
import { useState, useRef } from 'react'; // 用解構的語法
const YellowTodoItem = styled(TodoItem)`
background-color: yellow;
`
function App() {
const [todos, setTodos] = useState([
123, 456
]);
const inputRef = useRef();
const handleButtonClick = () => {
console.log(inputRef);
setTodos([...todos, 777]);
}
return (
<div className="App">
<input ref={inputRef} className="input-todo" type="text" placeholder="add todo" />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} />)
}
</div>
);
}
export default App;
console.log(inputRef)
會印出一個物件,物件裡面有 current 這個值(這是 React 提供的)
因此,inputRef.current
就會是 input
這個 DOM 元素
我就可以用 inputRef.current.value
來拿到 input
的 value
範例 - 新增 todo
接下來的範例,會使用「controlled component」這個方法
在 setTodos()
用 ...todos
的方式來新增 todo
在 setTodos()
小括號裡面,要產生一個新的陣列
import TodoItem from './TodoItem';
import { useState } from 'react'; // 用解構的語法
let id = 2; // 會一直遞增的 id
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id,
content: value
}, ...todos]);
setValue('');
id ++
}
return (
<div className="App">
<input type="text" placeholder="add 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
存成 state
不好的原因是:
id 的改變並不會造成「畫面的改變」,並不需要 re-render 畫面
但是如果把 id
存成 state,每當 id
改變時(state 改變),畫面就會 re-render,造成不必要的效能浪費
import TodoItem from './TodoItem';
import { useState } from 'react'; // 用解構的語法
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const [id, setId] = useState(2);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id,
content: value
}, ...todos]);
setValue('');
setId(id + 1);
}
return (
<div className="App">
<input type="text" placeholder="add 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
存成 state
推薦的做法:用 useRef
useRef
為了要讓「值可以保存住」,useRef
可以當成 state 來用,也可以直接操作。但是在 component re-render 時,useRef
的值不會變
- 用
useRef(2)
來設定id
的初始值是 2 useRef(2)
會回傳下面的物件,所以要用id.current
才能拿到useRef(2)
小括號裡面的值,也就是 2
{
current: 2
}
- 要更新
id
的值,就用id.current++
todos.map(todo => <TodoItem key={todo.id} todo={todo} />)
為了確定是否有成功把id
放到 todo 上面,可以在<TodoItem>
直接把整個todo
傳入
App.js:
import TodoItem from './TodoItem';
import { useState, useRef } from 'react'; // 用解構的語法
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const id = useRef(2);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value
}, ...todos]);
setValue('');
id.current++
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} />)
}
</div>
);
}
export default App;
TodoItem.js:
export default function TodoItem({ className, size, todo }){
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<RedButton>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
這樣就可以看到:
data-todo-id
有成功放到 todo 上面了
加上「刪除 todo」的功能
在 App.js 裡面,會 render 出 TodoItem,所以
- App.js 是 parent(父層)
- TodoItem 是 child(子層)
當我按下了在 TodoItem.js 的 <RedButton>刪除</RedButton>
,我要怎麼去改變 App.js 裡面的 state 呢?
作法如下:
React 重要觀念
把「要做事情的 function」寫在 parent,再把這個 function 傳給 child
當 child 呼叫這個 function,我就可以在 parent 處理這個 function 該做的事情和資訊
這樣就可以達成「在 child 改變 “parent 的 state”」
在 App.js(父層)
在 App.js 宣告一個 function 叫做 handleDeleteTodo
,會接收一個 id
把 handleDeleteTodo
這個 function 當作 prop 傳給 <TodoItem>
App.js:
function App() {
const [value, setValue] = useState('');
const [todos, setTodos] = useState([
{id: 1, content: 'abc'}
]);
const id = useRef(2);
const handleInputChange = (e) => {
setValue(e.target.value);
}
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value
}, ...todos]);
setValue('');
id.current++
}
const handleDeleteTodo = id => {
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} />)
}
</div>
);
}
在 TodoItem.js(子層)
這時,在 TodoItem.js 裡面的 TodoItem
就可以接收到 handleDeleteTodo
這個 function
接著,就可以在 TodoItem
component 的 <RedButton>
加上 onClick
事件:當點擊刪除按鈕時,執行 handleDeleteTodo
function,並傳入 todo.id
TodoItem.js:
export default function TodoItem({ className, size, todo, handleDeleteTodo }){
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<RedButton onClick={() => {
handleDeleteTodo(todo.id)
}
}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
在 setTodos()
用 .filter()
來刪除指定的 todo
執行 handleDeleteTodo
function 就會去呼叫在 App.js 的 handleDeleteTodo
function
我就可以在 App.js 裡面,用 handleDeleteTodo
function 接收到的 id
來把該 todo 刪掉
因此,在 handleDeleteTodo
function 裡面,用 setTodo()
來更新 todos
陣列:
不能用 .splice()
來刪除 todo,因為 .splice()
會改到原本的 todos
陣列
要用 .filter()
來刪除 todo,因為 .filter()
會產生一個新的陣列,不會去改到原本的 todos
陣列
App.js:
...
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} />)
}
</div>
);
}
render 出 todo 的「已完成/未完成」
因為在 JSX 語法中,沒有 if...else
,所以要寫判斷式的話,有兩個方式:
方式一:用「三元運算子」 (如果條件就只有 true/false,那就用三元運算子即可)
TodoItem.js:
用 {todo.isFinished ? '未完成' : '已完成'}
三元運算子來決定 todo
是要顯示已完成 or 未完成
export default function TodoItem({ className, size, todo, handleDeleteTodo }){
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>
{todo.isFinished ? '未完成' : '已完成'}
</Button>
<RedButton onClick={() => {
handleDeleteTodo(todo.id)
}
}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
然後,同樣在 TodoItem.js 裡面,就可以在 TodoContent
的 styled component 用 props
加上 text-decoration: line-through
這段
用 &&
意思就是:如果 props.isFinished
是 true 的話,就會回傳後面那行 text-decoration: line-through;
const TodoContent = styled.div`
color: ${props => props.theme.colors.primary_500};
font-size: 16px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
${props => props.isFinished && `
text-decoration: line-through;
`}
`
方式二:用短路運算子 (如果條件有很多個,不只有 true/false,那就要用短路運算子)
{todo.isFinished && '未完成'}
意思是:如果todo.isFinished
是 true,就會回傳 '未完成'{!todo.isFinished && '已完成'}
意思是:如果todo.isFinished
是 false,就會回傳 '已完成'
export default function TodoItem({ className, size, todo, handleDeleteTodo, handleToggleIsFinished }){
const handleToggleClick = () => {
handleToggleIsFinished(todo.id)
}
const handleDeleteClick = () => {
handleDeleteTodo(todo.id)
}
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button onClick={handleToggleClick}>
{todo.isFinished && '未完成'}
{!todo.isFinished && '已完成'}
</Button>
<RedButton onClick={handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
transient prop 的觀念
可參考文件 Transient props
在 component 傳入的 props
,除了會傳給 styled component 之外,也會直接傳到 DOM 上面
例如我在 <TodoContent>
加上一個 props
叫做 id="abc"
<TodoContent id="abc" isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
render 出來的 DOM 就會有 id="abc"
這個屬性:
但如果我不想要把我傳入的 props
傳給 DOM,我只想要傳給 styled component 做 style 的處理就好,那就在 props
前面加一個 $
即可
加上 $
就會是一個 transient prop
在 id="abc"
前面加一個 $
<TodoContent $id="abc" isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
render 出來的 DOM 就不會有 id="abc"
這個屬性了:
在 styled component 用時,就要寫成 props.$id
,而不是 props.id
:
const TodoContent = styled.div`
color: ${props => props.theme.colors.primary_500};
font-size: 16px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
${props => props.$isFinished && `
text-decoration: line-through;
`}
${props => props.$id && `
color: red
`}
`
改變 todo 的「已完成/未完成」
宣告一個 handleToggleIsFinished
function 來處理「已完成/未完成」
在 setTodos()
用 .map()
修改指定的 todo
的屬性
.map()
會產生出一個新的陣列,我就可以在這個新的陣列裡面修改那個 id
的 todo
(修改 isFinished
的狀態)
然後,要把 handleToggleIsFinished
當作 prop 傳到 <TodoItem>
App.js:
...
// 切換已完成/未完成
const handleToggleIsFinished = id => {
setTodos(todos.map(todo => {
if (todo.id !== id) return todo;
return {
...todo,
isFinished: !todo.isFinished
}
}))
}
return (
<div className="App">
<input type="text" placeholder="add todo" value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsFinished={handleToggleIsFinished} />)
}
</div>
);
}
這樣,在 TodoItem.js 的 <TodoItem>
就可以接受到 handleToggleIsFinished
在 <Button>
就可以加上 onClick
的事件了
這裡把 onClick
裡面的 function 抽出來寫(handleToggleClick
),這樣的可讀性會比直接寫在 onClick
裡面好(直接寫在裡面叫做 inline function)
export default function TodoItem({ className, size, todo, handleDeleteTodo, handleToggleIsFinished }){
const handleToggleClick = () => {
handleToggleIsFinished(todo.id)
}
const handleDeleteClick = () => {
handleDeleteTodo(todo.id)
}
return (
<TodoItemWrapper className={className} data-todo-id={todo.id}>
<TodoContent $isFinished={todo.isFinished} size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button onClick={handleToggleClick}>
{todo.isFinished ? '未完成' : '已完成'}
</Button>
<RedButton onClick={handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
}
todo list 重點整理
在 setTodos()
小括號裡面,要產生一個新的陣列
- 在
setTodos()
用...todos
的解構語法來新增todo
- 在
setTodos()
用.filter()
來刪除指定的todo
- 在
setTodos()
用.map()
修改指定的todo
的屬性
從 todo list 學到的重點觀念有:
- Component
- Props
- Style
- Event handler
- JSX
- State