Context
Context
2021/07/18
2021/11/23 (內容更新)
在react裡,元件間的資料主要是靠props來傳遞,然而,當元件越來越多的時候,這樣的傳遞就會過於複雜,這時候就可以利用context來分享資料。在Routing 裡,我們介紹了ThemeProvider,其實,ThemeProvider就是利用context。當然,也可以使用更複雜的Redux/Recoil來達成這樣的效果,不過,多數的情況下,Context就能解決問題了。
首先,在Authentication 的範例中,我們可以先利用state變數(status)來決定在Main中可顯示內容。
src/ui/Main.js
import React, {useState} from 'react';
import AppMenu from './AppMenu';
import SignUp from '../account/SignUp';
import SignIn from '../account/SignIn';
import SignOut from '../account/SignOut';
export default function Main() {
const [status, setStatus] = useState("signIn");
return (
<div>
<AppMenu/>
{status==="signUp"?
<SignUp setStatus={setStatus}/>
:status==="signIn"?
<SignIn setStatus={setStatus}/>
:
<SignOut setStatus={setStatus}/>
}
</div>
)
}
那如果要讓其他的元件,如:PersonList,收到status,因為不是Main的子元件,無法使用props傳遞變數。在react裡提供了Context來解決這樣的問題,利用Context可以創造類似全域變數的效果。首先,先利用createContext,產生一個context物件。
createContext
/src/account/AuthContext.js
import React from 'react';
export const STATUS = {
toSignIn: 1,
toSignOut: 2,
toSignUp: 0,
};
export const AuthContext = React.createContext({
status: STATUS.toSignIn
})
/*
status及setStatus在provider會被覆蓋
status為toSignIn 已註冊,將要登入
status為toSignOut 已登入,將要登出
status為toSignUp 未註冊,將要註冊
*/
context.Provider
先將原本在index.js裡Router的內容移到AppRouter.js
** 注意 ** 內容已改為react router v6
/src/AppRouter.js
import React from 'react';
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom';
import ProductList from './product/ProductList';
import EmployeeList from './employee/EmployeeList';
import Main from './ui/Main';
export default function AppRouter(){
return (
<Router>
<Routes>
<Route path="/" element={<Main/>}/>
<Route path="/product" element={<ProductList/>}/>
<Route path="/employee" element={<EmployeeList/>}/>
</Routes>
</Router>
);
}
/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import reportWebVitals from './reportWebVitals';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import AppRouter from './AppRouter';
// theme
const theme = createTheme({
palette: {
primary: {
main: '#f44336',
},
secondary: {
main: '#F5B7B1',
},
},
});
ReactDOM.render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<AppRouter/>
</ThemeProvider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
在/src/AppRouter.js裡先import AuthContext
import {AuthContext, STATUS} from './src/account/AuthContext';
接下來,將需要使用AuthContext的元件利用AuthContext.Provider包起來,並預設status為toSignIn:
import React from 'react';
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom';
import ProductList from './product/ProductList';
import EmployeeList from './employee/EmployeeList';
import Main from './ui/Main';
import {AuthContext, STATUS} from './account/AuthContext';
export default function AppRouter(){
return(
<AuthContext.Provider value={{status:STATUS.toSignIn}}>
<Router>
<Routes>
<Route path="/" element={<Main/>}/>
<Route path="/product" element={<ProductList/>}/>
<Route path="/employee" element={<EmployeeList/>}/>
</Routes>
</Router>
</AuthContext.Provider>
);
}
useContext
在ProductList裡,先import useContext及AuthContext
import React, {useState, useEffect, useContext} from 'react';
import {AuthContext, STATUS} from '../account/AuthContext';
在ProductList裡,利用useContext hook取得AuthContext裡的值。
const authContext = useContext(AuthContext);
利用authContext就可以取得status了! 不再需要從App.js將變數傳進ProductList了! 當還沒登入時,我們可以把新增的FAB,隱藏起來。利用同樣的方式,可以把修改、刪除的ICON隱藏起來。
{(authContext.status===STATUS.toSignIn)?
<Box></Box>:
<Fab color="primary" aria-label="新增" onClick={addData}
sx={{
position: "fixed",
bottom: (theme) => theme.spacing(2),
right: (theme) => theme.spacing(8)
}}
>
<AddIcon />
</Fab>
}
更動context
那要怎麼更動Context裡的值呢? 先在AuthContext留一個空的方法,其實這裡的變數值及方法都會在設定Provider時覆蓋,在這裡寫的好處是可以知道AuthContext會有status變數及set方法。
/src/account/AuthContext.js
import React from 'react';
export const STATUS = {
toSignIn: 1,
toSignOut: 2,
toSignUp: 0,
};
export const AuthContext = React.createContext({
status: STATUS.signIn, setStatus:(newStatus)=>{this.status=newStatus}
})
/*
status及setStatus在provider會被覆蓋
status為toSignIn 已註冊,將要登入
status為toSignOut 已登入,將要登出
status為toSignUp 未註冊,將要註冊
*/
在AppRouter.js裡,新增status,將status及更動的方法綁定AuthContext的Provider。這就是為何我們需要把內容從index.js移到AppRouter.js,因為index.js裡不能有useState。
import React, {useState} from 'react';
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom';
import ProductList from './product/ProductList';
import EmployeeList from './employee/EmployeeList';
import Main from './ui/Main';
import {AuthContext, STATUS} from './account/AuthContext';
export default function AppRouter(){
const [status, setStatus] = useState(STATUS.toSignIn);
return(
<AuthContext.Provider value={{status, setStatus}}>
<Router>
<Routes>
<Route path="/" element={<Main/>}/>
<Route path="/product" element={<ProductList/>}/>
<Route path="/employee" element={<EmployeeList/>}/>
</Routes>
</Router>
</AuthContext.Provider>
);
}
在SignIn.js裡
import {AuthContext, STATUS} from '../account/AuthContext';
利用useContext取得authContext
export default function SignIn() {
const [account, setAccount] = useState({email:"",password:"", displayName:""});
const [message, setMessage] = useState("");
const authContext = useContext(AuthContext);
就可以去設定變數內容了:
authContext.setStatus(STATUS.signOut);
將原本使用props的部分改成呼叫authContext裡的setStatus,記得,事實上是呼叫App的setStatus。可是,不需要從App傳props到Main,再從Main傳進SignIn。
/src/account/SignIn.js
import React, {useState, useContext} from 'react';
import {Button, TextField} from '@mui/material';
import { getApps, initializeApp } from "firebase/app";
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
import {config} from '../settings/firebaseConfig';
import {AuthContext, STATUS} from '../account/AuthContext';
//import { Box } from '@mui/system';
export default function SignIn() {
if (getApps().length===0) {
initializeApp(config);
}
const authContext = useContext(AuthContext);
const [account, setAccount] = useState({email:"",password:"", displayName:""});
const [message, setMessage] = useState("");
const handleChange = function(e){
setAccount({...account,[e.target.name]:e.target.value})
}
const handleSubmit = async function(){
try {
const auth = getAuth();
const res = await signInWithEmailAndPassword(auth, account.email, account.password);
//console.log(res);
if (res) {
console.log(auth.currentUser.displayName);
setMessage("");
authContext.setStatus(STATUS.toSignOut);
//updateProfile(auth.currentUser,{displayName: account.displayName});
}
}
catch(error){
setMessage(""+error);
}
}
const changeStatus = function(){
authContext.setStatus(STATUS.toSignUp);
}
return(
<form>
<TextField type = "email" name = "email" value={account.email}
placeholder="電子郵件信箱" label="電子郵件信箱:" onChange={handleChange} autoComplete="email"/><br/>
<TextField type = "password" name = "password" value={account.password}
placeholder="密碼" label="密碼:" onChange={handleChange} autoComplete="current-password"/><br/>
{message}<br/>
<Button variant="contained" color="primary" onClick={handleSubmit}>登入</Button>
<Button variant="contained" color="secondary" onClick={changeStatus}>我要註冊</Button>
</form>
)
}
作業
完成Main、SignUp、SignOut的修改
完成其他相關元件,如ProductList的修改