react hooks使用_如何使用React Hooks和Context API构建简单的PokémonWeb App

react hooks使用

After seven years of full stack development using Ruby, Python, and vanilla JavaScript, these days I mostly work with JavaScript, Typescript, React, and Redux.

在使用Ruby,Python和Vanilla JavaScript进行了七年的全栈开发之后,这些天我主要使用JavaScript,Typescript,React和Redux。

The JavaScript community is great and moves really fast. Tons of things are created "overnight", usually figuratively, but sometimes literally. All this makes it is really difficult to keep up to date.

JavaScript社区很棒,而且发展很快。 吨的事物通常是形象地“隔夜”创建的,但有时却是字面上的。 所有这一切使得保持最新状态确实非常困难。

I always feel like I'm late to the JavaScript party. And I want to be there, even though I don't really like parties.

我总是觉得我迟到了JavaScript聚会。 即使我不太喜欢聚会,我也想去那里。

Just one year of working with React and Redux and I felt like I needed to learn new things like Hooks and the Context API to manage state. After reading some articles about it I wanted to try these concepts, so I created a simple project as a laboratory to experiment with those things.

与React和Redux一起工作只有一年,我觉得我需要学习新知识,例如Hooks和Context API来管理状态。 阅读了一些有关它的文章后,我想尝试这些概念,因此我创建了一个简单的项目作为实验室来尝试这些东西。

Since I was a little boy, I've been passionate about Pokémon. It was always fun playing the games on the Game Boy and conquering all the Leagues. Now as a developer, I want to play around with the Pokémon API.

从小我就对神奇宝贝充满热情。 在Game Game上玩游戏并征服所有联赛总是很有趣。 现在,作为开发人员,我想使用PokémonAPI

I decided to build a simple web page where I could share data among different parts of the page. The page would have three main sections:

我决定构建一个简单的网页,在其中可以在页面的不同部分之间共享数据。 该页面将包含三个主要部分:

  • A box with a list of all existing pokémon

    一个包含所有现有神奇宝贝列表的盒子
  • A box with a list of all captured pokémon

    一个包含所有捕获的神奇宝贝列表的盒子
  • A box with input to add new pokémon to the list

    一个带有输入的框,用于将新的神奇宝贝添加到列表中

And each box would have the following behavior or actions:

并且每个框将具有以下行为或动作:

  • For each pokémon in the first box, I can capture them and send to the second box

    对于第一个盒子中的每个神奇宝贝,我都可以捕获它们并将其发送到第二个盒子中
  • For each pokémon in the second box, I can release them and send to the first box

    对于第二个盒子中的每个神奇宝贝,我可以将它们释放并发送到第一个盒子中
  • As a game god, I'm able to create pokémon by filling in the input and sending them to the first box

    作为游戏之神,我可以通过填写输入内容并将其发送到第一个框来创建神奇宝贝

So all the features I wanted to implement were clear – lists and actions.

因此,我想实现的所有功能都很清晰-列表和操作。

上市神奇宝贝 (Listing Pokémon)

The basic feature I wanted to build first was listing pokémon. So for an array of objects, I wanted to list and show the name attribute of each object.

我首先要构建的基本功能是列出神奇宝贝。 因此对于一个对象数组,我想列出并显示每个对象的name属性。

I started with the first box: the existing pokémon.

我从第一个盒子开始:现有的神奇宝贝。

At first I thought I don't need the Pokémon API – I could just mock the list and see if it works. With useState, I can declare my component state and use it.

起初我以为我不需要PokémonAPI-我可以只模拟列表,看看它是否有效。 通过useState ,我可以声明我的组件状态并使用它。

We define it with a default value of a mock pokémon list, just to test it:

我们将其定义为模拟神奇宝贝列表的默认值,只是为了对其进行测试:

const [pokemons] = useState([
  { id: 1, name: 'Bulbasaur' },
  { id: 2, name: 'Charmander' },
  { id: 3, name: 'Squirtle' }
]);

Here we have a list of three pokémon objects. The useState hook provides a pair of items: the current state and a function to let you update this created state.

在这里,我们列出了三个神奇宝贝对象。 useState挂钩提供了一对项目:当前状态和一个用于更新此创建状态的函数。

Now with the pokémon's state, we can map it and render the name of each one.

现在有了神奇宝贝的状态,我们可以将其映射并渲染每个名称。

{pokemons.map((pokemon) => <p>{pokemon.name}</p>)}

It is just a map returning each pokémon's name in a paragraph tag.

这只是一张地图,在段落标签中返回每个神奇宝贝的名字。

This is the whole component implemented:

这是实现的整个组件:

import React, { useState } from 'react';

const PokemonsList = () => {
  const [pokemons] = useState([
    { id: 1, name: 'Bulbasaur' },
    { id: 2, name: 'Charmander' },
    { id: 3, name: 'Squirtle' }
  ]);

  return (
    <div className="pokemons-list">
      <h2>Pokemons List</h2>
      
      {pokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <p>{pokemon.id}</p>
          <p>{pokemon.name}</p>
        </div>)}
    </div>
  )
}

export default PokemonsList;

Just a little tweak here:

只是在这里稍作调整:

  • I added the key in a combination of the pokémon's id and name

    我在神奇宝贝的idname的组合中添加了key

  • And I also rendered a paragraph for the id attribute (I was just testing it. But we will remove it later.)

    而且我还为id属性渲染了一个段落(我只是在测试它。但是我们稍后会删除它。)

Great! Now we have the first list up and running.

大! 现在,我们有了第一个列表并开始运行。

I want to make this same implementation but now for the captured pokémon. But for the captured pokémon, I first want to create an empty list because when the "game" starts, I won't have any captured pokémon, right? Right!

我想进行同样的实现,但现在要获取捕获的神奇宝贝。 但是对于捕获的神奇宝贝,我首先要创建一个空列表,因为当“游戏”开始时,我将不会捕获任何神奇宝贝,对吗? 对!

const [pokemons] = useState([]);

That's it, really simple!

就是这样,真的很简单!

The whole component looks similar to the other:

整个组件看起来与其他组件相似:

import React, { useState } from 'react';

const CapturedPokemons = () => {
  const [pokemons] = useState([]);

  return (
    <div className="pokedex">
      <h2>Captured Pokemons</h2>

      {pokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <p>{pokemon.id}</p>
          <p>{pokemon.name}</p>
        </div>)}
    </div>
  )
}

export default CapturedPokemons;

Here we use map, but as the array is empty, it doesn't render anything.

在这里,我们使用map ,但是由于数组为空,因此不呈现任何内容。

Now that I have the two main components, I can use them together in the App component:

现在,我有了两个主要组件,可以在App组件中一起使用它们:

import React from 'react';
import './App.css';

import PokemonsList from './PokemonsList';
import Pokedex from './Pokedex';

const App = () => (
  <div className="App">
    <PokemonsList />
    <Pokedex />
  </div>
);

export default App;

捕获和发布 (Capturing and Releasing)

This is the second part of our app where we can capture and release pokémon. So let's go over the expected behavior.

这是我们应用程序的第二部分,我们可以在其中捕获和释放神奇宝贝。 因此,让我们回顾一下预期的行为。

For each pokémon in the list of available pokémon, I want to enable an action to capture them. The capture action will remove them from the list where they were and add them to the list of captured pokémon.

对于可用的神奇宝贝列表中的每个神奇宝贝,我想启用一个动作来捕获它们。 捕获动作会将它们从它们所在的列表中删除,并将它们添加到捕获的神奇宝贝列表中。

The release action will have similar behavior. But instead of moving from the available list to the captured list, it will be the reverse. We will move them from the captured list to the available list.

释放动作将具有类似的行为。 但是,与其从可用列表移动到捕获列表,不如说是相反。 我们会将它们从捕获的列表移至可用列表。

So both boxes need to share data to be able to add pokémon to the other list. How do we do this as they are different components in the app? Let's talk about the React Context API.

因此,两个盒子都需要共享数据才能将神奇宝贝添加到另一个列表中。 由于它们是应用程序中的不同组件,我们该怎么做? 让我们谈谈React Context API。

The Context API was designed to make global data for a defined tree of React components. As the data is global, we can share it among components in this defined tree. So let's use it to share our simple Pokemon data between the two boxes.

Context API旨在为React组件的已定义树生成全局数据。 由于数据是全局数据,因此我们可以在定义的树中的各个组件之间共享数据。 因此,让我们用它在两个盒子之间共享我们的简单口袋妖怪数据。

Mental Note: "Context is primarily used when some data needs to be accessible by many components at different nesting levels." - React Docs.

提示:“上下文主要用于需要不同嵌套级别的许多组件访问某些数据的情况。” -React文件。

Using the API, we simply create a new context like this:

使用API​​,我们只需创建一个新的上下文,如下所示:

import { createContext } from 'react';

const PokemonContext = createContext();

Now, with the PokemonContext, we can use its provider. It will work as a component wrapper of a tree of components. It provides global data to these components and enables them to subscribe to any changes related to this context. It looks like this:

现在,通过PokemonContext ,我们可以使用其提供程序。 它将用作组件树的组件包装。 它为这些组件提供全局数据,并使它们能够订阅与此上下文相关的任何更改。 看起来像这样:

<PokemonContext.Provider value={/* some value */}>

The value prop is just a value that this context provides the wrapped components. What should we provide to the available and the captured lists?

value属性仅是此上下文提供包装组件的值。 我们应该为可用列表和捕获列表提供什么?

  • pokemons: to list in the available list

    pokemons :在可用列表中列出

  • capturedPokemons: to list in the captured list

    capturedPokemons :在捕获列表中列出

  • setPokemons: to be able to update the available list

    setPokemons :能够更新可用列表

  • setCapturedPokemons: to be able to update the captured list

    setCapturedPokemons :能够更新捕获的列表

As I mentioned before in the useState part, this hook always provides a pair: the state and a function to update this state. This function handles and updates the context state. In other words, they are the setPokemons and setCapturedPokemons. How?

正如我之前在useState部分中提到的useState ,此挂钩始终提供一对:状态和用于更新此状态的函数。 此函数处理并更新上下文状态。 换句话说,它们是setPokemonssetCapturedPokemons 。 怎么样?

const [pokemons, setPokemons] = useState([
  { id: 1, name: 'Bulbasaur' },
  { id: 2, name: 'Charmander' },
  { id: 3, name: 'Squirtle' }
]);

Now we have the setPokemons.

现在我们有了setPokemons

const [capturedPokemons, setCapturedPokemons] = useState([]);

And now we also have the setCapturedPokemons.

现在我们还有setCapturedPokemons

With all these values in hand, we can now pass them to the provider's value prop.

有了所有这些价值,我们现在可以将它们传递给提供者的valueStruts。

import React, { createContext, useState } from 'react';

export const PokemonContext = createContext();

export const PokemonProvider = (props) => {
  const [pokemons, setPokemons] = useState([
    { id: 1, name: 'Bulbasaur' },
    { id: 2, name: 'Charmander' },
    { id: 3, name: 'Squirtle' }
  ]);

  const [capturedPokemons, setCapturedPokemons] = useState([]);

  const providerValue = {
    pokemons,
    setPokemons,
    capturedPokemons,
    setCapturedPokemons
  };

  return (
    <PokemonContext.Provider value={providerValue}>
      {props.children}
    </PokemonContext.Provider>
  )
};

I created a PokemonProvider to wrap all this data and the APIs to create the context and return the context provider with the defined value.

我创建了一个PokemonProvider来包装所有这些数据,并创建了API以创建上下文并返回具有定义值的上下文提供程序。

But how do we provide all this data and APIs to the component? We need to do two main things:

但是,我们如何为组件提供所有这些数据和API? 我们需要做两件事:

  • Wrap the components into this context provider

    将组件包装到此上下文提供程序中
  • Use the context in each component

    在每个组件中使用上下文

Let's wrap them first:

让我们先包装它们:

const App = () => (
  <PokemonProvider>
    <div className="App">
      <PokemonsList />
      <Pokedex />
    </div>
  </PokemonProvider>
);

And we use the context by using the useContext and passing the created PokemonContext. Like this:

然后,通过使用useContext并传递创建的PokemonContext来使用上下文。 像这样:

import { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

useContext(PokemonContext); // returns the context provider value we created

We want to be able to catch the available pokémon, so it would be useful to have the setCapturedPokemons function API update the captured pokémon.

我们希望能够捕获可用的神奇宝贝,因此让setCapturedPokemons函数API更新捕获的神奇宝贝会很有用。

As each pokémon is captured, we need to remove it from the available list. setPokemons is also needed here. And to update each list, we need the current data. So basically we need everything from the context provider.

捕获每个神奇宝贝后,我们需要将其从可用列表中删除。 在这里也需要setPokemons 。 要更新每个列表,我们需要当前数据。 因此,基本上,我们需要上下文提供者提供的一切。

We need to build a button with an action to capture the pokémon:

我们需要构建一个带有捕获神奇宝贝的动作的按钮:

  • <button> tag with an onClick calling the capture function and passing the pokémon

    带有onClick <button>标记,调用capture函数并传递神奇宝贝

<button onClick={capture(pokemon)}>+</button>
  • The capture function will update the pokemons and the capturedPokemons lists

    capture功能将更新pokemons和已capturedPokemonspokemons列表

const capture = (pokemon) => (event) => {
  // update captured pokemons list
  // update available pokemons list
};

To update the capturedPokemons, we can just call the setCapturedPokemons function with the current capturedPokemons and the pokémon to be captured.

要更新capturedPokemons ,我们可以直接调用setCapturedPokemons当前功能capturedPokemons和神奇宝贝被捕获。

setCapturedPokemons([...capturedPokemons, pokemon]);

And to update the pokemons list, just filter the pokémon that will be captured.

而更新pokemons列表,只是过滤将捕获的神奇宝贝。

setPokemons(removePokemonFromList(pokemon));

removePokemonFromList is just a simple function to filter the pokémon by removing the captured pokémon.

removePokemonFromList只是一个简单的函数,可以通过删除捕获的神奇宝贝来过滤神奇宝贝。

const removePokemonFromList = (removedPokemon) =>
  pokemons.filter((pokemon) => pokemon !== removedPokemon)

How does the component look now?

现在该组件的外观如何?

import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

export const PokemonsList = () => {
  const {
    pokemons,
    setPokemons,
    capturedPokemons,
    setCapturedPokemons
  } = useContext(PokemonContext);

  const removePokemonFromList = (removedPokemon) =>
    pokemons.filter(pokemon => pokemon !== removedPokemon);

  const capture = (pokemon) => () => {
    setCapturedPokemons([...capturedPokemons, pokemon]);
    setPokemons(removePokemonFromList(pokemon));
  };

  return (
    <div className="pokemons-list">
      <h2>Pokemons List</h2>
      
      {pokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <div>
            <span>{pokemon.name}</span>
            <button onClick={capture(pokemon)}>+</button>
          </div>
        </div>)}
    </div>
  );
};

export default PokemonsList;

It will look very similar to the captured pokémon component. Instead of capture, it will be a release function:

它看起来与捕获的神奇宝贝组件非常相似。 而不是capture ,它将是一个release函数:

import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

const CapturedPokemons = () => {
  const {
    pokemons,
    setPokemons,
    capturedPokemons,
    setCapturedPokemons,
  } = useContext(PokemonContext);

  const releasePokemon = (releasedPokemon) =>
    capturedPokemons.filter((pokemon) => pokemon !== releasedPokemon);

  const release = (pokemon) => () => {
    setCapturedPokemons(releasePokemon(pokemon));
    setPokemons([...pokemons, pokemon]);
  };

  return (
    <div className="captured-pokemons">
      <h2>CapturedPokemons</h2>

      {capturedPokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <div>
            <span>{pokemon.name}</span>
            <button onClick={release(pokemon)}>-</button>
          </div>
        </div>)}
    </div>
  );
};

export default CapturedPokemons;

降低复杂度 (Reducing complexity)

Now we use the useState hook, the Context API, and the context provider useContext. And more importantly, we can share data between pokémon boxes.

现在,我们使用useState挂钩,Context API和上下文提供程序useContext 。 更重要的是,我们可以在神奇宝贝盒子之间共享数据。

Another way to manage the state is by using useReducer as an alternative to useState.

管理状态的另一种方法是使用useReducer作为useReducer的替代useState

The reducer lifecycle works like this: useReducer provides a dispatch function. With this function, we can dispatch an action inside a component. The reducer receives the action and the state. It understands the type of action, handles the data, and return a new state. Now, the new state can be used in the component.

reducer的生命周期如下所示: useReducer提供了一个dispatch功能。 使用此功能,我们可以在组件内部调度actionreducer接收动作和状态。 它了解操作的类型,处理数据并返回新状态。 现在,可以在组件中使用新状态。

As an exercise and to have a better understanding of this hook, I tried to replace useState with it.

为了更好地理解此钩子,我尝试用它代替useState

useState was inside the PokemonProvider. We can redefine the initial state for the available and the captured pokémon in this data structure:

useStatePokemonProvider内部。 我们可以在此数据结构中重新定义可用的和捕获的神奇宝贝的初始状态:

const defaultState = {
  pokemons: [
    { id: 1, name: 'Bulbasaur' },
    { id: 2, name: 'Charmander' },
    { id: 3, name: 'Squirtle' }
  ],
  capturedPokemons: []
};

And pass this value to useReducer:

并将此值传递给useReducer

const [state, dispatch] = useReducer(pokemonReducer, defaultState);

useReducer receives two parameters: the reducer and the initial state. Let's build the pokemonReducer now.

useReducer接收两个参数:reducer和初始状态。 让我们现在构建pokemonReducer

The reducer receives the current state and the action that was dispatched.

减速器接收当前状态和已调度的动作。

const pokemonReducer = (state, action) => // returns the new state based on the action type

Here we get the action type and return a new state. The action is an object. It looks like this:

在这里,我们获得动作类型并返回一个新状态。 动作是一个对象。 看起来像这样:

{ type: 'AN_ACTION_TYPE' }

But could also be bigger:

但也可以更大:

{
  type: 'AN_ACTION_TYPE',
  pokemon: {
    name: 'Pikachu'
  }
}

In this case, we'll pass a pokémon to the action object. Let's pause for a minute and think about what we want to do inside the reducer.

在这种情况下,我们会将一个神奇宝贝传递给动作对象。 让我们暂停片刻,想一想我们在减速器内部要做什么。

Here, we usually update data and handle actions. Actions are dispatched, so actions are behavior. And the behaviors from our app are capture and release! These are the actions we need to handle here.

在这里,我们通常会更新数据并处理操作。 调度动作,所以动作就是行为。 从我们的应用程序的行为是捕获释放 ! 这些是我们需要在此处处理的动作。

This is what our reducer will look like:

这是我们的减速器的外观:

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case 'CAPTURE':
      // handle capture and return new state
    case 'RELEASE':
      // handle release and return new state
    default:
      return state;
  }
};

If our action type is CAPTURE, we handle it in one way. If our action type is RELEASE, we handle it another way. If the action type doesn't match any of these types, just return the current state.

如果我们的操作类型是CAPTURE ,那么我们将以一种方式处理它。 如果我们的操作类型是RELEASE ,那么我们将以另一种方式处理它。 如果操作类型与这些类型都不匹配,则只需返回当前状态即可。

When we capture the pokémon, we need to update both lists: remove the pokémon from the available list and add it to the captured list. This state is what we need to return from the reducer.

捕获神奇宝贝时,我们需要更新两个列表:从可用列表中删除神奇宝贝并将其添加到捕获的列表中。 此状态是我们需要从减速器返回的状态。

const getPokemonsList = (pokemons, capturedPokemon) =>
  pokemons.filter(pokemon => pokemon !== capturedPokemon)

const capturePokemon = (pokemon, state) => ({
  pokemons: getPokemonsList(state.pokemons, pokemon),
  capturedPokemons: [...state.capturedPokemons, pokemon]
});

The capturePokemon function just returns the updated lists. The getPokemonsList removes the captured pokémon from the available list.

capturePokemon函数仅返回更新的列表。 getPokemonsList从可用列表中删除捕获的神奇宝贝。

And we use this new function in the reducer:

我们在化简器中使用此新功能:

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case 'CAPTURE':
      return capturePokemon(action.pokemon, state);
    case 'RELEASE':
      // handle release and return new state
    default:
      return state;
  }
};

Now the release function!

现在release功能!

const getCapturedPokemons = (capturedPokemons, releasedPokemon) =>
  capturedPokemons.filter(pokemon => pokemon !== releasedPokemon)

const releasePokemon = (releasedPokemon, state) => ({
  pokemons: [...state.pokemons, releasedPokemon],
  capturedPokemons: getCapturedPokemons(state.capturedPokemons, releasedPokemon)
});

The getCapturedPokemons remove the released pokémon from the captured list. The releasePokemon function returns the updated lists.

getCapturedPokemons从捕获的列表中删除已发布的神奇宝贝。 releasePokemon函数返回更新的列表。

Our reducer looks like this now:

我们的减速器现在看起来像这样:

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case 'CAPTURE':
      return capturePokemon(action.pokemon, state);
    case 'RELEASE':
      return releasePokemon(action.pokemon, state);
    default:
      return state;
  }
};

Just one minor refactor: action types! These are strings and we can extract them into a constant and provide for the dispatcher.

只是一个小的重构:动作类型! 这些是字符串,我们可以将它们提取为常量并提供给调度程序。

export const CAPTURE = 'CAPTURE';
export const RELEASE = 'RELEASE';

And the reducer:

减速器:

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case CAPTURE:
      return capturePokemon(action.pokemon, state);
    case RELEASE:
      return releasePokemon(action.pokemon, state);
    default:
      return state;
  }
};

The entire reducer file looks like this:

整个reducer文件如下所示:

export const CAPTURE = 'CAPTURE';
export const RELEASE = 'RELEASE';

const getCapturedPokemons = (capturedPokemons, releasedPokemon) =>
  capturedPokemons.filter(pokemon => pokemon !== releasedPokemon)

const releasePokemon = (releasedPokemon, state) => ({
  pokemons: [...state.pokemons, releasedPokemon],
  capturedPokemons: getCapturedPokemons(state.capturedPokemons, releasedPokemon)
});

const getPokemonsList = (pokemons, capturedPokemon) =>
  pokemons.filter(pokemon => pokemon !== capturedPokemon)

const capturePokemon = (pokemon, state) => ({
  pokemons: getPokemonsList(state.pokemons, pokemon),
  capturedPokemons: [...state.capturedPokemons, pokemon]
});

export const pokemonReducer = (state, action) => {
  switch (action.type) {
    case CAPTURE:
      return capturePokemon(action.pokemon, state);
    case RELEASE:
      return releasePokemon(action.pokemon, state);
    default:
      return state;
  }
};

As the reducer is now implemented, we can import it into our provider and use it in the useReducer hook.

现在实现了reducer,我们可以将其导入到我们的提供程序中,并在useReducer挂钩中使用它。

const [state, dispatch] = useReducer(pokemonReducer, defaultState);

As we are inside the PokemonProvider, we want to provide some value to the consuming components: the capture and release actions.

当我们在PokemonProvider内部时,我们希望为使用中的组件提供一些价值:捕获和释放操作。

These functions just need to dispatch the correct action type and pass the pokémon to the reducer.

这些功能只需要调度正确的动作类型,并将神奇宝贝传递给减速器。

  • The capture function: it receives the pokémon and returns a new function that dispatches an action with the type CAPTURE and the captured pokémon.

    capture函数:它接收神奇宝贝并返回一个新函数,该函数以CAPTURE类型和捕获的神奇宝贝调度一个动作。

const capture = (pokemon) => () => {
  dispatch({ type: CAPTURE, pokemon });
};
  • The release function: it receives the pokémon and returns a new function that dispatches an action with the type RELEASE and the released pokémon.

    release函数:它接收神奇宝贝并返回一个新函数,该函数以RELEASE类型和释放的神奇宝贝调度一个动作。

const release = (pokemon) => () => {
  dispatch({ type: RELEASE, pokemon });
};

Now with the state and the actions implemented, we can provide these values to the consuming components. Just update the provider value prop.

现在,随着状态和操作的实施,我们可以将这些值提供给使用组件。 只需更新提供者价值道具即可。

const { pokemons, capturedPokemons } = state;

const providerValue = {
  pokemons,
  capturedPokemons,
  release,
  capture
};

<PokemonContext.Provider value={providerValue}>
  {props.children}
</PokemonContext.Provider>

Great! Now back to the component. Let's use these new actions. All the capture and release logics are encapsulated in our provider and reducer. Our component is pretty clean now. The useContext will look like this:

大! 现在回到组件。 让我们使用这些新动作。 所有捕获和释放逻辑都封装在我们的提供程序和reducer中。 我们的组件现在很干净。 useContext将如下所示:

const { pokemons, capture } = useContext(PokemonContext);

And the whole component:

以及整个组件:

import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

const PokemonsList = () => {
  const { pokemons, capture } = useContext(PokemonContext);

  return (
    <div className="pokemons-list">
      <h2>Pokemons List</h2>
      
      {pokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <span>{pokemon.name}</span>
          <button onClick={capture(pokemon)}>+</button>
        </div>)}
    </div>
  )
};

export default PokemonsList;

For the captured pokémon component, it will look the very similar to the useContext:

对于捕获的神奇宝贝组件,它看起来与useContext非常相似:

const { capturedPokemons, release } = useContext(PokemonContext);

And the whole component:

以及整个组件:

import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

const Pokedex = () => {
  const { capturedPokemons, release } = useContext(PokemonContext);

  return (
    <div className="pokedex">
      <h2>Pokedex</h2>

      {capturedPokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <span>{pokemon.name}</span>
          <button onClick={release(pokemon)}>-</button>
        </div>)}
    </div>
  )
};

export default Pokedex;

No logic. Just UI. Very clean.

没有逻辑。 只是UI。 很干净。

神奇宝贝神–创造者 (Pokémon God – The Creator)

Now that we have the communication between the two lists, I want to build a third box. This will how we create new pokémon. But it is just a simple input and submit button.

现在我们可以在两个列表之间进行通信了,我想建立第三个框。 这将是我们创建新神奇宝贝的方式。 但这只是一个简单的输入和提交按钮。

When we add a pokémon's name into the input and press the button, it will dispatch an action to add this pokémon to the available list.

当我们在输入中添加一个神奇宝贝的名字并按下按钮时,它将分派一个动作来将该神奇宝贝添加到可用列表中。

As we need to access the available list to update it, we need to share the state. So our component will be wrapped by our PokemonProvider together with the other components.

由于我们需要访问可用列表以进行更新,因此我们需要共享状态。 因此,我们的组件将由PokemonProvider与其他组件一起包装。

const App = () => (
  <PokemonProvider>
    <div className="main">
      <PokemonsList />
      <Pokedex />
    </div>
    <PokemonForm />
  </PokemonProvider>
);

Let's build the PokemonForm component now. The form is pretty straightforward:

让我们现在构建PokemonForm组件。 表单非常简单:

<form onSubmit={handleFormSubmit}>
  <input type="text" placeholder="pokemon name" onChange={handleNameOnChange} />
  <input type="submit" value="Add" />
</form>

We have a form, an input, and a button. To sum up, we also have a function to handle the form submit and another function to handle the input on change.

我们有一个表单,一个输入和一个按钮。 总结起来,我们还有一个函数来处理表单提交,还有一个函数来处理更改时的输入。

The handleNameOnChange will be called every time the user types or removes a character. I wanted to build a local state, a representation of the pokemon name. With this state, we can use it to dispatch when submitting the form.

每当用户键入或删除字符时,都会调用handleNameOnChange 。 我想建立一个地方状态,代表宠物小精灵的名字。 在这种状态下,我们可以在提交表单时使用它进行分派。

As we want to try hooks, we will use useState to handle this local state.

当我们想尝试钩子时,我们将使用useState来处理此本地状态。

const [pokemonName, setPokemonName] = useState();

const handleNameOnChange = (e) => setPokemonName(e.target.value);

We use the setPokemonName to update the pokemonName every time the user interacts with the input.

每当用户与输入进行交互时,我们使用setPokemonName更新pokemonName

And the handleFormSubmit is a function to dispatch the new pokémon to be added to the available list.

handleFormSubmit是一个函数,用于分派要添加到可用列表中的新神奇宝贝。

const handleFormSubmit = (e) => {
  e.preventDefault();
  addPokemon({
    id: generateID(),
    name: pokemonName
  });
};

addPokemon is the API we will build later. It receives the pokémon's id and name. The name is the local state we defined, pokemonName.

addPokemon是我们稍后将构建的API。 它会收到神奇宝贝的ID和名称。 名称是我们定义的本地状态pokemonName

generateID is just a simple function I built to generate a random number. It looks like this:

generateID只是我构建的用于生成随机数的简单函数。 看起来像这样:

export const generateID = () => {
  const a = Math
    .random()
    .toString(36)
    .substring(2, 15)

  const b = Math
    .random()
    .toString(36)
    .substring(2, 15)

  return a + b;
};

addPokemon will be provided by the context API we build. That way, this function can receive the new pokémon and add to the available list. It looks like this:

addPokemon将由我们构建的上下文API提供。 这样,此功能可以接收新的神奇宝贝并添加到可用列表中。 看起来像这样:

const addPokemon = (pokemon) => {
  dispatch({ type: ADD_POKEMON, pokemon });
};

It will dispatch this action type ADD_POKEMON and also pass the pokémon.

它将分派此动作类型ADD_POKEMON并通过神奇宝贝。

In our reducer, we add the case for the ADD_POKEMON and handle the state to add the new pokémon to state.

在我们的reducer中,我们添加ADD_POKEMON的大小写,并处理状态以将新的神奇宝贝添加到state。

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case CAPTURE:
      return capturePokemon(action.pokemon, state);
    case RELEASE:
      return releasePokemon(action.pokemon, state);
    case ADD_POKEMON:
      return addPokemon(action.pokemon, state);
    default:
      return state;
  }
};

And the addPokemon function will be:

并且addPokemon函数将是:

const addPokemon = (pokemon, state) => ({
  pokemons: [...state.pokemons, pokemon],
  capturedPokemons: state.capturedPokemons
});

Another approach is to destructure the state and change only the pokémon's attribute, like this:

另一种方法是破坏状态并仅更改神奇宝贝的属性,如下所示:

const addPokemon = (pokemon, state) => ({
  ...state,
  pokemons: [...state.pokemons, pokemon],
});

Back to our component, we just need to make sure the useContext provides the addPokemon dispatch API based on the PokemonContext:

回到我们的组件,我们只需要确保useContext提供addPokemon基于调度API PokemonContext

const { addPokemon } = useContext(PokemonContext);

And the whole component looks like this:

整个组件如下所示:

import React, { useContext, useState } from 'react';
import { PokemonContext } from './PokemonContext';
import { generateID } from './utils';

const PokemonForm = () => {
  const [pokemonName, setPokemonName] = useState();
  const { addPokemon } = useContext(PokemonContext);

  const handleNameOnChange = (e) => setPokemonName(e.target.value);

  const handleFormSubmit = (e) => {
    e.preventDefault();
    addPokemon({
      id: generateID(),
      name: pokemonName
    });
  };

  return (
    <form onSubmit={handleFormSubmit}>
      <input type="text" placeholder="pokemon name" onChange={handleNameOnChange} />
      <input type="submit" value="Add" />
    </form>
  );
};

export default PokemonForm;

Now we have the available pokémon list, the captured pokémon list, and the third box to create new pokémon.

现在,我们有了可用的神奇宝贝列表,捕获的神奇宝贝列表以及创建新神奇宝贝的第三个框。

神奇宝贝效果 (Pokémon Effects)

Now that we have our app almost complete, we can replace the mocked pokémon list with a list of pokémon from the PokéAPI.

现在我们的应用程序已接近完成,我们可以用PokéAPI中的神奇宝贝列表替换模拟的神奇宝贝列表。

So, inside the function component, we can't do any side effects like logging or subscriptions. This is why the useEffect hook exists. With this hook, we can fetch pokémon (a side-effect), and add to the list.

因此,在功能组件内部,我们不能做任何副作用,例如日志记录或订阅。 这就是为什么useEffect挂钩存在的原因。 使用此钩子,我们可以获取pokémon(副作用),并将其添加到列表中。

Fetching from the PokéAPI looks like this:

从PokéAPI获取如下所示:

const url = "https://pokeapi.co/api/v2/pokemon";
const response = await fetch(url);
const data = await response.json();
data.results; // [{ name: 'bulbasaur', url: 'https://pokeapi.co/api/v2/pokemon/1/' }, ...]

The results attribute is the list of fetched pokémon. With this data, we will be able to add them to the pokémon list.

results属性是获取的神奇宝贝的列表。 有了这些数据,我们就能将它们添加到神奇宝贝列表中。

Let's get the request code inside useEffect:

让我们在useEffect获取请求代码:

useEffect(() => {
  const fetchPokemons = async () => {
    const response = await fetch(url);
    const data = await response.json();
    data.results; // update the pokemons list with this data
  };

  fetchPokemons();
}, []);

To be able to use async-await, we need to create a function and call it later. The empty array is a parameter to make sure useEffect knows the dependencies it will look up to re-run.

为了能够使用async-await ,我们需要创建一个函数并在以后调用它。 空数组是一个参数,以确保useEffect知道它将重新运行的依赖项。

The default behavior is to run the effect of every completed render. If we add a dependency to this list, useEffect will only re-run when the dependency changes, instead of running in all completed renders.

默认行为是运行每个完成的渲染的效果。 如果将依赖项添加到此列表中,则useEffect仅在依赖项更改时重新运行,而不是在所有完成的渲染中运行。

Now that we've fetched the pokémon, we need to update the list. It's an action, a new behavior. We need to use the dispatch again, implement a new type in the reducer, and update the state in the context provider.

现在我们已经获取了神奇宝贝,我们需要更新列表。 这是一个动作,一种新的行为。 我们需要再次使用分派,在化简器中实现新类型,并在上下文提供程序中更新状态。

In PokemonContext, we created the addPokemons function to provide an API to the consuming component using it.

PokemonContext ,我们创建了addPokemons函数,以向使用它的使用组件提供API。

const addPokemons = (pokemons) => {
  dispatch({ type: ADD_POKEMONS, pokemons });
};

It receives pokémon and dispatches a new action: ADD_POKEMONS.

它接收神奇宝贝并调度一个新动作: ADD_POKEMONS

In the reducer, we add this new type, expect the pokémon, and call a function to add the pokémon to the available list state.

在化简器中,我们添加这种新类型,期待神奇宝贝,然后调用一个函数将神奇宝贝添加到可用列表状态。

const pokemonReducer = (state, action) => {
  switch (action.type) {
    case CAPTURE:
      return capturePokemon(action.pokemon, state);
    case RELEASE:
      return releasePokemon(action.pokemon, state);
    case ADD_POKEMON:
      return addPokemon(action.pokemon, state);
    case ADD_POKEMONS:
      return addPokemons(action.pokemons, state);
    default:
      return state;
  }
};

The addPokemons function just adds the pokémon to the list:

addPokemons函数只是将神奇宝贝添加到列表中:

const addPokemons = (pokemons, state) => ({
  pokemons: pokemons,
  capturedPokemons: state.capturedPokemons
});

We can refactor this by using state destructuring and the object property value shorthand:

我们可以使用状态分解和对象属性值的简写来重构它:

const addPokemons = (pokemons, state) => ({
  ...state,
  pokemons,
});

As we provide this function API to the consuming component now, we can use the useContext to get it.

当我们现在向消费组件提供此功能API时,我们可以使用useContext来获取它。

const { addPokemons } = useContext(PokemonContext);

The whole component looks like this:

整个组件如下所示:

import React, { useContext, useEffect } from 'react';
import { PokemonContext } from './PokemonContext';

const url = "https://pokeapi.co/api/v2/pokemon";

export const PokemonsList = () => {
  const { state, capture, addPokemons } = useContext(PokemonContext);

  useEffect(() => {
    const fetchPokemons = async () => {
      const response = await fetch(url);
      const data = await response.json();
      addPokemons(data.results);
    };    

    fetchPokemons();
  }, [addPokemons]);

  return (
    <div className="pokemons-list">
      <h2>Pokemons List</h2>

      {state.pokemons.map((pokemon) =>
        <div key={pokemon.name}>
          <div>
            <span>{pokemon.name}</span>
            <button onClick={capture(pokemon)}>+</button>
          </div>
        </div>)}
    </div>
  );
};

export default PokemonsList;

结语 (Wrapping up)

This was my attempt to share what I learned while trying to use hooks in a mini side project.

这是我尝试分享在迷你辅助项目中尝试使用钩子时学到的知识。

We learned how to handle local state with useState, building a global state with the Context API, how to rewrite and replace useState with useReducer, and how to do side-effects within useEffect.

我们学习了如何使用useState处理局部状态,如何使用Context API构建全局状态,如何使用useReducer重写和替换useState以及如何在useEffect产生副作用。

Disclaimer: this was just an experimental project for learning purposes. I may not have used best practices for hooks or made them scalable for big projects.

免责声明:这只是出于学习目的的实验项目。 我可能没有对钩子使用最佳实践,也没有使它们可用于大型项目。

I hope this was good reading! Keep learning and coding!

我希望这是一本好书! 继续学习和编码!

You can other articles like this on my blog.

您可以像这样的其他文章 在我的博客上

My Twitter and Github.

我的TwitterGithub

资源资源 (Resources)

翻译自: https://www.freecodecamp.org/news/building-a-simple-pokemon-web-app-with-react-hooks-and-context-api/

react hooks使用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值