# Note ## 1. React 專案架構:主要使用套件介紹 在開始實作專案前,我們要先了解 React 專案的架構 - App: 專案初始進入點,會期待在這層,能夠看到 頁面的結構 - Views(或稱 Pages): 專案的每個頁面,通常會有自己的內部狀態 - Compoents: 根據專案需求與共用性拆分的元件,會有 - Compoents/common: 單純可被複用的元件,通常內部不會有狀態,是 controlled component 我們以上述的觀念來看 todo list 的設計稿 **todo page** ![](https://i.imgur.com/2ZziLEF.png) ```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** ![](https://i.imgur.com/l25DQjr.png) ```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 的專案架構,如下圖所示 ![](https://i.imgur.com/zXZ8a3B.png) ## 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 <> ...</> } ```