react上下文
Since the announcement of React Hooks, hundreds, if not thousands of articles, libraries, and video courses about them have been released. If you look carefully into the sea of resources, you'll find an article I wrote a while back that involved building a sample application using Hooks. You can find that article here.
自从宣布React Hooks以来,已经发布了数百本(甚至数千本)关于它们的文章,库和视频课程。 如果仔细看一下资源之海,您会发现我前一段时间写的一篇文章,其中涉及使用Hooks构建示例应用程序。 您可以在这里找到该文章。
Based on that article, a lot (two actually) of people asked questions related to how State can be managed in a React application using just Context and Hooks, which led to me doing a little research on the subject.
基于该文章,很多人(实际上是两个人)提出了有关如何仅使用Context和Hooks在React应用程序中管理State的问题,这导致我对此进行了一些研究。
So for this article, we will be working with a pattern for managing state using two very important Hooks, useContext and useReducer, to build a simple music gallery app. The application will have only two views: one for login and the other to list the songs in that gallery.
因此,对于本文,我们将使用一种模式来管理状态,该模式使用两个非常重要的Hooks(useContext和useReducer)来构建简单的音乐画廊应用。 该应用程序只有两个视图:一个用于登录,另一个用于列出该画廊中的歌曲。
The main reason for the login page is to show how we can share the Auth state across the application, which is a common use case for applications that use a library like Redux.
登录页面的主要原因是要显示我们如何在应用程序之间共享Auth状态,这是使用Redux之类的应用程序的常见用例。
By the time we are done we should have an application that looks like the images below:
到完成时,我们应该拥有一个如下图所示的应用程序:
For the backend server, I set up a simple Express application and hosted it on Heroku. It has two main endpoints:
对于后端服务器,我设置了一个简单的Express应用程序并将其托管在Heroku上。 它有两个主要端点:
/login
— For authentication. On successful login, it returns a JWT token and user details./login
用于身份验证。 成功登录后,它将返回JWT令牌和用户详细信息。/songs
— Returns a list of songs./songs
返回歌曲列表。
In case you want to add extra functionality, the repository for the backend application can be found here.
如果您想添加额外的功能,可以在此处找到后端应用程序的存储库。
收获 (RECAP)
Before we go into building the application, let’s look at some of the hooks we will be using:
在构建应用程序之前,让我们看一下将要使用的一些钩子:
useState
— This hook allows us to use state in function components (the equivalent tothis.state
andthis.setState
in class components)useState
—该钩子允许我们在函数组件中使用状态(等效于类组件中的this.state
和this.setState
)useContext
— This hook takes in a context object and returns whatever is passed in as a value prop inMyContext.Provider
. If you do not know about context, it's a way of passing state from a parent component to any other component within the tree (no matter how deep) without having to pass it through other components that do not require it (a problem aptly named prop drilling). You can read more about context here.useContext
此挂钩接受一个上下文对象,并返回作为MyContext.Provider
的值prop传入的任何MyContext.Provider
。 如果您不了解上下文,那么这是一种将状态从父组件传递到树中任何其他组件的方法(无论深度如何),而不必通过不需要它的其他组件传递(这个问题恰当地称为prop钻Kong)。 您可以在此处阅读更多有关上下文的信息 。useReducer
— This is an alternative touseState
and it can be used for complex state logic. This is my favorite hook because it works just like the Redux library. It accepts a reducer of type:useReducer
-这是一个替代useState
并且它可用于复杂的状态的逻辑。 这是我最喜欢的钩子,因为它就像Redux库一样工作。 它接受以下类型的减速器:
(state, action) => newState
And also an initial state object before returning the new state.
以及返回新状态之前的初始状态对象。
入门 (GETTING STARTED)
To get started, we are going to use the create-react-app library to bootstrap the project. But before that, below are some of the requirements needed to follow along:
首先,我们将使用create-react-app库来引导项目。 但在此之前,下面是一些需要遵循的要求:
- Node (≥ 6) 节点(≥6)
- A text editor 文字编辑器
In your terminal, enter the command:
在您的终端中,输入命令:
npx create-react-app hooked
If you do not have npx
available you can install create-react-app globally on your system:
如果没有可用的npx
,则可以在系统上全局安装create-react-app:
npm install -g create-react-app
create-react-app hooked
You will create five components by the end of this article:
您将在本文结尾创建五个组件:
- Header.js — This component will contain the header of the application (obviously), and also display a logout button that contains the user’s first name. The button will only show if the user is authenticated. Header.js —该组件将(显然)包含应用程序的标题,并显示一个包含用户名字的注销按钮。 仅当用户通过身份验证时,该按钮才会显示。
- App.js — This is the top-level component where we will create the authentication context (I will talk about this later). This component will also conditionally render either the Login component if the user isn’t logged in or the Home component if the user is authenticated. App.js —这是我们将在其中创建身份验证上下文的顶级组件(我将在后面讨论)。 如果用户未登录,则此组件还将有条件地呈现“登录”组件,如果用户通过了身份验证,则将有条件呈现“主页”组件。
- Home.js — This component will fetch a list of songs from the server and render it on the page. Home.js —该组件将从服务器获取歌曲列表并将其呈现在页面上。
- Login.js — This component will contain the login form for the user. It will also be responsible for making a POST request to the login endpoint and updating the authentication context with the response from the server. Login.js —该组件将包含用户的登录表单。 它还将负责向登录端点发出POST请求,并使用服务器的响应来更新身份验证上下文。
- Card.js — This is a presentational component (UI) that renders the details of a song passed into it. Card.js —这是一个表示性组件(UI),用于呈现传递到其中的歌曲的详细信息。
Now let's create empty components that we will later add logic to. In the src
folder, create a folder and name it components
then create four these four files, namely, Header.js
, Home.js
, Login.js
, and Card.js
:
现在,让我们创建空组件,稍后再向其中添加逻辑。 在src
文件夹中,创建一个文件夹并命名其components
然后创建这四个文件中的四个,即Header.js
, Home.js
, Login.js
和Card.js
:
Header.js (
Header.js)
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;
Login.js (Login.js)
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;
And the App.js
file should look like this:
而且App.js
文件应如下所示:
import React from "react";
import "./App.css";
function App() {
return (
<div className="App"></div>
);
}
export default App;
In the App.js
file, we will create the Auth context that will pass the auth state from this component to any other component that requires it. Create an authentication context like this below:
在App.js
文件中,我们将创建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;
Then we add the useReducer
hook to handle our authentication state, and conditionally render either the Login component or the Home component.
然后,我们添加useReducer
挂钩来处理我们的身份验证状态,并有条件地呈现Login组件或Home组件。
Remember that the useReducer
hook takes two parameters, a reducer (which is simply a function that takes in state and action as parameters and returns a new state based on an action) and an initial state which will be passed into the reducer. Let's then add the hook into our App
component as shown below:
请记住, useReducer
挂钩具有两个参数,一个reducer(这是一个将状态和操作作为参数并根据操作返回新状态的函数)和一个初始状态,该状态将传递给reducer。 然后,将钩子添加到我们的App
组件中,如下所示:
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;
There is a lot going on in the snippet above, but let me explain each part:
上面的代码片段中发生了很多事情,但是让我解释一下每个部分:
const initialState = {
isAuthenticated: false,
user: null,
token: null,
};
The above snippet is our initial state object that will be used in our reducer. The values in this object depend mainly on your use case. In our case we need to check if a user is authenticated, contains the user
data, and if a token
was sent back from the server after login.
上面的代码片段是将在我们的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;
}
};
The reducer function contains a case-switch statement that, based on certain actions, returns a new state. The actions in the reducer are:
reducer函数包含一个case-switch语句,该语句根据某些操作返回一个新状态。 减速器中的动作是:
LOGIN
— When this type of action is dispatched, it will also be dispatched with a payload (containinguser
andtoken
). It saves the user and token to localStorage and then returns a new state, settingisAuthenticated
totrue
, and also sets theuser
andtoken
keys to their respective values based on the action’s payload.LOGIN
—调度此类型的操作时,还将与有效负载(包含user
和token
)一起调度。 它将用户和令牌保存到localStorage,然后返回新状态,将isAuthenticated
设置为true
,并根据操作的有效负载将user
和token
键设置为其各自的值。LOGOUT
— When this action is dispatched, we clear localStorage of all data and setuser
andtoken
tonull
.LOGOUT
—调度此操作后,我们将清除所有数据的localStorage并将user
和token
设置为null
。
If no action is dispatched, it returns the initial state.
如果未分派任何操作,它将返回初始状态。
const [state, dispatch] = React.useReducer(reducer, initialState);
The useReducer
hook returns two parameters, state
and dispatch
. state
contains the state that is used in the component and it is updated based on the actions dispatched. Dispatch
is a function that is used in the application to call/dispatch actions that transform or change the state.
useReducer
挂钩返回两个参数: state
和dispatch
。 state
包含组件中使用的状态,并根据分派的操作进行更新。 Dispatch
是应用程序中用于调用/调度转换或更改状态的动作的功能。
<AuthContext.Provider
value={{
state,
dispatch
}}
>
<Header />
<div className="App">{!state.isAuthenticated ? <Login /> : <Home />}</div>
</AuthContext.Provider>
Here in the Context.Provider
component, we are passing an object into the value
prop. The object contains the state
and the dispatch
function so that it can be used by any other component that requires that context. Then we conditionally render the components–if the user is authenticated we render the Home
component, else we render the Login
component.
在Context.Provider
组件中,我们将一个对象传递给value
属性。 该对象包含state
和dispatch
功能,以便可以由需要该上下文的任何其他组件使用。 然后,我们有条件地呈现组件-如果用户通过了身份验证,则呈现Home
组件,否则呈现Login
组件。
登录组件 (Login Component)
In the login component, let us add the necessary elements for the form as shown below:
在登录组件中,让我们为表单添加必要的元素,如下所示:
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;
In the above code, we added the JSX that displays the form, next we will be adding the useState
hook to handle the form state. Once we add the hook, our code should look like this:
在上面的代码中,我们添加了显示表单的JSX,接下来,我们将添加useState
钩子来处理表单状态。 添加钩子后,我们的代码应如下所示:
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;
In the code above, we passed in an initialState
object into the useState
hook. In the object we handle the email state, the password state, a state that is used to check if the form is being sent to the server and also an errorMessage
value that handles errors from the server.
在上面的代码中,我们将initialState
对象传递给useState
挂钩。 在该对象中,我们处理电子邮件状态,密码状态,用于检查表单是否已发送到服务器的状态以及用于处理来自服务器的错误的errorMessage
值。
Next, we will add a function that handles the form submission to the backend API. In that function, we will use the fetch
API to send the payload to the server. If the response is successful, we will dispatch a LOGIN
action and also pass the response from the server as a payload in the dispatched action. If there is an error from the server (if the login credentials are not valid), we call setData
and pass the errorMessage
from the server which will be displayed on the form. In order to call dispatch, we need to import the AuthContext
from the App
component into our Login
component and then use the dispatch
function in the app. Your final Login
component should look like below:
接下来,我们将添加一个函数,用于处理向后端API提交表单。 在该函数中,我们将使用fetch
API将有效负载发送到服务器。 如果响应成功,我们将分派LOGIN
操作,并将来自服务器的响应作为有效负载传递给分派的操作。 如果服务器出现错误(如果登录凭据无效),我们将调用setData
并从服务器传递errorMessage
,该消息将显示在表单上。 为了调用调度,我们需要将AuthContext
从App
组件导入到我们的Login
组件中,然后在应用中使用dispatch
功能。 您最终的Login
组件应如下所示:
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 Component)
The Home
component will handle fetching the songs from the server and displaying them. Since the API endpoint requires that we send the authentication token, we will need to find a way to get it from the App
component where it was stored.
Home
组件将处理从服务器获取的歌曲并显示它们。 由于API端点要求我们发送身份验证令牌,因此我们需要找到一种从存储令牌的App
组件中获取身份验证令牌的方法。
Let’s build the markup for this component. We want to fetch the songs and map through the list of returned songs and then render a Card
component for each song. The Card
component is a simple functional component that is passed some props
to render. Create a Card.js
file in the components
folder, and in that file add the following code below:
让我们为该组件构建标记。 我们要获取歌曲并在返回的歌曲列表中进行映射,然后为每首歌曲渲染Card
组件。 Card
组件是一个简单的功能组件,传递了一些props
来进行渲染。 在components
文件夹中创建一个Card.js
文件,然后在该文件中添加以下代码:
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;
Because it does not handle any custom logic but rather renders the props passed into it, we call it a Presentational Component.
因为它不处理任何自定义逻辑,而是渲染传递给它的道具,所以我们将其称为演示组件。
Back in our Home
component, when handling network requests in most applications, we try to visualize three main states. First, when the request is processing (by using a loader of some sort), then when the request is successful (by rendering the payload or showing a success notification), and finally, when the request fails (by showing an error notification). In order to make a request when the component is mounted and also handling these three states, we will make use of the useEffect
and useReducer
hooks.
回到我们的Home
组件中,当在大多数应用程序中处理网络请求时,我们尝试可视化三个主要状态。 首先,在处理请求时(通过使用某种加载程序),然后在请求成功时(通过呈现有效负载或显示成功通知),最后在请求失败时(通过显示错误通知)。 为了在安装组件时发出请求并同时处理这三种状态,我们将使用useEffect
和useReducer
钩子。
For our useReducer
hook, we will first create an object to hold the initial state for our reducer, the initial state object will look like the snippet below:
对于我们的useReducer
挂钩,我们将首先创建一个对象来保存我们的reducer的初始状态,该初始状态对象将类似于以下代码片段:
const initialState = {
songs: [],
isFetching: false,
hasError: false,
};
songs
will hold the list of songs retrieved from the server and it is initially empty. isFetching
is used to represent the loading state and is initially set to false
. hasError
is used to represent the error state and is also initially set to false
.
songs
将保存从服务器检索到的歌曲列表,并且最初为空。 isFetching
用于表示加载状态,初始设置为false
。 hasError
用于表示错误状态,并且最初也设置为false
。
We can now create the reducer for this component, it will look like the snippet below:
现在,我们可以为此组件创建reducer,它看起来像下面的代码片段:
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;
}
};
Let’s break it down. If we dispatch a FETCH_SONGS_REQUEST
action in our app, we return a new state with the value of isFetching
set to true
. If we dispatch a FETCH_SONGS_SUCCESS
action in our app, we return a new state with the value of isFetching
set to false
, and then songs
set to the payload sent back from the server. Finally, if we dispatch a FETCH_SONGS_FAILURE
action in our app, we return a new state with the value of isFetching
set to false
and hasError
set to false
.
让我们分解一下。 如果我们在应用程序中调度FETCH_SONGS_REQUEST
操作, FETCH_SONGS_REQUEST
返回isFetching
设置为true
的新状态。 如果我们在应用程序中调度FETCH_SONGS_SUCCESS
操作, FETCH_SONGS_SUCCESS
返回一个新状态,其isFetching
的值设置为false
,然后将songs
设置为从服务器发送回的有效负载。 最后,如果我们在应用程序中调度FETCH_SONGS_FAILURE
操作,我们将返回一个新状态,其isFetching
值设置为false
, hasError
设置为false
。
Now that we have the useReducer hook, our Home
component should look like this:
现在我们有了useReducer挂钩,我们的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;
To quickly run through what is going on, inside the Home
function we add the useReducer
hook and pass in the reducer
and initialState
which in turn returns two variables, namely, state
and dispatch
.
为了快速了解发生的情况,我们在Home
函数中添加了useReducer
钩子,并传入reducer
和initialState
,然后返回两个变量,即state
和dispatch
。
Then in our render function, we conditionally render a span
with a “loading…” text if state.isFetching = true
, or we render a span
with an error message if state.hasError = true
. Otherwise we loop through the list of songs and render each one as a Card
component, passing in the necessary props
.
然后在我们的渲染功能,我们有条件地呈现span
与“载入中...”文本,如果state.isFetching = true
,或在我们渲染span
与如果错误信息state.hasError = true
。 否则,我们循环浏览歌曲列表,并将每首歌曲呈现为Card
组件,并传递必要的props
。
To tie everything up, we will add the useEffect
function that will handle the network calls and dispatch the necessary ACTION
based on the server response. Adding the hook should make our Home
component look like the snippet below:
为了捆绑所有内容,我们将添加useEffect
函数,该函数将处理网络调用并根据服务器响应调度必要的ACTION
。 添加该钩子应该使我们的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: 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;
If you notice, in the code above, we used another hook, the useContext
hook. The reason is, in order to fetch songs from the server we have to also pass the token that was given to us on the login page. But since that was another component, we stored the token in the AuthContext
and we use the useContext
hook to get that context value and use it in our own component.
如果您注意到,在上面的代码中,我们使用了另一个钩子useContext
钩子。 原因是,为了从服务器获取歌曲,我们还必须传递在登录页面上提供给我们的令牌。 但是由于那是另一个组件,所以我们将令牌存储在AuthContext
并使用useContext
钩子获取该上下文值并在我们自己的组件中使用它。
Inside the useEffect
function, we initially dispatch the FETCH_SONGS_REQUEST
so that the loading span shows, then we make the network request using the fetch
API and passing the token we got from the AuthContext
as a header. If the response is successful, we dispatch the FETCH_SONGS_SUCCESS
action and pass the list of songs gotten from the server as payload in the action. If there is an error from the server, we dispatch FETCH_SONGS_FAILURE
action so that the error span is displayed on the screen.
在useEffect
函数内部,我们首先调度FETCH_SONGS_REQUEST
以便显示加载范围,然后使用fetch
AuthContext
API发出网络请求,并将从AuthContext
获得的令牌作为标头传递。 如果响应成功,我们将调度FETCH_SONGS_SUCCESS
操作,并将从服务器获取的歌曲列表作为有效负载传递给该操作。 如果服务器出现错误,我们将调度FETCH_SONGS_FAILURE
操作,以便在屏幕上显示错误范围。
The last thing to note in our useEffect
hook is that we pass the token in the dependency array of the hook (read more about useEffect
here). This means that our hook will only be called when that token changes, which can only happen if the token expires and we need to fetch a new one or we log in as a new user. So for this user, the hook will be called only once.
在useEffect
挂钩中要注意的最后一件事是,我们在挂钩的依赖项数组中传递令牌(在此处了解有关useEffect
更多信息)。 这意味着我们只会在令牌更改时才调用该挂钩,只有在令牌过期且我们需要获取一个新令牌或以新用户身份登录时才会发生。 因此,对于此用户,该挂钩仅被调用一次。
OK, we are done with the logic. All that’s left is the CSS. Since going into the details of the styling of the app is beyond the scope of this article, you can copy the CSS snippet below and paste it in the App.css
file:
好的,我们已经完成了逻辑。 剩下的就是CSS。 由于进入应用程序样式的详细信息不在本文的讨论范围之内,因此您可以复制下面CSS代码段并将其粘贴到App.css
文件中:
/****** LOGIN PAGE ******/
.login-container{
display: flex;
align-items: center;
background-image: url("./assets/carry-on-colour.svg");
height: calc(100vh - 70px);
background-repeat: no-repeat;
background-position: right;
padding-left: 5%;
padding-right: 5%;
margin-top: 70px;
}
.card {
/* Add shadows to create the "card" effect */
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
transition: 0.3s;
height: 70%;
width: 45%;
}
/* On mouse-over, add a deeper shadow */
.card:hover {
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
/* Add some padding inside the card container */
.login-container .container {
padding-left: 7%;
padding-right: 7%;
height: 100%;
}
.login-container .container h1{
font-size: 2.5rem;
}
.login-container .container form{
display: flex;
height: 80%;
flex-direction: column;
justify-content: space-around;
align-self: center;
}
input[type="text"], input[type="password"]{
padding-left: 1px;
padding-right: 1px;
height: 40px;
border-radius: 5px;
border: .5px solid rgb(143, 143, 143);
font-size: 15px;
}
label{
display: flex;
flex-direction: column;
}
.login-container button{
height: 40px;
font-weight: bold;
font-size: 15px;
background-color: #F42B4B;
color: rgb(255, 255, 255);
}
.login-container button:hover{
background-color: rgb(151, 25, 46);
cursor: pointer;
}
.login-container button:focus{
outline: none !important;
}
.spinner {
animation: spinner infinite .9s linear;
height: 90%;
}
.spinner:focus{
border:none;
}
@keyframes spinner {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.form-error{
color: #F42B4B;
text-align: center;
}
@media screen and (max-width: 700px){
.login-container{
justify-content: center;
background-image: none;
}
.card {
width: 80%;
align-self: center;
}
}
@media screen and (max-width: 350px){
.card {
width: 100%;
}
}
/****** LOGIN PAGE ******/
/****** HEADER ******/
#navigation{
width: 100%;
position: fixed;
z-index: 10;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
background-color: #F42B4B;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
height: 70px;
top: 0;
padding-right: 5px;
padding-left: 5px;
}
#navigation h1{
color: white;
}
#navigation button{
background-color: transparent;
border: none;
align-self: center;
}
#navigation button:hover{
cursor: pointer;
}
#navigation button:focus{
outline: none !important;
}
/****** HEADER ******/
/****** HOME PAGE ******/
.home {
margin-top: 100px;
margin-left: 2%;
margin-right: 2%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.home .loader{
align-self: center;
width: 100%;
text-align: center;
}
.home .error{
width: 100%;
align-self: center;
color: #F42B4B;
font-size: 30px;
font-weight: bold;
text-align: center;
}
.home>.card {
/* Add shadows to create the "card" effect */
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
transition: 0.3s;
height: 400px;
width: 30%;
position: relative;
margin-bottom: 2%;
}
/* On mouse-over, add a deeper shadow */
.home .card:hover {
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
.home .card>img{
width: 100%;
height: 100%;
}
.home .content{
bottom: 0;
z-index: 9;
position: absolute;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
height: 35%;
padding-bottom: 5px;
transition: 0.5s;
}
.home .content:hover{
background-color: rgba(255, 255, 255, 1);
height: 50%;
cursor: pointer;
}
.content>h2{
text-align: center;
font-size: 2rem;
}
@media screen and (max-width: 780px){
.home{
justify-content: space-around;
}
.home .card {
width: 45%;
}
}
@media screen and (max-width: 500px){
.home .card {
width: 90%;
}
}
@media screen and (min-width: 1400px){
.home {
margin: auto;
width: 1400px;
}
.toggle-button{
margin-bottom: 10px;
}
}
/****** HOME PAGE ******/
This article was a bit long, but I hope it does cover a common use case with using hooks to manage state in our application.
本文有点长,但是我希望它确实涵盖了使用钩子在我们的应用程序中管理状态的常见用例。
You can access the GitHub repo by clicking this link. Note that the repo has some added features like creating a new song.
您可以通过单击此链接访问GitHub存储库。 请注意,回购具有一些新增功能,例如创建新歌曲。
翻译自: https://www.freecodecamp.org/news/state-management-with-react-hooks/
react上下文