# Note
## 1. React 專案架構:主要使用套件介紹
在開始實作專案前,我們要先了解 React 專案的架構
- App: 專案初始進入點,會期待在這層,能夠看到 頁面的結構
- Views(或稱 Pages): 專案的每個頁面,通常會有自己的內部狀態
- Compoents: 根據專案需求與共用性拆分的元件,會有
- Compoents/common: 單純可被複用的元件,通常內部不會有狀態,是 controlled component
我們以上述的觀念來看 todo list 的設計稿
**todo page**

```jsx=
<div>
<Header />
<AddTodo
inputValue={inputValue}
onChange={handleChange}
onKeyPress={handleKeyPress}
onAddTodo={handleAddTodo}
/>
<TodoCollection
todos={todos}
onDelete={handleDelete}
onSave={handleSave}
onToggleIsDone={handleToggleIsDone}
updateIsEdit={updateIsEdit}
/>
<Footer numOfTodos={numOfTodos} />
</div>
```
**login register page**

```jsx=
<Container>
<div className="mt-5">
<ACLogoIcon />
</div>
<h1 className="h3 mb-3 font-weight-normal">登入 Todo</h1>
<InputContainer>
<Input
label={'帳號'}
title={'username'}
value={userName}
placeholder={'請輸入帳號'}
onChange={(nameInputValue) => setUserName(nameInputValue)}
/>
</InputContainer>
<InputContainer>
<Input
label={'密碼'}
type={'password'}
value={password}
placeholder={'請輸入密碼'}
onChange={(passwordInputValue) => setPassword(passwordInputValue)}
/>
</InputContainer>
<Button onClick={handleSubmit}>登入</Button>
<Link to="/register">
<StyledLinkText>註冊</StyledLinkText>
</Link>
</Container>
```
以 component 的專案架構,如下圖所示

## 2. React router 切分頁面
在 app 層建立好 router 架構
react router dom v6 的寫法,是以 Routes 與 Route 組成,外層以 BrowserRouter 包裝
index.js
```jsx=
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);
```
此處定義三個頁面的路徑,在這三個以外的路徑會統一導到 HomePage,會在後續的章節做好登入驗證的功能後
app.jsx
```jsx=
<div className="app">
<Routes>
<Route path="login" element={<LoginPage />} />
<Route path="todos" element={<TodoPage />} />
<Route path="register" element={<RegisterPage />} />
<Route path="*" element={<HomePage />} />
</Routes>
</div>
```
## 3. 狀態管理 CRUD
用 todo list 來練習 增刪查改
- AddTodo: controlled component, 資料皆以 props 傳入並改動
```jsx=
import clsx from 'clsx';
const AddTodoInput = ({ inputValue, onChange, onKeyPress, onAddTodo }) => (
<div className={clsx('add-todo', { active: inputValue.length > 0 })}>
<label className="add-todo-icon icon" htmlFor="add-todo-input"></label>
<div className="add-todo-input">
<input
id="add-todo-input"
type="text"
placeholder="新增工作"
onChange={(e) => onChange && onChange(e.target.value)}
onKeyPress={(e) => onKeyPress && onKeyPress(e)}
value={inputValue}
/>
</div>
<div className="add-todo-action">
<button
className="btn-reset btn-add"
onClick={() => onAddTodo && onAddTodo()}
>
{' '}
新增{' '}
</button>
</div>
</div>
);
export default AddTodoInput;
```
- TodoItem:
- 兩種模式:read mode 與 edit mode
- todo title 編輯到一半的狀態
- 實作 save, delete, editing, mark done 功能
- TodoCollection:
- TodoItem 的集合
> ! 注意:由 props 傳進來的 function 必須先確保 function 是存在的在做使用,否則會出現 runtime error
以 collection 為例,可擅用 logical expression 可達到
```jsx=
const TodoCollection = ({
todos,
onDelete,
onSave,
onToggleIsDone,
updateIsEdit,
}) => (
<div className="todos">
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={(id) => onDelete && onDelete(id)}
onSave={({ id, title, isDone }) =>
onSave && onSave({ id, title, isDone })
}
onToggleIsDone={(id) => {
onToggleIsDone && onToggleIsDone(id);
}}
updateIsEdit={({ id, isEdit }) =>
updateIsEdit && updateIsEdit({ id, isEdit })
}
/>
))}
```
## 4. 封裝 api request 細節
這次請同學皆以這組帳號 密碼實驗登入與註冊功能
登入成功會回傳一組 JWT token
並成功進入 todo 頁面
**Admin (JWT expiry: 1800s)**
- Username: iamuser1
- Email: admin@alpha.co
- Password: pass1234
register
```jsx=
export const register = async ({ username, email, password }) => {
const res = await fetch(`${authURL}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
email,
password,
}),
});
return res.json();
};
```
login
```jsx=
export const login = async (data) => {
console.log('data', data);
const res = await fetch(`${authURL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: data.username,
password: data.password,
}),
});
return res.json();
};
```
check permission
```jsx=
export const checkTokenExpired = async (authToken) => {
try {
const res = await fetch(`${authURL}/test-token`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + authToken,
},
});
return res.json();
} catch (error) {
return error;
}
};
```
logout
```jsx=
export const logout = () => {
localStorage.removeItem('authToken');
};
```
## 5. Context 登入 / 註冊實作
說明
在先前 todo crud 的實作中,我們可以發現使用 useState 管理元件內部的狀態時,為了讓子元件可以讀取到父層的狀態值,會透過 props 傳遞。
當我們的專案結構越來越具規模時,這樣的傳遞方式會讓元件的 props 控管越顯複雜,這個時候,useContext 可以有效解決這樣的問題,可以透過 context provider 提供 global state 讓子元件
以 todo list 這個專案而言,我們會將登入狀態放在 context, 因為我們會需要判斷使用者是否是登入狀態來決定渲染的頁面
1. createConext:定義集中管理的狀態(預設值)
```javascript=
const defaultAuthContext = {
isAuthenticating: false,
isAuthenticated: false,
authToken: null,
currentMember: null,
register: null,
login: null,
logout: null,
checkAuth: null,
};
export const AuthContext = createContext(defaultAuthContext);
```
2. 使用 provider 管理登入後的狀態,並封裝登入與註冊的方法到 context 中作取用
```javascript=
export const AuthProvider = ({ children }) => {
const [loading, setLoading] = useState(defaultAuthContext.isAuthenticating);
const [authToken, setAuthToken] = useState(null);
const [payload, setPayload] = useState(null);
useEffect(() => {
if (!authToken) {
setPayload(null);
return;
}
try {
const tmpPayload = jwt.decode(authToken);
console.log(tmpPayload);
if (!tmpPayload) {
return;
}
setPayload(tmpPayload);
} catch (error) {
process.env.NODE_ENV === 'development' && console.error(error);
}
}, [authToken]);
return (
<AuthContext.Provider
value={{
isAuthenticating: loading,
isAuthenticated: Boolean(authToken),
authToken,
currentMember: payload && {
id: payload.sub,
name: payload.name,
},
register: async (data) => {
setLoading(true);
register({
email: data.email,
username: data.username,
password: data.password,
})
.then(({ authToken }) => {
setAuthToken(authToken);
})
.catch((error) => {
setAuthToken(null);
console.error(error);
})
.finally(() => {
setLoading(false);
});
},
login: async (data) => {
console.log('context data', data);
setLoading(true);
login({
username: data.username,
password: data.password,
})
.then((res) => {
console.log('login', res);
setAuthToken(res.authToken);
})
.catch((error) => {
setAuthToken(null);
console.error(error);
})
.finally(() => {
setLoading(false);
});
},
logout: async () => {
localStorage.clear();
setAuthToken(null);
},
checkAuth: async () => {
checkTokenExpired(authToken)
.then((res) => {
console.log('checkTokenExpired', res);
})
.catch((error) => console.error(error));
},
}}
>
{children}
</AuthContext.Provider>
);
};
```
3. useContext:訂閱並取用 context 中的狀態,是取得資料的人(Consumer)
```javascript=
// AuthContext
export const useAuth = () => useContext(AuthContext);
// LoginPage
const LoginPage = () => {
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
const { login, isAuthenticated } = useAuth();
...
}
```
4. 透過 context 掛載在 各個頁面,以 `isAuthnticated` 切換頁面狀態
HomePage
```javascript=
const HomePage = () => {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
console.log('home', isAuthenticated);
return <Navigate to="/login" replace />;
}
return <Navigate to="/todos" replace />;
};
```
LoginPage
```javascript=
const LoginPage = () => {
const { login, isAuthenticated } = useAuth();
if (isAuthenticated) {
return <Navigate to="/todos" replace />;
}
return <>...</>
}
```
RegisterPage
```javascript=
const RegisterPage = () => {
const [email, setEmail] = useState('');
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
const { register, isAuthenticated } = useAuth();
if (isAuthenticated) {
return <Navigate to="/todos" replace />;
}
return <>...</>
}
```
TodoPage
```javascript=
const TodoPage = () => {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <> ...</>
}
```