# useState
###### tags: `六角筆記王`
[TOC]
useState:用來設定 react 中的 state
語法如下:
```jsx=
const [currentValue, setCurrentValue] = useState(initialValue);
```
- currentValue:存放 state 的值
- setCurrentValue:用來設定 state 值
- initialValue:state 的初始值
## 先以計數器來說
以下面的範例來說,就是使用到`setCounter`來改變`counter`的值。
我們無法直接改變`counter`的值,只能藉由`setCounter`。
```jsx=
const [counter,setCounter] = useState(0)
console.log('render',counter)
const handleButtonClick = () =>{
console.log('button click!');
setCounter(counter+1);
}
return (
<div className="App">
<button onClick={handleButtonClick}>increment</button>
{counter}
</div>
)
```
在下圖中可以注意到,再還沒按下`button`之前,`react`會先`render`一次。 ( redner 0 )
每當點擊完`button`之後,`react`就`render`一次。

### 結論
由此可知,React是以「資料」驅動「畫面」進行更新。不需要再透過原生`JS`的`textContent`,`innerHTML`這樣的方式來更新HTML的畫面。
而現在畫面和元件的資料是關聯再一起的。因此,一旦資料改變了,React就會自動幫我們更新呈現的畫面。
### JSX 需注意的點
在jsx裡面是無法使用迴圈的,因為沒有迴圈的概念。
不能在條件式(conditions),迴圈(loops) 或 嵌套函式(nested functions)中呼叫 Hook方法。
以useState來說,正確的寫法為
```jsx=
const Conuter = () =>{
const [count,setCount] = useState();
return{
{/* ... */}
}
}
```
如果把useState放到if內可能會導致嚴重錯誤。
```jsx=
const Counter = () =>{
if(isValidCounter<=10){
const [count,setCount] = useState();
}
return{
{/* ... */}
};
}
```
那為什麼會產嚴重錯誤呢?
原因是因為React Hooks中會去==記錄這些Hooks在函式中被呼叫的順序==,以確保資料能夠被相互對應。
但是,當我們將Hooks放到條件式或是迴圈內,就會==破壞==了這些Hooks被呼叫到的順序,因此造成錯誤。
## 新增 todo 選項
所以這邊只能使用到`map`方法。
`map`與 forEach 非常類似,但是 `map` 會 return 一個值,並且產生一個**新陣列**出來。
需特別注意的是,下方第10行中,可能會想要寫`todos.push(777)`,但上面提過,todos會被視為相同的state,不能直接改state,它不會進行更新。
```jsx=
// 解構語法
import { useState } from 'react';
function App() {
// 給他一個初始值
const [todos, setTodos] = useState([1]);
const handleButtonClick = () => {
// 傳入參數: 新的 todo + 解構 todos
setTodos(["new todo", ...todos]);
}
return (
<div className="App">
/* 在 JSX 中,單標籤必須 /> 結尾 */
<input type="text" placeholder="Add todo..." />
<button onClick={handleButtonClick}>Add Todo</button>
{
/* 這邊加上key是因為React的規範 */
todos.map((todo, index) => <TodoItem key={index} content={todo} /> )
}
</div>
);
}
```

## Controlled vs Uncontrolled
因為這邊也可以使用到`useRef`傳值,所以提一下。
在 React 中,表單元素的處理可分為 uncontrolled 和 controlled,兩者之間的差別,在==於 `component` 的資料是否受到 React 的控制==:
### Controlled component
> 資料受到 React 的控制
如果將資料的控制權交給 React 來處理,畫面就會根據 state 是否改變來重新渲染。
### Uncontrolled component
> 資料不受 React 的控制
例如:input、textarea 等表單元素,通常會維持本身的 state,並根據使用者的輸入來更新該元素的 state。
若想取得 `uncontrolled` component 的值,可透過直接操作 DOM 或使用 useRef 來選取特定元素。
### 簡單來說
input 值有無放到state裡面,如果有的話,就是`controll`,沒有的話就是`uncontrolled`。
## 新增 todo 選項裡的 value
```jsx=
const [todos,setTodos] = useState([]);
const [value,setVaule] = useState('');
const InputValueHandler = e => {
setVaule(e.target.value);
};
const ButtonClickHandler = e =>{
setTodos([value, ...todos]);
setVaule('')
}
return(
<div className="App">
<input type="text" placeholder="todo" value={value} onChange={InputValueHandler}/>
<button onClick={ButtonClickHandler}>Add Todo</button>
{
todos.map((todo,index) => <TodoItem content={todo} key={index} value={value} onChange={InputValueHandler}/>)
}
</div>
)
```
可以觀察到value就寫進去了。

## 新增 todo 選項裡 自帶 id
```jsx=
import './App.css';
import ToDoItem from './components/ToDoItem';
import { useState, useEffect, useRef } from 'react';
function App() {
const [todos, setTodos] = useState([]);
const [value, setVaule] = useState('');
// 使用useRef將id傳進去
const id = useRef(2);
const InputValueHandler = e => {
setVaule(e.target.value);
};
const ButtonClickHandler = () => {
// id 與 content 的部分
setTodos([{
id: id.current,
content: value,
}, ...todos]);
// 輸入框清空
setVaule('');
id.current++;
}
return (
<div className="App">
<input type="text" placeholder="todo" value={value} onChange={InputValueHandler} />
<button onClick={ButtonClickHandler}>Add Todo</button>
{
// 將整個 todo 傳入 ToDoItem component
todos.map((todo) => <ToDoItem todo={todo} onChange={InputValueHandler} />)
}
</div>
)
}
export default App;
```
### ToDoItem 元件
```jsx=
import '../App.css';
import styled from 'styled-components';
const TodoItemWrapper = styled.div`
display:flex;
align-items:center;
justify-content: space-between;
padding: 8px 16px;
border: 1px solid black;
width:600px;
margin: 0 auto;
`;
const TodoContent = styled.div`
color:green;
font-size: ${props => props.size === "S" ? '20px' : '12px'};
`;
const TodoButtonWrapper = styled.div`
`;
const Button = styled.button`
padding:4px;
color:black;
&:hover{
color:red;
}
&+& {
margin-left:4px;
}
`;
const RedButton = styled(Button)`
color:red;
`;
// 父元件傳過來的 props.todo
export default function ToDoItem({ todo }) {
return (
// 將id帶入
<TodoItemWrapper data-todo-id={todo.id}>
<TodoContent>
{todo.content}
</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
<RedButton>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
};
```
## 刪除功能
刪除功能想起來很複雜,但實作相對於其他功能是容易的。
為以下步驟,
1. 將 function 寫在 Parent,並傳入參數(id)
2. 再由 Children 呼叫 Parent
3. 在 Parent 處理 function
### 1 將 function 寫在 Parent,並傳入參數(id)
```jsx=
const handleDeleteTodo = id => {
}
return (
<div className="App">
<input type="text" placeholder="todo" value={value} onChange={InputValueHandler} />
<button onClick={ButtonClickHandler}>Add Todo</button>
{
todos.map((todo) => <ToDoItem todo={todo} onChange={InputValueHandler} handleDeleteTodo={handleDeleteTodo} />)
}
</div>
)
```
### 2 再由 Children 呼叫 Parent
```jsx=
// 傳入props
export default function ToDoItem({ todo, handleDeleteTodo }) {
return (
<TodoItemWrapper data-todo-id={todo.id}>
<TodoContent>
{todo.content}
</TodoContent>
<TodoButtonWrapper>
<Button>已完成</Button>
{/* 將handleDeleteTodo函式傳入 */}
<RedButton onClick={() => { handleDeleteTodo(todo.id) }}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
```
### 3 在 Parent 處理 function
```jsx=
// 把有這個id的item利用filter去掉
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
```
## 編輯todo功能
在開始實做之前,可以先將預設值帶入,在物件中加入`isDone`的狀態,來判斷是否已經完成。
```jsx=
function App() {
const [todos, setTodos] = useState([
{ id: 1, content: 'done', isDone: true },
{ id: 2, content: 'not done', isDone: false }
]);
const [value, setVaule] = useState('');
const id = useRef(2);
```
這裡分為兩個步驟,
1. 在 Parent 建立函式,傳入 Children
2. Children 寫好編輯後的 css,當isDone參數變化時,帶入不同css
## 1 在 Parent 建立函式,傳入 Children
```jsx=
// 修改使用map,刪除用filter,新增使用解構語法
const handleToggleDone = id => {
setTodos(todos.map(todo => {
// 回傳原本的todo
if (todo.id !== id) return todo
// 原本的todo + 要修改的屬性
return {
...todo, isDone: !todo.isDone
}
}))
}
```
## 2 當isDone參數變化時,帶入不同css
```jsx=
const TodoContent = styled.div`
color:green;
${props => props.$isDone && `
text-decoration: line-through
`};
`;
```
```jsx=
export default function ToDoItem({ todo, handleDeleteTodo, handleToggleDone }) {
return (
<TodoItemWrapper data-todo-id={todo.id}>
{/* 加上isDone props,在前方加上$,可以使它變成style component */}
<TodoContent $isDone={todo.isDone}>
{todo.content}
</TodoContent>
<TodoButtonWrapper>
{/* inline-function */}
<Button onClick={() => { handleToggleDone(todo.id) }}>
{/* 使用三元運算子進行判斷 */}
{todo.isDone ? '已完成' : "未完成"}
</Button>
<RedButton onClick={() => { handleDeleteTodo(todo.id) }}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
)
```
除了使用三元運算子進行判斷,也可以使用邏輯運算子 `&&`
```jsx=
<Button onClick={handleToggleClick}>
{todo.isDone && '已完成'}
{!todo.isDone && '未完成'}
</Button>
```
### 邏輯運算子的使用:`&&` 和 `||`
> 當 `||` 前面的值為 false 時,就取後面的值。
```jsx=
const a = 0 || 'iPhone'; // 0轉型後為false,所以 a 是iphone
const b = true || "不會輪到我"; // 因為true為真,所以b是true。
```
> 當 `&&` 前面的值為 true 時,就取後面的值。
```jsx=
const a = 0 && 'iPhone'; // 0轉型後為false,所以 a 是 0
const b = true && "會輪到我"; // 因為true為真,所以b是會輪到我。
```
所以計數器的判斷式可以改成
```jsx=
{
count < 10 && (
<div className="chevron chevron-up" onClick={()=>
setCount(count+1)}/>
);
}
```
### Transient props:`$<props>`
在上方程式碼中,加在 `TodoContent` 的 `$isDone` 這個 props,會被視為 style component props,不會被繼續傳到下一個 DOM 元素,也就不會顯示在 `TodoContent` 標籤上。
如果沒有加上 `$` 符號,這個 props 就會被直接加在 `TodoContent` 這個 DOM 結構上。
再以下方程式碼為例:
```jsx=
<TodoContent id="hello" $isDone={todo.isDone}>{todo.content}</TodoContent>
```
可以發現經過 render 之後,在 DOM 元素只會出現 `id="hello"` 這個屬性,而不會有 `$isDone`,這是因為 Transient props 不會被往下傳。

## 結論
React 和以往的思考模式其實很不一樣,像是在切好的 UI 畫面上新增各種功能;而 React 則是先==思考 state 狀態==,再去想會如何改變畫面。
記住一個重點,就是 Component 之間可透過 props 把 state 傳遞下去。並且,只要 state 所有變動,就會觸發 render() 來更新 UI 畫面。