原文: How to manage state in a React app with just Context and Hooks
做者:Samuel Omole
譯者:博軒
爲保證文章的可讀性,本文采用意譯
自從 React Hooks 發佈以來,數以千計關於它的文章,庫和視頻課程已經被髮布。若是本身搜索下這些資源,您會發現我前段時間寫的一篇文章,是關於如何使用 Hooks 構建示例應用程序。您能夠在 這裏找到它。
基於該文章,不少人(其實是兩個)提出了有關如何僅使用 Context 和 Hooks 在 React 應用程序中管理 State 的問題,這讓我對這個問題產生了一些研究。javascript
所以,對於本文,咱們將使用一種模式來管理狀態,該模式使用兩個很是重要的 Hooks ( useContext
和 useReducer
) 來構建簡單的音樂畫廊應用。該應用程序只有兩個視圖:一個用於登陸,另外一個用於列出該畫廊中的歌曲。css
採用登陸頁面做爲示例的主要緣由,是當咱們想在組件之間共享登陸(Auth)狀態的時候,一般會採用 Redux 來實現。html
等到完成的時候,咱們應該會擁有一個以下圖所示的應用程序:java
對於後端服務,我設置了一個簡單的 Express 應用程序,並將其託管於 Heroku 上。它有兩個主要的接口:react
/login
- 用於認證。成功登陸後,它將返回 JWT 令牌和用戶詳細信息。/songs
- 返回歌曲列表。若是您想添加其餘功能,能夠在此處找到後端應用程序的儲存庫。git
在構建應用以前,讓咱們先來看下接下來要使用的 Hooks:github
useState
- 該 Hook 容許咱們在函數組件中使用狀態(至關於 this.state
與 this.setState
在類組件中的做用)useContext
- 該 Hook 接受一個上下文( Context )對象,並在 MyContext.Provider
中返回任何傳入 value
屬性的值。若是您還不瞭解上下文,那麼這是一種將狀態從父組件傳遞到組件樹中任何其餘組件的方法(不論組件的深度如何),而沒必要經過不須要該狀態的其餘組件進行傳遞(這個問題也叫作「prop drilling」)。您能夠在此處閱讀有關上下文( Context )的更多信息。useReducer
- 這是 useState
的替代方法, 可用於複雜的狀態邏輯。這是我最喜歡的 Hook ,由於它使用起來就像 Redux 。 它能夠接收一個相似下面這樣的 reducer
函數:(state, action) => newState複製代碼
這個函數在返回新狀態以前會接收一個初始狀態。sql
首先,咱們可使用 create-react-app 腳手架來開始構建這個項目。在此以前,須要準備一些東西:typescript
在您的終端,輸入:express
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地址 | 在線預覽 地址