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的修改

參考資料