react上下文_如何仅使用上下文和挂钩来管理React应用程序中的状态

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 to this.state and this.setState in class components)

    useState —该钩子允许我们在函数组件中使用状态(等效于类组件中的this.statethis.setState )

  • useContext — This hook takes in a context object and returns whatever is passed in as a value prop in MyContext.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 to useState 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.jsHome.jsLogin.jsCard.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;

Home.js

Home.js

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 (containing user and token ). It saves the user and token to localStorage and then returns a new state, setting isAuthenticated to true, and also sets the user and token keys to their respective values based on the action’s payload.

    LOGIN —调度此类型的操作时,还将与有效负载(包含usertoken )一起调度。 它将用户和令牌保存到localStorage,然后返回新状态,将isAuthenticated设置为true ,并根据操作的有效负载将usertoken键设置为其各自的值。

  • LOGOUT — When this action is dispatched, we clear localStorage of all data and set user and token to null .

    LOGOUT —调度此操作后,我们将清除所有数据的localStorage并将usertoken设置为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挂钩返回两个参数: statedispatchstate包含组件中使用的状态,并根据分派的操作进行更新。 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属性。 该对象包含statedispatch功能,以便可以由需要该上下文的任何其他组件使用。 然后,我们有条件地呈现组件-如果用户通过了身份验证,则呈现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 useStatehook. 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 ,该消息将显示在表单上。 为了调用调度,我们需要将AuthContextApp组件导入到我们的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组件中,当在大多数应用程序中处理网络请求时,我们尝试可视化三个主要状态。 首先,在处理请求时(通过使用某种加载程序),然后在请求成功时(通过呈现有效负载或显示成功通知),最后在请求失败时(通过显示错误通知)。 为了在安装组件时发出请求并同时处理这三种状态,我们将使用useEffectuseReducer钩子。

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用于表示加载状态,初始设置为falsehasError用于表示错误状态,并且最初也设置为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值设置为falsehasError设置为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钩子,并传入reducerinitialState ,然后返回两个变量,即statedispatch

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上下文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值