# [Udemy - React] React - The Complete Guide (incl Hooks, React Router, Redux)
###### tags: `Udemy 課程筆記` `React` `前端筆記`
## Sec. 4
1. Components are only functions.
2. Parent -> children 必須使用 `props` 傳遞資料
- `props` 就是 component 的 parameter,如果想要更快的取出值,可以使用物件解構
- parent 傳給 children 的樣子就是 `keyName={value}`,所以 children 直接用 `props.keyName` 或者解構 `{keyName}` 即可
```javascript=
// Parent.js
const Parent = () => {
const name = 'Lun';
// ..
return (
// ...
<Children userName={name}/>
)
}
```
```javascript=
// Children.js
const Children = (props) => {
// ..
return (
// ...
<p>{props.userName}</p>
)
}
export default Children;
```
3. state 也可以是物件型別也可以是基本型別,要注意的是如果更新 state 的方式是以「上一個 state」為基準更新的話,再更新 state 時強烈建議丟 fucntion 給 setState,這樣子 React 可以確保更新 state 時都可以正確地拿到上次的 state
> 這個之後要好好找文件整理一下,關鍵字:React useState hook is asynchronous
In general it's a good practice to use the functional approach if the new state depends on the previous state. With this approach it's guaranteed by React that you always get the latest state automatically.
Otherwise you might get undesired results since setState is executed asynchronously.
==That means the state update is not executed before the call stack is empty and before there is no task further ahead in the event queue (these are JS internals, not specific to React).==
You can play around with my live example here which uses a setTimeout (just for demo purposes) to provoke the possible problem: https://stackblitz.com/edit/react-set-state-function-version
> 呼叫 state update (set state func) 時會先把 function 推入 event queue 排隊,event loop 會負責檢查 calling stack 是否為空,如果空了才會推 set state func 進去 calling stack,更新 state 後觸發 re-render。所以這時候如果直接用 non-function 的方式執行以上次 state 為基準更新的動作時,會讓 set state 有機率拿到同一次的基準 state,造成 state 不是開發者預期的方式更新。
### 那為什麼 functional set state 就可以確保正確的 state update?
因為 setState 也可以丟 function 為 argument 呼叫 setState func,而且可以確保該 function 擁有取得上次 state 的手段(透過 function paramete 取得),所以可以確保在這個 functino 內正確地更新 state。
4. Two-way binding
以下只有 one way binding,只是透過 state + event 綁定 state (input),而 input tag 本身並沒有和 state 完全的連結,因為點擊 submit 時雖然會觸發 `setInput('')` 但是頁面的 input 卻不會消失。
```javascript=
const Form = () => {
const [input, setInput] = useState('');
const inputHandler = (event) => {
const { value } = event;
setInput(value);
}
const submitHandler = (event) => {
event.preventDefault();
setInput('');
}
return (
<form onSubmit={submitHandler}>
<input onInput={inputHandler}/>
<button type="submit">Submit</button>
</form>
)
}
```
但在 input tag 內綁定 `value` attribute 就可以達到 two-way binding,讓 input 顯示的 value 可以跟 state 完全吻合:
```javascript=
const Form = () => {
const [input, setInput] = useState('');
const inputHandler = (event) => {
const { value } = event;
setInput(value);
}
const submitHandler = (event) => {
event.preventDefault();
setInput('');
}
return (
<form onSubmit={submitHandler}>
<input onInput={inputHandler} value={input}/>
<button type="submit">Submit</button>
</form>
)
}
```
5. children -> parent (向上傳遞)
paretn -> children 需要透過 `props` 傳遞,那麼由下至上呢?也是透過 `props`,但是是傳 function 給 children,再由 children 叫用該函式。
```javascript=
// Child component
// 透過 props 取得 parent 傳下來的 function,並在 child component 依靠事件觸發該 function
function BookTitle(props) {
return (
<label>
Title:
<input onChange={props.onTitleChange} value={props.title} />
</label>
)
}
```
```javascript=
// Parent component
// 從 Parent 傳 function 給 child
import React, { useState } from 'react'
function BookEditForm(props) {
const [title, setTitle] = useState(props.book.title)
function handleTitleChange(evt) {
// 會往外找取得 setTitle
setTitle(evt.target.value)
}
return (
<form>
<BookTitle onTitleChange={handleTitleChange} title={title} />
</form>
)
}
```
### 為什麼這樣子可行?
1. 只是傳 function,叫用的時機點在 child component 中的 event(所以傳 `props` 時不需要叫用 `propsName={func()}`)
2. 當函式、變數宣告時就決定了其可見(可取用範圍)了,所以不是呼叫函式時決定函式可以拿什麼東西,而是在宣告的時候便決定函式可以拿什麼
```javascript=
const name = 'Lun';
const funcOne = () => {
console.log('funcOne!');
console.log(name);
}
const funcTwo = (name) => {
console.log(name);
funcOne(name);
}
funcTwo('Liu');
// 'Liu'
// 'funcOne!'
// 'Lun'
```
## Sec. 5
整體專案結構

1. Component 就是 function,所以也可以在 function 內整理 JSX 變數或者直接在 template 用三元運算子控制 component 的顯示
2. 可以用 wrapper component 整理資料,避免一個 component 整理過多的資料,造成該 component 行數超多
3. render list 時記得要給 React `key`(必須是獨一無二),`key` 會幫助 React 更有效率地處理 list DOM
## Sec. 6
在 React 專案內如果沒有使用 styled component 或者 CSS module 將樣式區隔的話,所有樣式都是 global(意思是所有 component 都會吃到),為了解決這個問題,便出現 styled component / CSS module。
### styled component
在 component 內引入 styled component,並切記需要把 styled component 放在 function component 之外(不要放在 function component 內宣告),因為這樣子會造成 component re-render 時重複建立 styled component,這麼做會造成 render 的速度下降。
正確:
```javascript=
const StyledWrapper = styled.div`
/* ... */
`;
const Wrapper = ({ message }) => {
return <StyledWrapper>{message}</StyledWrapper>;
};
// ref.: https://styled-components.com/docs/basics#passed-props
```
錯誤:
```javascript=
const Wrapper = ({ message }) => {
// WARNING: THIS IS VERY VERY BAD AND SLOW, DO NOT DO THIS!!!
const StyledWrapper = styled.div`
/* ... */
`;
return <StyledWrapper>{message}</StyledWrapper>;
};
// ref.: https://styled-components.com/docs/basics#passed-props
```
### Props 的傳遞:
- styled component 會自動接受該 component 上有的 props,透過函式的方式 styled component 可以取得傳入 component 的 props
```javascript=
const Button = styled.button`
/* Adapt the colors based on primary prop -> 用 arrow function 取得傳入的 props,即可依照需求動態地顯示樣式 */
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
render(
<div>
<Button>Normal</Button>
/** 傳入 props -> primary */
<Button primary>Primary</Button>
</div>
);
```
- 原本需要用 `props.children` 才可以在 component 插入其他 component / template,使用 style component 就可以自動插入了
```javascript=
// UI/Button.js
const Button = props => {
console.log(props);
return (
<button type={props.type} className="button" onClick={props.onClick} disabled={props.isDisabled}>
{props.children}
</button>
);
};
// someComponent
// ..
<Button type="submit">
Add Goal
</Button>
```
```javascript=
// UI/Button.js
const Button = styled.button`
//... using styled component
`;
// someComponent
//
// ..
<Button type="submit">
// 還是可以插入 template
Add Goal
</Button>
```
### CSS module
- 比較乾淨的作法,因為 style 還是存在於 style sheet 中,不會和 component 擠在同一個 file 內
- 使用 `npx create-react-app` 建立專案的話就可以直接使用了
- style sheet 的檔名需要修改成 componentName.module.css/scss
- 有別於直接 `import './style.css'`,需要給 style sheet 一個變數 `import styles from './componentName.module.css`(或者是用 `import classes from './componentName.module.css'`
- 此時就可以直接用物件取值的方式使用 style class
- 編譯完顯示在 DOM 上會是 `ComponentFileName_styleClassName__hash`
> CSS Modules allows the scoping of CSS by automatically creating a unique classname of the format [filename]\_[classname]\_\_[hash]
```javascript=
// componentName.module.css
.title {
color: Red;
}
.my-class {
color: Green;
}
// Component.js
import styles from './componentName.module.css'
// ...
{
return (
<>
// {} -> expression 所以可以用 template literal
<MyComponent className={`${styles.title}}`}/>
// dot notation 不可以用特別符號,有特別符號的需求就需要用 bracket notation
<MyComponent className={`${styles['my-class']}`}/>
</>
)
}
```
## Sec. 8
如果 children component 並沒有需要額外處理從 parent 接收的 functional props,而是單純要把這個 function props 繼續往下或者單純在 children component 叫用的話,其實就只要在 event 寫入 functional props,之後事件觸發後就會如期叫用該 funtional props了。
```javascript=
// UI/Button
const Button = props => {
// 只是單純地叫用 parent 拿到的 function 就不用再寫 function 包起來了
const clickHandler = () => {
props.onClick();
}
return (
// onClick -> 會叫用 props.onClick,就不用在 Button component 再寫 function,畢竟我們沒有要額外處理事情,只希望 Button 點擊時可以呼叫函式而已
<button type={props.type ?? 'button'} className={`${styles.btn} ${styles[props.classes]}`} onClick={props.onClick}>
{props.children}
</button>
// 就不用這樣子寫了
<button type={props.type ?? 'button'} className={`${styles.btn} ${styles[props.classes]}`} onClick={clickHandler}>
{props.children}
</button>
);
};
```
```javascript=
const Modal = props => {
// ..
return (
// 從 parent 拿到了 onConfirm function,透過 onClick(props! not event) 傳給 Button,讓 Button onClick(event) 可以叫用此函式
// 這種時候也可以直接用 eventName 當作 propName,即可清楚表達我只是單純傳這個 function chilren component 叫用的
<Button type="button" onClick={props.onConfirm} classes="btn-secondary">
Okay
</Button>
);
};
```
## Sec 10
### `useEffect`
> `useEffect` 告訴 React:「component render 完就做某事。」
#### `useEffect` 處理的是 side effect
副作用(side effect):React 將 render 後和 React 本身無關但又需要執行且改變 UI 的行為稱之 side effect。比如說「打 API」及「手動更改 DOM」等等。
#### 如何告訴 React 做某事?
有別於 `useState` 是傳 state 的起始值, `useEffect` 是傳入函式(畢竟是要告訴 React 做某事)以及一個陣列(稱作 dependency,用來額外告知 React 除了 render 外,還必須符合**條件**才可以做某事)。
```javascript=
// ref.: https://reactjs.org/docs/hooks-effect.html#gatsby-focus-wrapper
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
```
#### 觸發的條件及順序
> **Does useEffect run after every render?**
> Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
> `useEffect` 預設是在每一次 component 觸發 render 後就會叫用。

```javascript=
import React, { useEffect, useState } from 'react';
import Card from '../UI/Card/Card';
import classes from './Login.module.css';
import Button from '../UI/Button/Button';
const Login = props => {
const [enteredEmail, setEnteredEmail] = useState('');
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState('');
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
useEffect(() => {
console.log('in useEffect');
});
const emailChangeHandler = event => {
setEnteredEmail(event.target.value);
setFormIsValid(event.target.value.includes('@') && enteredPassword.trim().length > 6);
};
const passwordChangeHandler = event => {
setEnteredPassword(event.target.value);
setFormIsValid(event.target.value.trim().length > 6 && enteredEmail.includes('@'));
};
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes('@'));
};
const validatePasswordHandler = () => {
setPasswordIsValid(enteredPassword.trim().length > 6);
};
const submitHandler = event => {
event.preventDefault();
props.onLogin(enteredEmail, enteredPassword);
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div className={`${classes.control} ${emailIsValid === false ? classes.invalid : ''}`}>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={enteredEmail}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
<div className={`${classes.control} ${passwordIsValid === false ? classes.invalid : ''}`}>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={enteredPassword}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
/>
</div>
<div className={classes.actions}>
<Button type="submit" className={classes.btn} disabled={!formIsValid}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;
```
因為 input 都有綁定 event + setStateFunction,所以 input event 會觸發 setStateFunc,造成 component re-render,進而也造成 `useEffect` 內的函式被呼叫:


(輸入了五個數字,`useEffect` 也被叫用了五次)
#### `dependency` 很重要
> `useEffect` 收的是==怎麼處理任務(函式)== 及 ==處理任務的額外條件。==
「處理任務的額外條件」只的就是 `useEffect(callback, [dependency])`,中的 `[dependency]`。`[dependency]` 為陣列,React 會藉由 `Object.is` 判斷 `[dependency]` 是否有改變,有改變才會再次叫用 `useEffect` 內的任務。
> 1. `Object.is` 基本上使用嚴格比較(`===`)查看 `[dependency]` 的值是否有改變。
> 2. `useEffect()` 給 `[dependency]` 後,只有第一次 component render 一定會叫用 `useEffect` 內的函式,接下來 component re-render 時,`useEffect` 內的 `[dependency]` 有變動才會再次呼叫 `useEffect` 內的函式。
#### `dependency` 給空陣列 `[]`,只有第一次 component render 才會執行 `useEffect` 內的任務
只有 component 第一次 render 時會觸發 `useEffect`:
```javascript=
// ..
useEffect(() => {
console.log('in useEffect');
if (sessionStorage.getItem('isLoggedIn')) {
setIsLoggedIn(true);
}
// 因為第二次 render 後,React 就會檢查 dependency 是否有改變,但已經給 [],就消滅 dependency 會更動的可能性
}, []);
// ..
```
#### 只要 `[dependency]` 其中的一個 element 有改動,就會觸發 `useEffect` 內的任務
不是全部的 elements 有更動才會觸發 `useEffect` 的任務,只要有一個 element 有更動就會觸發:

(兩個 inputs 共輸入 10 個字元)

(因為兩個 inputs 都有綁定 state + event,以及 `useEffect` 的 `[dependency]` 為兩個 inputs 的 state,所以輸入 10 個字元就會觸發 10 次 `useEffect` 的任務)
```javascript=
// ..
const Login = props => {
const [enteredEmail, setEnteredEmail] = useState('');
const [enteredPassword, setEnteredPassword] = useState('');
// ..
useEffect(() => {
console.log('in useEffect');
// [dependency] 只要其中一個 element(state) 有更動便會觸發 useEffect 的任務
}, [enteredEmail, enteredPassword]);
const emailChangeHandler = event => {
// 事件綁定後更新 state
setEnteredEmail(event.target.value);
// ..
};
const passwordChangeHandler = event => {
// 事件綁定後更新 state
setEnteredPassword(event.target.value);
// ..
};
// ..
return (
// ..
<div className={`${classes.control} ${emailIsValid === false ? classes.invalid : ''}`}>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={enteredEmail}
// 綁定事件
onChange={emailChangeHandler}
/>
</div>
<div className={`${classes.control} ${passwordIsValid === false ? classes.invalid : ''}`}>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={enteredPassword}
onChange={passwordChangeHandler}
/>
</div>
// ..
);
};
export default Login;
```
#### 由 `useEffect` 檢查 form 內的欄位是否通過驗證
原本是在每個 input change event 檢查輸入的值,藉此控制 form 是否通過驗證:
```javascript=
import React, { useEffect, useState } from 'react';
import Card from '../UI/Card/Card';
import classes from './Login.module.css';
import Button from '../UI/Button/Button';
const Login = props => {
const [enteredEmail, setEnteredEmail] = useState('');
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState('');
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
const emailChangeHandler = event => {
setEnteredEmail(event.target.value);
// inout onChange event 再觸發 setFormIsValid 的函式,藉此控制表單的驗證狀態
setFormIsValid(event.target.value.includes('@') && enteredPassword.trim().length > 6);
};
const passwordChangeHandler = event => {
setEnteredPassword(event.target.value);
setFormIsValid(event.target.value.trim().length > 6 && enteredEmail.includes('@'));
};
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes('@'));
};
const validatePasswordHandler = () => {
setPasswordIsValid(enteredPassword.trim().length > 6);
};
const submitHandler = event => {
event.preventDefault();
props.onLogin(enteredEmail, enteredPassword);
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div className={`${classes.control} ${emailIsValid === false ? classes.invalid : ''}`}>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={enteredEmail}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
<div className={`${classes.control} ${passwordIsValid === false ? classes.invalid : ''}`}>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={enteredPassword}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
/>
</div>
<div className={classes.actions}>
<Button type="submit" className={classes.btn} disabled={!formIsValid}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;
```
但透過 `useEffect` 提供的叫用任務條件,可以更有效率地處理 form 的驗證狀態:
> `useEffect` 第一次 render 後會叫用,如果有 `dependency`,第二次 re-render 就會比對 `dependency` 有無更動,有才會呼叫 `useEffect` 的任務
```javascript=
// ..
const Login = props => {
const [enteredEmail, setEnteredEmail] = useState('');
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState('');
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
useEffect(() => {
console.log('in useEffect');
setFormIsValid(enteredEmail.includes('@') && enteredPassword.trim().length > 6);
// useEffect 綁定 dependency,由 useEffect 統一檢查 form 的驗證狀態
// 第一次 render -> 接下來等到 input onChange 觸發 setFunc 導致 re-render,待 re-render 結束後,useEffect 偵測到 dependency 不同(因為有輸入 input)後會自動執行檢查 form 的驗證狀態之任務
}, [enteredEmail, enteredPassword]);
const emailChangeHandler = event => {
setEnteredEmail(event.target.value);
// 不再每次 onChange 時檢查 form 的驗證
// setFormIsValid(event.target.value.includes('@') && enteredPassword.trim().length > 6);
};
const passwordChangeHandler = event => {
setEnteredPassword(event.target.value);
// setFormIsValid(event.target.value.trim().length > 6 && enteredEmail.includes('@'));
};
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes('@'));
};
const validatePasswordHandler = () => {
setPasswordIsValid(enteredPassword.trim().length > 6);
};
const submitHandler = event => {
event.preventDefault();
props.onLogin(enteredEmail, enteredPassword);
};
// .. 略(與上面的例子相同)
};
export default Login;
```
#### `cleanup function`
> 可以參考的資料:
>1. [[week 21] React Hooks API:useEffect & 實作一個自己的鉤子](https://hackmd.io/@Heidi-Liu/note-fe302-hooks-useeffect)
>2. [Understanding React’s useEffect cleanup function](https://blog.logrocket.com/understanding-react-useeffect-cleanup-function/)
>3. [lecture. 114](https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/25599220#notes)
`useEffect` 其實也可以回傳一個函式,透過這個函式開發者可以處理上一次 `useEffect` 呼叫任務所產生的 effect:
>1. 回傳的函式稱作 cleanup function
>2. 只能回傳函式
>3. 回傳函式後,`useEffect` 的叫用順序會稍微不同:第一次 render 後叫用 `useEffect`,第二次 re-render 如有 `[dependecy]` 更動,則會觸發 `useEffect`,但是在觸發 `useEffect` 以前會先觸發 cleanup function
>4. 當 component unmount 時也會觸發 `useEffect` 的 cleanup function
==第一次叫用 `useEffect` 不會呼叫 cleanup function,但再第二次開始就是 cleanup -> `useEffect` 的任務。==
藉由 `useEffect` 的 cleanup,使用 React 可以很輕鬆地實作 input debounce:
```javascript=
// ..
const Login = props => {
const [enteredEmail, setEnteredEmail] = useState('');
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState('');
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
// 重點是節省這一段所需要消耗的資源
setFormIsValid(enteredEmail.includes('@') && enteredPassword.trim().length > 6);
}, 500);
// clean up function
// -> 第一次 useEffect 不會觸發,但是之後就會觸發,且會優先於 useEffect 塞入的 callback:call clean up than useEffect callback
return () => {
// 上一次 useEffect 後產生的變化(effect)
clearTimeout(timer);
};
}, [enteredPassword, enteredEmail]);
// ..
```
第二次觸發 `useEffect` 都會刪除前一次 `useEffect` 所產生的 `timer`(因為閉包的關係,所以可以取得 `timer`),等到使用者輸入完畢半秒內都沒再次輸入,就會執行 `useEffect` 產生的 `timer`。
### `useReducer`
> 1. 處理較複雜的 state 變化時可以使用,且更新 state 的行為被拉至 `reducerFunc` 處理(由 `dispatchFunc` 驅動 `reducerFunc`)
> 2. 變更 state 的話也是會造成 re-render
> 3. 如果有許多「類似的」state,就很適合用 `useReducer` 整理成一包(比如說課程範例:`inputValue` 及該 `inputValueValidity` 被 `userReducer` 整理成一包
> 4. React 確保 `reducerFunc` 可以有最新 state 的存取權

(`useReducer` 的 syntax)
### `reducerFunc` 的 parameters
#### `dispatch` 驅動 `reducerFunc` 更動資料

(與 `useState` 的使用時機區分)
> 可以參考的資料
> 1. [lecture 117](https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/25599228#notes)
> 2. [Examples of the useReducer Hook](https://daveceddia.com/usereducer-hook-examples/)
> 3. [useState vs useReducer](https://tkdodo.eu/blog/use-state-vs-use-reducer)
> 4. [React Hooks | 既生 useState 何生 useReducer ?](https://medium.com/%E6%89%8B%E5%AF%AB%E7%AD%86%E8%A8%98/react-hooks-usestate-vs-usereducer-b14966ad37dd)
:::success
1. 開發者建立變更 state 的手段(`reducerFunc`)
- `reducerFunc` 有 React 給的兩個 parameters(`state`, `action`)
- `state`: React 保證 `reducerFunc` 可以拿到當前正確的 `state`
- `action`: `action` 為執行 `reducerFunc` 時可以接收外部資訊的洞口
- 既然有接外部資料的洞口,就可以依照洞口執行更靈活地運算
2. 透過 `dispatch` 叫用 `reducerFunc`
- 可以想像成這樣
```javascript=
const dispatchFunc = (action) => {
reducerFunc(action)
}
```
3. 必須回傳 `state`,該 `state` 會觸發 re-render
:::
### `useContext`
> 1. 打破 `props` 只有上下層傳遞,建立一個 app-wide 的垂直傳遞鏈(在這條鏈中不論 component 多深都可以拿到 `context` 保存的資料)
> 2 .但當 `context` 更新時,有拿 `context` 的 component 會 re-render,因此建議 `useContext` 保存的是「不常變動的 `context`」(比如說全站的 theme 或者登入狀態等等)
> 3. `context` 為拉出 `props` 傳遞,成為一個 app-wide 的 state(但還是得遵從「垂直」的規則)
> 4. 心法:不管有沒有要把 state 拉出成獨立的一個 `context`,使用時都必須放在外層(等同於宣告其 `context` 方便的範圍為何),讓其內層的元件都可以使用

(`props` 遇到非上下傳遞時必須網上找到共同上層,再慢慢傳到想要的 children 內)
#### `store/` -> 放 `context` 的 file
基本上會在根目錄(也就是 src)內建立一個名為 store 的資料夾存放 `useContext` 保存的 `context`(檔案名稱就 xx-context.js 清楚表達這個是負責什麼 `context` 即可)
#### Step 1: 建立 `context`
`context` 可以為物件型別或者基礎型別,但實戰中常用物件將 `context` 組起來:
```javascript=
// 建立預設值 React.createContext
const AuthContext = React.createContext({
isLoggedIn: false,
onLogout: () => {},
onLogin: (email, password) => {},
});
export default AuthContext;
```
#### Step 2: 使用 `context`
##### 不使用 `useContext`
不使用 `useContext` hook,就要必須透過外層再包 `<ContextName.Consumer>` + `{(context) => (JSX)}`,讓函式內的 JSX 可以藉由 function parameter 取得 `context`:
```javascript=
// src/App.js
import React, { useState, useEffect, useContext } from 'react';
// some imported components ...
// 將 context 引入
import AuthContext from './store/auth-context';
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const storedUserLoggedInInformation = localStorage.getItem('isLoggedIn');
if (storedUserLoggedInInformation === '1') {
setIsLoggedIn(true);
}
}, []);
const loginHandler = (email, password) => {
localStorage.setItem('isLoggedIn', '1');
setIsLoggedIn(true);
};
const logoutHandler = () => {
localStorage.removeItem('isLoggedIn');
setIsLoggedIn(false);
};
return (
/* AuthContext -> 建立一個外層的 component
* - component template name 與 export React.createContext 的變數名稱相同
* - 需要再傳入 value props,並與預設值相同的值(如果給物件就開對應物件的 properties)
* - 這時其 childrem compoents 都可以讀取 value props 內的值
* */
<AuthContext.Provider value={{ isLoggedIn, onLogout: logoutHandler, onLogin: loginHandler }}>
<MainHeader />
<main>
{!isLoggedIn && <Login />}
{isLoggedIn && <Home />}
</main>
</AuthContext.Provider>
);
}
export default App;
// src/components/MainHeader/Navigation.js -> 以其中的 Navigation 為例(跳過 MainHeader,直接拿 Context 的資料)
// some imports ...
const Navigation = () => {
return (
/* 不使用 useContext 就必須外層包 ContextName.Consumer template + 藉由函式取得 Context 回傳 JSX */
<AuthContext.Consumer>
{context => (
<nav className={classes.nav}>
<ul>
{context.isLoggedIn && (
<li>
<a href="/">Users</a>
</li>
)}
{context.isLoggedIn && (
<li>
<a href="/">Admin</a>
</li>
)}
{context.isLoggedIn && (
<li>
<button onClick={context.onLogout}>Logout</button>
</li>
)}
</ul>
</nav>
)}
</AuthContext.Consumer>
);
};
export default Navigation;
```
#### 使用 `useContext`
使用 `useContext` 的好處就是:==將 `context` 存放在一個變數後,functional component 就可以透過 function scope 讀取變數(進而取得該變數存放的 `context`),省去外層再包 `<ContextName.Consumer>` 的麻煩==:
```javascript=
// src/App.js
// useContext 不會改寫外層宣告 context 範圍的寫法,只會簡化需要拿取 context 的寫法 ...
// src/components/MainHeader/Navigation.js -> 以其中的 Navigation 為例(跳過 MainHeader,直接拿 Context 的資料)
// some imports ...
const Navigation = () => {
/* 記得必須回傳 context 本身 */
const authCxt = useContext(AuthContext);
return (
/* 透過 function scope 取得 context */
<nav className={classes.nav}>
<ul>
{authCxt.isLoggedIn && (
<li>
<a href="/">Users</a>
</li>
)}
{authCxt.isLoggedIn && (
<li>
<a href="/">Admin</a>
</li>
)}
{authCxt.isLoggedIn && (
<li>
<button onClick={authCxt.onLogout}>Logout</button>
</li>
)}
</ul>
</nav>
);
};
export default Navigation;
```
#### Bonus: `useContext` 可以直接把 state 拉出成一個獨立的 `context`,達到 `context` 內部自己控管 state
> 可以參考課程:[lecture 125](https://www.udemy.com/course/react-the-complete-guide-incl-redux/learn/lecture/25599262#questions)
1. 將登入的 state 拉出成一個獨立的 `context`(原本放在 `App.js` 搭配 `useContext` 傳給 children components,現在直接拉出成獨立的 `context`)
2. 建立一個外層 component,接下來外層 component 必須與該外層 component 相同名稱
```javascript=
// src/store/auth-context.js
import React, { useState, useEffect } from 'react';
const AuthContext = React.createContext({
isLoggedIn: false,
onLogout: () => {},
onLogin: (email, password) => {},
});
// 直接建立一個外層 component,將 App.js 控管的登入 state 拉出成獨立的 context
/* name export -> 外層的 component 就用這個名字了 */
export const AuthContextProvider = props => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const storedUserLoggedInInformation = localStorage.getItem('isLoggedIn');
if (storedUserLoggedInInformation === '1') {
setIsLoggedIn(true);
}
}, []);
const loginHandler = (email, password) => {
// We should of course check email and password
// But it's just a dummy/ demo anyways
localStorage.setItem('isLoggedIn', '1');
setIsLoggedIn(true);
};
const logoutHandler = () => {
localStorage.removeItem('isLoggedIn');
setIsLoggedIn(false);
};
return (
/* 透過 props.children 讓其 compoennt 可以在 template 起始間插入其他 template */
<AuthContext.Provider value={{ isLoggedIn, onLogin: loginHandler, onLogout: logoutHandler }}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
```
```javascript=
// App.js
// some imports ...
function App() {
// 原本控管的登入 state 都拉出來成獨立的 context 控管了
// 剩下 useContext 取得 context
const authCxt = useContext(AuthContext);
return (
<React.Fragment>
<MainHeader />
<main>
{!authCxt.isLoggedIn && <Login />}
{authCxt.isLoggedIn && <Home />}
</main>
</React.Fragment>
);
}
export default App;
```
```javascript=
// src/index.js
// 一般來說是不會放在這麼上層,但課程的練習專案層數較少,因此需要直接丟到最上層
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { AuthContextProvider } from './store/auth-context';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
/* 直接丟到最上層 */
<AuthContextProvider>
<App />
</AuthContextProvider>
);
```

(因為效能問題,有繁瑣的 props chains 才使用 `useContext` 優化)
### `useImperativeHandle`
## 課程
[React - The Complete Guide (incl Hooks, React Router, Redux)](https://www.udemy.com/course/react-the-complete-guide-incl-redux/)