原文: How to manage state in a React app with just Context and Hooks
做者:Samuel Omole
譯者:博軒
爲保證文章的可讀性,本文采用意譯
自從 React Hooks 發佈以來,數以千計關於它的文章,庫和視頻課程已經被髮布。若是本身搜索下這些資源,您會發現我前段時間寫的一篇文章,是關於如何使用 Hooks 構建示例應用程序。您能夠在 這裏找到它。
基於該文章,不少人(其實是兩個)提出了有關如何僅使用 Context 和 Hooks 在 React 應用程序中管理 State 的問題,這讓我對這個問題產生了一些研究。css
所以,對於本文,咱們將使用一種模式來管理狀態,該模式使用兩個很是重要的 Hooks ( useContext
和 useReducer
) 來構建簡單的音樂畫廊應用。該應用程序只有兩個視圖:一個用於登陸,另外一個用於列出該畫廊中的歌曲。html
採用登陸頁面做爲示例的主要緣由,是當咱們想在組件之間共享登陸(Auth)狀態的時候,一般會採用 Redux 來實現。react
等到完成的時候,咱們應該會擁有一個以下圖所示的應用程序:git
對於後端服務,我設置了一個簡單的 Express 應用程序,並將其託管於 Heroku 上。它有兩個主要的接口:github
/login
- 用於認證。成功登陸後,它將返回 JWT 令牌和用戶詳細信息。/songs
- 返回歌曲列表。若是您想添加其餘功能,能夠在此處找到後端應用程序的儲存庫。express
在構建應用以前,讓咱們先來看下接下來要使用的 Hooks:npm
useState
- 該 Hook 容許咱們在函數組件中使用狀態(至關於 this.state
與 this.setState
在類組件中的做用)useContext
- 該 Hook 接受一個上下文( Context )對象,並在 MyContext.Provider
中返回任何傳入 value
屬性的值。若是您還不瞭解上下文,那麼這是一種將狀態從父組件傳遞到組件樹中任何其餘組件的方法(不論組件的深度如何),而沒必要經過不須要該狀態的其餘組件進行傳遞(這個問題也叫作「prop drilling」)。您能夠在此處閱讀有關上下文( Context )的更多信息。useReducer
- 這是 useState
的替代方法, 可用於複雜的狀態邏輯。這是我最喜歡的 Hook ,由於它使用起來就像 Redux 。 它能夠接收一個相似下面這樣的 reducer
函數:(state, action) => newState
這個函數在返回新狀態以前會接收一個初始狀態。json
首先,咱們可使用 create-react-app 腳手架來開始構建這個項目。在此以前,須要準備一些東西:後端
在您的終端,輸入:api
npx create-react-app hooked
或者,在全局安裝 create-react-app
npm install -g create-react-app create-react-app hooked
您將在本文結束時,建立5個組件:
import React from "react"; export const Header = () => { return ( <nav id="navigation"> <h1 href="#" className="logo"> HOOKED </h1> </nav> ); }; export default Header;
import React from "react"; export const Home = () => { return ( <div className="home"> </div> ); }; export default Home;
import React from "react"; import logo from "../logo.svg"; import { AuthContext } from "../App"; export const Login = () => { return ( <div className="login-container"> <div className="card"> <div className="container"> </div> </div> </div> ); }; export default Login;
最開始的時候,App.js
文件應該以下所示:
import React from "react"; import "./App.css"; function App() { return ( <div className="App"></div> ); } export default App;
接下來,咱們將建立 Auth 上下文,該上下文將 auth
狀態從該組件傳遞到須要它的任何其餘組件。代碼以下:
import React from "react"; import "./App.css"; import Login from "./components/Login"; import Home from "./components/Home"; import Header from "./components/Header"; export const AuthContext = React.createContext(); // added this function App() { return ( <AuthContext.Provider> <div className="App"></div> </AuthContext.Provider> ); } export default App;
而後,咱們添加 useReducer
hook 來處理咱們的身份驗證狀態,並有條件的展現 Login 組件和 Home 組件。
請記住,useReducer
具備兩個參數,一個 reducer 函數 (這是一個將狀態和操做做爲參數並根據操做返回新狀態的函數) 和一個初始狀態,該狀態也會傳遞給 reducer 函數。代碼以下:
import React from "react"; import "./App.css"; import Login from "./components/Login"; import Home from "./components/Home"; import Header from "./components/Header"; export const AuthContext = React.createContext(); const initialState = { isAuthenticated: false, user: null, token: null, }; const reducer = (state, action) => { switch (action.type) { case "LOGIN": localStorage.setItem("user", JSON.stringify(action.payload.user)); localStorage.setItem("token", JSON.stringify(action.payload.token)); return { ...state, isAuthenticated: true, user: action.payload.user, token: action.payload.token }; case "LOGOUT": localStorage.clear(); return { ...state, isAuthenticated: false, user: null }; default: return state; } }; function App() { const [state, dispatch] = React.useReducer(reducer, initialState); return ( <AuthContext.Provider value={{ state, dispatch }} > <Header /> <div className="App">{!state.isAuthenticated ? <Login /> : <Home />}</div> </AuthContext.Provider> ); } export default App;
上面的代碼片斷中發生了不少事情,讓我來解釋每一部分:
const initialState = { isAuthenticated: false, user: null, token: null, };
上面的片斷是咱們對象的初始狀態,將在 reducer 函數中使用。該對象中的值主要取決於您的使用場景。在咱們的示例中,須要檢查用戶是否登陸,登陸以後,服務器返回的信息是否包含 user
以及 token
數據。
const reducer = (state, action) => { switch (action.type) { case "LOGIN": localStorage.setItem("user", JSON.stringify(action.payload.user)); localStorage.setItem("token", JSON.stringify(action.payload.token)); return { ...state, isAuthenticated: true, user: action.payload.user, token: action.payload.token }; case "LOGOUT": localStorage.clear(); return { ...state, isAuthenticated: false, user: null, token: null, }; default: return state; } };
reducer 函數包含一個 switch-case 語句,該函數將根據某些設定的動做來返回新的狀態。reducer 中的動做是:
若是爲執行任何操做,將返回初始狀態。
const [state, dispatch] = React.useReducer(reducer, initialState);
useReducer
會返回兩個參數, state
和 dispatch
。state
包含組件中使用的狀態,並根據執行的動做進行更新。dispatch
是在應用程序中用於執行動做,修改狀態的函數。
<AuthContext.Provider value={{ state, dispatch }} > <Header /> <div className="App">{!state.isAuthenticated ? <Login /> : <Home />}</div> </AuthContext.Provider>
在 Context.Provider
組件中,咱們正在將一個對象傳遞到 value
prop 中。該對象包含 state 和 dispatch 函數,所以能夠由須要該上下文的任何其餘組件使用。而後,咱們有條件地渲染組件–若是用戶經過身份驗證,則渲染 Home 組件,不然渲染 Login 組件。
首先,添加一些表單的必要組件:
import React from "react"; export const Login = () => { return ( <div className="login-container"> <div className="card"> <div className="container"> <form> <h1>Login</h1> <label htmlFor="email"> Email Address <input type="text" name="email" id="email" /> </label> <label htmlFor="password"> Password <input type="password" name="password" id="password" /> </label> <button> "Login" </button> </form> </div> </div> </div> ); }; export default Login;
在上面的代碼中,咱們添加了顯示錶單的 JSX,接下來,咱們將添加 useState
Hook 來處理表單狀態。添加 Hook 後,咱們的代碼展現以下:
import React from "react"; export const Login = () => { const initialState = { email: "", password: "", isSubmitting: false, errorMessage: null }; const [data, setData] = React.useState(initialState); const handleInputChange = event => { setData({ ...data, [event.target.name]: event.target.value }); }; return ( <div className="login-container"> <div className="card"> <div className="container"> <form> <h1>Login</h1> <label htmlFor="email"> Email Address <input type="text" value={data.email} onChange={handleInputChange} name="email" id="email" /> </label> <label htmlFor="password"> Password <input type="password" value={data.password} onChange={handleInputChange} name="password" id="password" /> </label> {data.errorMessage && ( <span className="form-error">{data.errorMessage}</span> )} <button disabled={data.isSubmitting}> {data.isSubmitting ? ( "Loading..." ) : ( "Login" )} </button> </form> </div> </div> </div> ); }; export default Login;
在上面的代碼中,咱們將一個 initialState
對象傳遞給了 useState
Hook。在該對象中,咱們處理電子郵件、密碼的狀態,一個用於檢查是否正在向服務器發送數據的狀態,以及服務器返回的錯誤值。
接下來,咱們將添加一個函數,用於處理向後端 API 提交表單。在該函數中,咱們將使用 fetch
API 將數據發送到後端。若是請求成功,咱們將執行 LOGIN
操做,並將服務器返回的數據一塊兒傳遞。若是服務器返回錯誤(登陸信息有誤),咱們將調用 setData 並傳遞來自服務器的 errorMessage
,它將顯示在表單上。爲了執行 dispatch 函數,咱們須要將 App 組件中的 AuthContext
導入到 Login
組件中,而後 dispatch
函數就可使用了。代碼以下:
import React from "react"; import { AuthContext } from "../App"; export const Login = () => { const { dispatch } = React.useContext(AuthContext); const initialState = { email: "", password: "", isSubmitting: false, errorMessage: null }; const [data, setData] = React.useState(initialState); const handleInputChange = event => { setData({ ...data, [event.target.name]: event.target.value }); }; const handleFormSubmit = event => { event.preventDefault(); setData({ ...data, isSubmitting: true, errorMessage: null }); fetch("https://hookedbe.herokuapp.com/api/login", { method: "post", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: data.email, password: data.password }) }) .then(res => { if (res.ok) { return res.json(); } throw res; }) .then(resJson => { dispatch({ type: "LOGIN", payload: resJson }) }) .catch(error => { setData({ ...data, isSubmitting: false, errorMessage: error.message || error.statusText }); }); }; return ( <div className="login-container"> <div className="card"> <div className="container"> <form onSubmit={handleFormSubmit}> <h1>Login</h1> <label htmlFor="email"> Email Address <input type="text" value={data.email} onChange={handleInputChange} name="email" id="email" /> </label> <label htmlFor="password"> Password <input type="password" value={data.password} onChange={handleInputChange} name="password" id="password" /> </label> {data.errorMessage && ( <span className="form-error">{data.errorMessage}</span> )} <button disabled={data.isSubmitting}> {data.isSubmitting ? ( "Loading..." ) : ( "Login" )} </button> </form> </div> </div> </div> ); }; export default Login;
該 Home
組件將處理從服務器獲取的歌曲並顯示他們。因爲後端須要請求的時候帶上身份信息,所以咱們須要找一種方法,把存貯在 App
組件中的身份信息取出來。
讓咱們開始構建這個組件。咱們須要獲取歌曲數據並映射到列表,而後使用 Card
組件來渲染每一首歌曲。Card
組件是一個簡單的函數組件,它會將 props 傳遞給 render 函數並渲染。代碼以下:
import React from "react"; export const Card = ({ song }) => { return ( <div className="card"> <img src={song.albumArt} alt="" /> <div className="content"> <h2>{song.name}</h2> <span>BY: {song.artist}</span> </div> </div> ); }; export default Card;
由於它不處理任何自定義邏輯,只是展現 props 中的內容,咱們稱它爲演示組件。
回到咱們的 Home
組件中,當大多數應用程序在處理網絡請求時,咱們一般經過三個狀態來實現可視化。首先,在處理請求時(展現加載中),請求成功時(展現頁面,並提示成功),最後請求失敗時(展現錯誤通知)。爲了在加載組件時發出請求並同時處理這三種狀態,咱們將使用 useEffect
和 useReducer
Hook。
首先咱們來建立一個初始狀態:
const initialState = { songs: [], isFetching: false, hasError: false, };
songs
將保留從服務器檢索到的歌曲列表,初始值爲空。isFetching 用於表示加載狀態,初始值爲 false。hasError 用於表示錯誤狀態,初始值爲 false。
如今,咱們能夠爲此組件建立 reducer,並結合 Home 組件,代碼以下:
import React from "react"; import { AuthContext } from "../App"; import Card from "./Card"; const initialState = { songs: [], isFetching: false, hasError: false, }; const reducer = (state, action) => { switch (action.type) { case "FETCH_SONGS_REQUEST": return { ...state, isFetching: true, hasError: false }; case "FETCH_SONGS_SUCCESS": return { ...state, isFetching: false, songs: action.payload }; case "FETCH_SONGS_FAILURE": return { ...state, hasError: true, isFetching: false }; default: return state; } }; export const Home = () => { const [state, dispatch] = React.useReducer(reducer, initialState); return ( <div className="home"> {state.isFetching ? ( <span className="loader">LOADING...</span> ) : state.hasError ? ( <span className="error">AN ERROR HAS OCCURED</span> ) : ( <> {state.songs.length > 0 && state.songs.map(song => ( <Card key={song.id.toString()} song={song} /> ))} </> )} </div> ); }; export default Home;
reducer 函數中定義了視圖的三種狀態,而視圖中也根據狀態設置了:加載中,請求失敗,請求成功三種狀態。
接下來,咱們須要添加 useEffect
來處理網絡請求,並調用相應的 ACTION
。代碼以下:
import React from "react"; import { AuthContext } from "../App"; import Card from "./Card"; const initialState = { songs: [], isFetching: false, hasError: false, }; const reducer = (state, action) => { switch (action.type) { case "FETCH_SONGS_REQUEST": return { ...state, isFetching: true, hasError: false }; case "FETCH_SONGS_SUCCESS": return { ...state, isFetching: false, songs: action.payload }; case "FETCH_SONGS_FAILURE": return { ...state, hasError: true, isFetching: false }; default: return state; } }; export const Home = () => { const { state: authState } = React.useContext(AuthContext); const [state, dispatch] = React.useReducer(reducer, initialState); React.useEffect(() => { dispatch({ type: "FETCH_SONGS_REQUEST" }); fetch("https://hookedbe.herokuapp.com/api/songs", { headers: { Authorization: `Bearer ${authState.token}` } }) .then(res => { if (res.ok) { return res.json(); } else { throw res; } }) .then(resJson => { console.log(resJson); dispatch({ type: "FETCH_SONGS_SUCCESS", payload: resJson }); }) .catch(error => { console.log(error); dispatch({ type: "FETCH_SONGS_FAILURE" }); }); }, [authState.token]); return ( <React.Fragment> <div className="home"> {state.isFetching ? ( <span className="loader">LOADING...</span> ) : state.hasError ? ( <span className="error">AN ERROR HAS OCCURED</span> ) : ( <> {state.songs.length > 0 && state.songs.map(song => ( <Card key={song.id.toString()} song={song} /> ))} </> )} </div> </React.Fragment> ); }; export default Home;
若是您注意到,在上面的代碼中,咱們使用了另外一個 hook,useContext
。緣由是,爲了從服務器獲取歌曲,咱們還必須傳遞在登陸頁面上提供給咱們的 token
。可是,咱們將 token
保存於另一個組件,因此須要使用 useContext
從 AuthContext
中取出 token
。
在 useEffect 函數內部,咱們首先執行 FETCH_SONGS_REQUEST
以便顯示加載中的狀態,而後使用 fetchAPI
發出網絡請求,並將 token 放到 header 中傳遞。若是響應成功,咱們將執行該 FETCH_SONGS_SUCCESS
動做,並將從服務器獲取的歌曲列表做傳遞給該動做。若是服務器出現錯誤,咱們將執行 FETCH_SONGS_FAILURE
動做,以使錯誤範圍顯示在屏幕上。
使用 useEffect hook 要注意的最後一件事,咱們在 hook 的依賴項數組中傳遞 token。這意味着咱們只會在令牌更改時調用該 hook,只有在 token 過時且咱們須要獲取一個新 token 或以新用戶身份登陸時,纔會觸發該 hook。所以,對於此用戶,該 hook 僅被調用一次。
好的,咱們已經完成全部邏輯。
本文有點長,可是它確實涵蓋了使用 hook 來管理應用程序中的狀態的常見用例。
你能夠訪問github 地址來查看代碼,也能夠在此基礎上添加一些功能。
我也寫了栗子: git地址 | 在線預覽地址