创建react应用程序_构建一个React应用程序:第二部分

创建react应用程序

In the first part of this tutorial, we built a basic React application with Create React App and covered some core concepts like components, hooks, and routing. With that knowledge under our belt, we’re ready to explore more advanced topics, like handling asynchronous calls to load data, managing global application state with Redux, and moving business logic out of components and into actions, reducers, and epics. It’s about to get wiiiiiild!

在本教程的第一部分中 ,我们使用Create React App构建了一个基本的React应用程序,并涵盖了一些核心概念,例如组件,钩子和路由。 有了这些知识之后,我们准备探索更高级的主题,例如处理异步调用以加载数据,使用Redux管理全局应用程序状态以及将业务逻辑移出组件,再移至操作,Reducer和Epic。 快要开始了!

As a refresher, below is a diagram of how a typical React application might be structured.

作为一个复习,下面是一个典型的React应用程序可能是如何构造的图。

Anatomy of a React Application React应用程序剖析

The first part of this tutorial covered the basics for the “Presentation” layer of the application. In this tutorial, we’re going to delve into the “Business Logic” and “Data” layers. But don’t worry, it’s not as boring as it sounds.

本教程的第一部分介绍了应用程序“演示”层的基础。 在本教程中,我们将深入研究“业务逻辑”和“数据”层。 但是不用担心,它并不像听起来那样无聊。

To fully understand this tutorial, you’ll want to be familiar with the concepts covered in the previous tutorial.

为了完全理解本教程,您需要熟悉上一教程中介绍的概念。

系统要求 (System requirements)

As before, to build this application, you’re going to need a recent version of Node.js installed. I recommend using a tool like Node Version Manager (nvm) which allows you to install different versions of Node in parallel and switch between them. There are versions of nvm for both MacOS (or Linux) and Windows.

和以前一样,要构建此应用程序,您将需要安装最新版本的Node.js。 我建议使用诸如Node Version Manager(nvm)之类的工具,该工具可让您并行安装不同版本的Node并在它们之间切换。 有适用于MacOS(或Linux)和Windows的nvm版本。

入门 (Getting started)

We’re going to be building on the Spider game we started in the previous tutorial. If you haven’t already built the previous project and just want to jump in here, you can grab the project code from GitHub and checkout the part-2 branch. Make sure your dependencies are up-to-date, then fire up the development server.

我们将建立在上一个教程中开始的Spider游戏上。 如果您尚未构建上一个项目,而只是想跳到这里,则可以从GitHub上获取项目代码并检出part-2分支。 确保您的依赖关系是最新的,然后启动开发服务器。

yarn install
yarn start

从API提取数据 (Fetching data from an API)

Up to this point, our application has been completely self-contained. In the real world though, you’re probably going to need to interact with a backend through some sort of RESTful API. So what would that look like in our Spider game?

到目前为止,我们的应用程序是完全独立的。 在现实世界中,您可能需要通过某种RESTful API与后端进行交互。 那么在我们的蜘蛛游戏中会是什么样?

How about we add some functionality to check whether or not the word/phrase entered by the user contains valid words? That seems like it could be useful. To accomplish this we’re going to use the Merriam-Webster Collegiate Dictionary API. You’ll need an API key to access the API, which you can get if you sign up for a free account.

我们如何添加一些功能来检查用户输入的单词/短语是否包含有效单词? 这似乎很有用。 为此,我们将使用Merriam-Webster大学词典API 。 您需要一个API密钥才能访问该API,如果您注册了免费帐户则可以获取该API密钥。

Once you’ve gotten an API key, let’s create a service for making requests to the API. For those coming from the Angular world, you may think of a service as a class that encompasses some narrow, well-defined functionality. For our purposes though, let’s think of a service as a stateless module that encompasses some narrow, well-defined functionality. That probably sounds like pretty much the same thing. The difference is that our service does not maintain any internal state — it simply exports stateless functions.

获取API密钥后,让我们创建一个服务以向API发出请求。 对于那些来自Angular世界的人来说,您可能会认为服务是一个包含一些狭窄的,定义明确的功能的类。 但是出于我们的目的,让我们将服务视为包含一些狭窄的,定义明确的功能的无状态模块。 这听起来似乎差不多。 不同之处在于我们的服务不维护任何内部状态-它仅导出无状态功能。

Create a file at src/services/dictionaryApi.ts. This file will be straight TypeScript with no JSX, so we can use the .ts extension. We’re going to export a single lookupWord function, that uses the Fetch API to make an HTTP request to the API and return the response.

src/services/dictionaryApi.ts创建一个文件。 该文件将是没有JSX的纯正TypeScript,因此我们可以使用.ts扩展名。 我们将导出一个单独的lookupWord函数,该函数使用Fetch API向该API发出HTTP请求并返回响应。

const API_KEY = process.env.REACT_APP_DICTIONARY_API_KEY;
const BASE_URL = process.env.REACT_APP_DICTIONARY_API_URL;


type DictionaryItem = {
	meta?: { stems: string[] };
};


export async function lookupWord(word: string) {
	const response = await fetch(
		`${BASE_URL}/${encodeURIComponent(word)}?key=${API_KEY}`
	);
	if (!response.ok) {
		throw response;
	}
	const json = await response.json();
	return json as DictionaryItem[];
}

You’ll notice we used async/await, which is really just syntactic sugar around Promises. You may have also noticed some weird, Node-like syntax to access some configuration from environment variables:

您会注意到我们使用了async / await ,它实际上只是Promises的语法糖。 您可能还注意到一些奇怪的,类似于Node的语法,可以从环境变量访问某些配置:

const API_KEY = process.env.REACT_APP_DICTIONARY_API_KEY;
const BASE_URL = process.env.REACT_APP_DICTIONARY_API_URL;

This is special functionality brought to you by React Scripts. It allows you to access environment variables, defined on the command line or in a .env file in the root of your project. Any environment variable that is prefixed with REACT_APP_ can be accessed this way, as well as the NODE_ENV variable which is defined by default. But keep in mind that the values are injected into the code at build-time, not at runtime.

这是React Scripts为您带来的特殊功能 。 它允许您访问环境变量,该变量在命令行中或项目根目录中的.env文件中定义。 可以使用这种方式访问​​任何以REACT_APP_为前缀的环境变量,以及默认情况下定义的NODE_ENV变量。 但是请记住,这些值是在构建时而不是在运行时注入到代码中的。

Create a .env file in the root directory of the project, so we can define our API base URL and key.

在项目的根目录中创建一个.env文件,以便我们可以定义我们的API基本URL和密钥。

REACT_APP_DICTIONARY_API_KEY=YOUR_KEY_HERE
REACT_APP_DICTIONARY_API_URL=https://www.dictionaryapi.com/api/v3/references/collegiate/json

Now that we have our service, we can use the lookupWord function in the handleSubmit function of our App component. Our approach will not be super sophisticated. Basically, we’ll split the text up into individual words and lookup each word independently. Here’s a sample of the JSON response from dictionaryapi.com, if we were looking up the word “car”:

现在我们有了服务,我们可以在App组件的lookupWord函数中使用handleSubmit函数。 我们的方法不会非常复杂。 基本上,我们会将文本分成单个单词,然后独立查找每个单词。 如果我们查找“ car”一词,这是来自dictionaryapi.com的JSON响应示例:

[
    {
        "meta": {
            "stems": [
                "car",
		"cars"
            ],
            // ...
        },
       // ...
    },
    // ...
]

There’s a lot more data in the response than we’ll actually use. For our purposes, we just want to iterate over each entry in the array, and check if our word exists in thestems array defined in the meta object. That should do reasonably well at identifying valid words. And of course, we’ll want to store a flag in state letting us know if we find any invalid words.

响应中的数据比我们实际使用的要多。 为了我们的目的,我们只想遍历数组中的每个条目,并检查我们的单词是否存在于meta对象定义的stems数组中。 这在识别有效单词方面应该做得很好。 当然,我们希望在状态中存储一个标志,让我们知道是否找到无效的单词。

import React, { useState } from 'react';
import { Redirect, Route, Switch, useHistory } from 'react-router-dom';
import PlayContainer from './containers/PlayContainer';
import StartContainer from './containers/StartContainer';
import { lookupWord } from './services/dictionaryApi';


function App() {
	const history = useHistory();


	const [invalid, setInvalid] = useState(false);
	const [text, setText] = useState('');


	const handleSubmit = async (value: string) => {
		try {
			// Get list of unique words
			const words = Object.keys(
				// Split on white space to get individual words
				value.split(/\s/).reduce((obj, word) => {
					// Remove non-alpha characters at beginning or end of word
					const key = word
						.replace(/^[^\w]+|[^\w]+$/, '')
						.toLowerCase();
					return { ...obj, [key]: true };
				}, {} as { [key: string]: boolean })
			);
			// Make individual requests in parallel
			const lookups = words.map(word => {
				return lookupWord(word);
			});
			// Wait for all requests, then check validity of each word
			const responses = await Promise.all(lookups);
			const invalid = !responses.reduce((valid, results, index) => {
				return (
					valid &&
					results.some(({ meta }) => {
						return meta?.stems.includes(words[index]) || false;
					})
				);
			}, true);
			setInvalid(invalid);
		} catch (err) {
			// Don't want to prevent play if there is an API error, and don't
			// want to potentially show a false warning.
			console.error(err);
			setInvalid(false);
		} finally {
			setText(value.toUpperCase());
			history.push('/play');
		}
	};


	return (
		

Spider

); } export default App;

If you’re thinking that seems like a lot of logic to stuff into an event handler, you’re absolutely right. And we’ll address that a little later. For now though, let’s update the PlayContainer component to take our new invalid value as a prop and display a warning message to the user.

如果您认为在事件处理程序中充入大量逻辑,那是绝对正确的。 我们稍后再解决。 现在,让我们更新PlayContainer组件,以将新的invalid值作为prop并向用户显示警告消息。

// ...


type PlayContainerProps = {
	invalid?: boolean;
	text?: string;
};


function PlayContainer({ invalid, text }: PlayContainerProps) {


	// ...


	return (
		<>

			// ...


			{invalid && (
Text may contain invalid word(s).{' '}
)} // ... ); } export default PlayContainer; // ...

You may have noticed we’re creating a lot of similar divs with various messages. As an additional exercise, feel free to encapsulate this into a Callout component.

您可能已经注意到我们正在 使用各种消息 创建许多类似的 divs 作为附加练习,请随时将其封装到一个 Callout 组件中。

And now that the PlayContainer can accept an invalid prop, let’s update our App component to pass the value to it.

现在PlayContainer可以接受invalid道具,让我们更新App组件以将值传递给它。

// ...


function App() {


	// ...


	return (


		// ...


					
						
					


    		// ...


	);
}


export default App;

And just like that, we’ve added another layer from our application diagram!

就像这样,我们在应用程序图中添加了另一层!

带上Redux! (Bring on Redux!)

So our application is working pretty well at this point. But you may have noticed that our components are starting to do an awful lot. As our application grows, that can start to make it unwieldy and hard to maintain. Redux is a predictable state container that centralizes your application’s state and logic, helping you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. Centralizing state and logic sounds like exactly what we need!

因此,我们的应用程序此时运行良好。 但是您可能已经注意到,我们的组件开始做很多事情。 随着我们应用程序的增长,这可能会使其变得笨拙且难以维护。 Redux是一个可预测的状态容器,它集中了应用程序的状态和逻辑,可帮助您编写行为一致,在不同环境(客户端,服务器和本机)中运行且易于测试的应用程序。 集中状态和逻辑听起来正是我们所需要的!

The basic principles of Redux are simple, as explained in the Redux documentation:

Redux的基本原理很简单,如Redux文档中所述 :

The whole state of your app is stored in an object tree inside a single store. The only way to change the state tree is to emit an action, an object describing what happened. To specify how the actions transform the state tree, you write pure reducers.

应用程序的整个状态存储在单个商店内的对象树中 更改状态树的唯一方法是发出一个 action ,一个描述发生了什么的对象。 要指定动作如何转换状态树,您可以编写pure reducers

That’s it!

而已!

So the gist of it is this: Redux provides a single store, which houses your application state as a plain old JavaScript object. The store also provides you with a dispatcher, that allows you to dispatch actions to update your state (similar to how events are fired from components). Dispatched actions are run through a reducer function, which takes the current state object and the action as parameters, and returns a new state object that has been updated with the data from the action.

因此,要点是:Redux提供了一个单一存储,将您的应用程序状态存储为普通的旧JavaScript对象。 该商店还为您提供了一个调度程序,该调度程序允许您调度操作以更新状态(类似于从组件激发事件的方式)。 调度的动作通过reducer函数运行,该函数将当前状态对象和该动作作为参数,并返回一个新的状态对象,该状态对象已使用该动作中的数据进行了更新。

There are lots of different ways people have chosen to organize their Redux code. For this example though, we’re going to use the Ducks Modular Redux Pattern. In my opinion, this pattern makes navigating your Redux code much simpler, as it keeps everything you need for a specific module all in one place.

人们选择了许多不同的方式来组织其Redux代码。 在本示例中,我们将使用Ducks Modular Redux Pattern 。 在我看来,这种模式使导航Redux代码变得更加简单,因为它将特定模块所需的一切都集中在一个地方。

We’re also going to use Redux Toolkit library, which comes directly from the Redux team. It provides some useful, opinionated tools for efficient Redux development, helping you to focus on your application’s core logic without worrying about a bunch of boilerplate code.

我们还将使用Redux Toolkit库,该库直接来自Redux团队。 它提供了一些有用的,有用的工具来进行高效的Redux开发,可帮助您专注于应用程序的核心逻辑,而不必担心一堆样板代码。

Let’s go ahead and install Redux Toolkit. It will also install Redux and some other useful libraries.

让我们继续安装Redux Toolkit。 它还将安装Redux和其他有用的库。

yarn add @reduxjs/toolkit

And with that, we can create our first action and reducer! In a typical Redux application you’ll have multiple reducers, which get combined together to create a root reducer. Each reducer correlates to a key in your state object. This may be getting confusing, but stick with me and hopefully it will start to make sense.

这样,我们就可以创建第一个动作和减速器! 在典型的Redux应用程序中,您将具有多个reducer,将它们组合在一起以创建根reducer。 每个化简器都与状态对象中的一个键相关联。 这可能会令人困惑,但请坚持我,并希望它将开始变得有意义。

You’ll want to divide your reducers up in a way that makes some organizational sense. Since we’re managing the global state for our UI, it might make sense to divide it up by route/view. If we take that approach, we could structure our global state object like this:

您需要按照某种组织上的意义来划分减速器。 由于我们正在管理UI的全局状态,因此按路线/视图将其划分可能是有意义的。 如果采用这种方法,我们可以像这样构造全局状态对象:

{
play: { },
start: { }
}

And then if we break down our /play route, we can identify what pieces of data we need for its state.

然后,如果我们分解/play路线,则可以确定状态所需的数据片段。

{
play: {
text?: string;
usedLetters: string[];
},
start: { }
}

Let’s dig into it by creating a new file at src/redux/modules/play.ts. In that file we’re going to define a new setText action creator that we can use to dispatch an action to update the word/phrase to guess. An action is just an object with a type field to identify the action, and an optional payload, which can be any type you want. A traditional action creator is a simple function that returns an action object.

让我们在src/redux/modules/play.ts创建一个新文件来进行src/redux/modules/play.ts 。 在该文件中,我们将定义一个新的setText动作创建器,我们可以使用它创建一个动作以更新要猜测的单词/短语。 一个动作只是一个对象,该对象具有用于标识该动作的type字段和一个可选的有效负载,可以是您想要的任何类型。 传统的动作创建者是一个简单的函数,它返回一个动作对象。

We’re also going to define our reducer function, which takes the current state and action as parameters, and returns the updated state object. This traditionally involves a switch statement that keys off of the action type.

我们还将定义我们的reducer函数,该函数将当前状态和操作作为参数,并返回更新后的状态对象。 传统上,这涉及到一个switch语句,该语句可以禁用动作类型。

// Action types


const SET_TEXT = 'play/SET_TEXT';


// Action creators


export function setText(text: string) {
	return { type: SET_TEXT, payload: text };
}


// Reducer


type PlayState = {
	text?: string;
	usedLetters: string[];
};


function reducer(
	state: PlayState = { usedLetters: [] },
	{ type, payload }: { type: string; payload?: any }
) {
	switch (type) {
		case SET_TEXT:
			return { ...state, text: payload };
		default:
			return state;
	}
}


export default reducer;

As you may have noticed, that’s a fair amount of code to write just to set a single string value in our state object. This is where Redux Toolkit starts to shine. They provide some utility functions, like createAction and createReducer, to cut down on the boilerplate.

您可能已经注意到,仅在状态对象中设置一个字符串值就需要编写大量代码。 这就是Redux Toolkit开始发光的地方。 它们提供了一些实用程序功能,例如createActioncreateReducer ,以减少样板。

import { createAction, createReducer } from '@reduxjs/toolkit';


// Action creators


export const setText = createAction('play/SET_TEXT');


// Reducer


type PlayState = {
	text?: string;
	usedLetters: string[];
};


const reducer = createReducer({ usedLetters: [] } as PlayState, builder =>
	builder.addCase(setText, (state, { payload: text }) => ({ ...state, text }))
);


export default reducer;

Now doesn’t that look nicer? It also gives the added benefit of better type support in our reducer functions, because the compiler is able to infer the type of payload from the action. Yay!

现在看起来还不错吗? 它还提供了我们的reducer函数中更好的类型支持的额外好处,因为编译器能够从操作中推断出payload的类型。 好极了!

It’s also worth mentioning that the createReducer function from Redux Toolkit uses immer under the hood, which allows you to write reducers as if they were mutating the state directly while still maintaining pure reducer functions. So in our reducer function above, it would have been completely safe to write it this way (I just prefer the more deliberate syntax):

还值得一提的是,来自Redux Toolkit的createReducer函数在引擎盖下使用了immer ,这使您可以编写化简器,就好像它们在直接改变状态的同时仍保持纯简化器功能。 因此,在上面的reducer函数中,用这种方式编写它是完全安全的(我只是更喜欢故意使用的语法):

builder.addCase(setText, (state, { payload: text }) => {
state.text = text;
});

With our first reducer in place, let’s move on to setting up our store. First we’ll want a root reducer to combine the reducers from each of our Redux modules. Right now we only have one module, but that’s OK because this is just a trivial example for learning purposes (or at least, that’s what I’ll keep telling myself). Create a file at src/redux/root.ts.

有了我们的第一个减速器,让我们继续建立我们的商店。 首先,我们需要一个根缩减器来组合每个Redux模块中的缩减器。 现在我们只有一个模块,但这没关系,因为这只是出于学习目的的一个简单示例(或者至少是我会不断告诉自己的示例)。 在src/redux/root.ts创建一个文件。

import { combineReducers } from '@reduxjs/toolkit';
import play from './modules/play';


const rootReducer = combineReducers({ play });


export default rootReducer;

Not much to see here. The usefulness of combineReducers is more obvious as your application grows and you add additional reducers. But for now, we’ve got the structure in place to support a growing application.

这里没什么可看的。 随着应用程序的增长以及添加其他reducer, combineReducers的用途更加明显。 但就目前而言,我们已经具备了支持不断增长的应用程序的结构。

Next, let’s configure our store. Create a file at src/redux/configureStore.ts.

接下来,让我们配置商店。 在src/redux/configureStore.ts创建一个文件。

import { configureStore } from '@reduxjs/toolkit';
import reducer from './root';


const store = configureStore({ reducer });


export default store;

Again, not much to see here. We’re passing a configuration object to the configureStore function that allows us to specify our root reducer. Later on we’ll do some additional stuff, like configure middleware — but let’s not get ahead of ourselves.

同样,在这里看不到太多。 我们正在将配置对象传递给configureStore函数,该函数允许我们指定根减速器。 稍后,我们将做一些其他的事情,例如配置中间件,但是请不要超越自己。

所以现在怎么办? (So what now?)

OK. We’ve created our first action and reducer. We’ve configured our store. But how do we use it in our application? Redux is designed to work in any JavaScript application. We could interact directly with the store object we created, which exposes a dispatch function and a getState function. However, React Redux makes integrating Redux into your React application much, much simpler.

好。 我们创建了第一个动作和减速器。 我们已经配置了商店。 但是我们如何在应用程序中使用它呢? Redux设计为可在任何JavaScript应用程序中使用。 我们可以直接与我们创建的store对象进行交互,该store对象公开了一个dispatch函数和一个getState函数。 但是, React Redux使Redux集成到您的React应用程序变得非常简单。

Before we can use it, we’ll need to install it:

在使用它之前,我们需要先安装它:

yarn add react-redux# Add types for better TypeScript supportyarn add -D @types/react-redux

React Redux has already done the work of tying state changes in your Redux store to React’s change detection cycle. To tap into this, we’ll need to wrap our App component with React Redux’sProvider component and pass our store to it. We can do that in our index.tsx file.

React Redux已经完成了将Redux存储中的状态更改绑定到React的更改检测周期的工作。 为此,我们需要使用React Redux的Provider组件包装App组件,并将商店传递给它。 我们可以在index.tsx文件中做到这index.tsx

import { Provider } from 'react-redux';
import store from './redux/configureStore';


// ...


ReactDOM.render(
	
		
			
				
			
		
	,
	document.getElementById('root')
);


// ...

With our Provider in place, we can start interacting with our store from our components. Traditionally, this was done through a higher-order component (HOC) that you would create with the connect function. The connect function allows you to provide mapping functions as parameters, that map state values and action dispatchers to the component’s props. That way, you’re interacting with the store via your component’s props.

有了我们的Provider之后,我们就可以从组件开始与商店互动。 传统上,这是通过使用connect函数创建的高阶组件 (HOC)来完成的。 connect函数允许您提供映射函数作为参数,将状态值和动作分派器映射到组件的prop。 这样,您就可以通过组件的道具与商店进行交互。

For example, our App component would look something like this:

例如,我们的App组件如下所示:

import React, { Dispatch, useState } from 'react';
import { connect, RootStateOrAny } from 'react-redux';
import { setText } from './redux/modules/play';
import { PayloadAction } from '@reduxjs/toolkit';


type AppProps = {
	text?: string;
	setText: (text: string) => void;
};


function App({ setText, text }: AppProps) {


	// ...


}


function mapStateToProps(state: RootStateOrAny) {
	return {
		text: state.play.text as string
	};
}


function mapDispatchToProps(dispatch: Dispatch>) {
	return {
		setText: (text: string) => dispatch(setText(text))
	};
}


export default connect(mapStateToProps, mapDispatchToProps)(App);

However, with the introduction of React Hooks, the React Redux team added their own custom hooks. So now it’s possible to access any piece of global state inside your component with useSelector, and dispatch any action with useDispatch — no HOCs required. So with the hooks, our App component looks like this:

但是,随着React Hooks的引入,React Redux团队添加了自己的自定义钩子 。 因此,现在可以使用useSelector访问组件内部的任何全局状态,并使用useSelector调度任何操作-无需useDispatch 。 因此,有了钩子,我们的App组件如下所示:

import { RootStateOrAny, useDispatch, useSelector } from 'react-redux';
import { setText } from './redux/modules/play';


// ...


function App() {
	const dispatch = useDispatch();
	const text = useSelector(
		(state: RootStateOrAny) => state.play.text as string
	);


	// ...


	const handleSubmit = async (value: string) => {
		try {
			// ...
		} catch (err) {
			// ...
		} finally {
			dispatch(setText(value.toUpperCase()));
			history.push('/play');
		}
	};
}


export default App;

我们开工吧! (Let’s do this!)

Hopefully Redux is starting to make more sense now that you’ve seen it in action. At this point we’re ready to take all our application state and move it into our Redux store, right? Well, not exactly.

希望您已经在实际操作中看到了Redux,这使它变得更加有意义。 至此,我们已经准备好接受所有应用程序状态并将其移至Redux存储中,对吗? 好吧,不完全是。

You want to be deliberate and pragmatic about what data goes into your Redux store. Keep in mind that every action that is dispatched will run through every reducer. Things like form data are not good candidates for Redux, because a controlled component is going to fire off a state change with each keystroke. The Redux FAQs outline some rules of thumb to help you determine what data belongs in Redux:

您想要对将什么数据输入到您的Redux存储中进行谨慎而务实的操作。 请记住,调度的每个动作都将通过每个reducer进行。 表单数据之类的东西不是Redux的理想选择,因为受控组件将在每次击键时触发状态更改。 Redux常见问题解答概述了一些经验法则,可帮助您确定Redux中的数据:

Do other parts of the application care about this data?

应用程序的其他部分是否关心此数据?

Do you need to be able to create further derived data based on this original data?

您是否需要能够基于此原始数据创建其他派生数据?

Is the same data being used to drive multiple components?

是否使用相同的数据来驱动多个组件?

Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?

能够将状态恢复到给定的时间点(例如,时间旅行调试)对您来说是否有价值?

Do you want to cache the data (ie, use what’s in state if it’s already there instead of re-requesting it)?

您是否要缓存数据(即,使用已经存在的状态而不是重新请求)?

Do you want to keep this data consistent while hot-reloading UI components (which may lose their internal state when swapped)?

您是否想在热重载UI组件时保持这些数据的一致性(交换时可能会丢失其内部状态)?

So with that in mind, let’s take a look at our application. In our PlayContainer component, there are a couple of easy candidates: usedLetters and maxIncorrect. First we’ll add the appropriate action creators and update our reducer:

因此,考虑到这一点,让我们看一下我们的应用程序。 在我们的PlayContainer组件中,有几个简单的候选对象: usedLettersmaxIncorrect 。 首先,我们将添加适当的动作创建者并更新我们的reducer:

import { createAction, createReducer } from '@reduxjs/toolkit';


export const DEFAULT_MAX_INCORRECT = 10;


// Action creators


export const addUsedLetter = createAction('play/ADD_USED_LETTER');
export const setMaxIncorrect = createAction('play/SET_MAX_INCORRECT');
export const setText = createAction('play/SET_TEXT');


// Reducer


type PlayState = {
	maxIncorrect: number;
	text?: string;
	usedLetters: string[];
};


const reducer = createReducer(
	{ maxIncorrect: DEFAULT_MAX_INCORRECT, usedLetters: [] } as PlayState,
	builder =>
		builder
			.addCase(
				addUsedLetter,
				({ usedLetters, ...state }, { payload }) => ({
					...state,
					usedLetters: [...usedLetters, payload]
				})
			)
			.addCase(setMaxIncorrect, (state, { payload: maxIncorrect }) => ({
				...state,
				maxIncorrect
			}))
			.addCase(setText, (state, { payload: text }) => ({
				...state,
				text
			}))
);


export default reducer;

Then use it in our PlayContainer component:

然后在我们的PlayContainer组件中使用它:

// ...
import { RootStateOrAny, useDispatch, useSelector } from 'react-redux';
// ...
import { addUsedLetter, setMaxIncorrect } from '../redux/modules/play';


// ...


function PlayContainer({ invalid, text }: PlayContainerProps) {
	// ...


	const dispatch = useDispatch();
	const usedLetters = useSelector((state: RootStateOrAny) => state.play.usedLetters as string[]);
	const maxIncorrect = useSelector((state: RootStateOrAny) => state.play.maxIncorrect as number);


	// ...


	const handleChange = (level: number) => {
		dispatch(setMaxIncorrect(level));
	};


	const handleClick = (letter: string) => {
		dispatch(addUsedLetter(letter));
	};


	// ...
}


export default PlayContainer;


// ...

选择器和派生状态 (Selectors and derived state)

Looking at our use of the useSelector hook, you’ve probably been able to figure out what a selector is in the context of Redux. It’s really just a function that takes the state object as a parameter, and selects the desired value from it.

查看我们对useSelector挂钩的使用,您可能已经能够弄清楚Redux上下文中的选择器是什么。 它实际上只是一个将状态对象作为参数并从中选择所需值的函数。

Up to this point we’ve been using anonymous functions for simplicity’s sake, but that approach can be problematic as the application grows. Ideally, we don’t want our components to need to be aware of the shape of our state object. If down the road we decide to move some things around in our state object, we don’t want to have to touch each component that relies on that piece of state. So how do we fix that? Well, a selector module should do the trick.

到目前为止,为简单起见,我们一直在使用匿名函数,但是随着应用程序的增长,这种方法可能会出现问题。 理想情况下,我们不希望组件知道状态对象的形状。 如果我们决定在状态对象中移动一些东西,那么我们就不必触摸依赖于该状态的每个组件。 那么我们该如何解决呢? 好吧,选择器模块应该可以解决问题。

Create a file at src/redux/selectors/play.ts. In this file, we’ll export functions to select the pieces of state we’re using so far.

src/redux/selectors/play.ts创建一个文件。 在此文件中,我们将导出函数以选择到目前为止所使用的状态。

import { RootStateOrAny } from 'react-redux';


export function selectMaxIncorrect(state: RootStateOrAny) {
	return state.play.maxIncorrect as number;
}


export function selectText(state: RootStateOrAny) {
	return state.play.text as string;
}


export function selectUsedLetters(state: RootStateOrAny) {
	return state.play.usedLetters as string[];
}

Then we can update our App and PlayContainer components to use our shiny, new selectors.

然后,我们可以更新AppPlayContainer组件,以使用闪亮的新选择器。

// ...
import { selectText } from './redux/selectors/play';
// ...


function App() {
	// ...


	const text = useSelector(selectText);


	// ...
}


export default App;
// ...
import { selectMaxIncorrect, selectUsedLetters } from '../redux/selectors/play';


// ...


function PlayContainer({ invalid, text }: PlayContainerProps) {


	// ...


	const usedLetters = useSelector(selectUsedLetters);
	const maxIncorrect = useSelector(selectMaxIncorrect);


	// ...
}


export default PlayContainer;


// ...

So now we have selectors, and they provide value with some degree of future proofing our components. But you may still find yourself asking, “Are they really worth it?” Well, before you pass judgement, let’s talk about derived state.

因此,现在我们有了选择器,它们为将来在某种程度上证明我们的组件提供了价值。 但是您可能仍然会问自己:“他们真的值得吗?” 好吧,在您通过判断之前,让我们谈谈派生状态。

Derived state refers to values that are computed from other values in state. As an example of this, we can look at the textLetters object in the PlayContainer component. It’s an object that maps the unique letters in our text, and is derived from the text value coming out of our Redux store. The same is true of our correctCount and incorrectCount values, which derive their values from the usedLetters array coming out of our Redux store, along with the previously derived textLetters object.

派生状态是指从状态中的其他值计算得出的值。 作为示例,我们可以查看PlayContainer组件中的textLetters对象。 这是一个对象,它映射文本中的唯一字母,并且是从Redux存储中提取的text值派生的。 这同样是我们的真正correctCountincorrectCount值,从获得其值usedLetters阵列走出我们的终极版店,与先前得到的沿textLetters对象。

const textLetters = useMemo(
	() =>
		(text || '').split('').reduce((obj, letter) => {
			// We only care about characters between 65-90 (A-Z)
			const code = letter.charCodeAt(0);
			if (code >= 65 && code <= 90) {
				return { ...obj, [letter]: true };
			}
			return obj;
		}, {} as { [letter: string]: boolean }),
	[text]
);


const [correctCount, incorrectCount] = useMemo(
	() =>
		usedLetters.reduce(
			([correct, incorrect], letter) =>
				textLetters[letter]
					? [correct + 1, incorrect]
					: [correct, incorrect + 1],
			[0, 0]
		),
	[textLetters, usedLetters]
);

There are a few different ways we can manage this kind of derived state. One option would be to leave it as-is. But that’s not very sharable across components. Another option would be to calculate the derived values in our reducer and store them in the state object. That’s definitely more sharable, but it can bloat our state object, and adds more overhead to our reducer. A third option is to create a selector function that uses your existing selectors to get the relevant pieces of state, then returns the derived value. That’s sharable, and doesn’t bloat our state object … but what about performance? Remember, selectors are called all the time as part of React’s change detection cycle to determine if any state values have changed. We certainly don’t want to perform those computations repeatedly — potentially tens or hundreds of times per second. But what if we take some inspiration from our original solution, using memoized functions? Well now we’re getting somewhere!

我们可以通过几种不同的方式来管理这种派生状态。 一种选择是保持原样。 但这并不是跨组件共享的。 另一种选择是在我们的化简器中计算派生值,并将它们存储在状态对象中。 这绝对是更可共享的,但是它会使我们的状态对象膨胀,并给我们的reducer增加更多的开销。 第三种选择是创建一个选择器函数,该函数使用您现有的选择器来获取相关状态,然后返回派生值。 这是可以共享的,并且不会膨胀我们的状态对象……但是性能呢? 请记住,选择器在React的更改检测周期中始终被调用,以确定是否更改了任何状态值。 我们当然不希望重复执行这些计算-每秒可能数十次或数百次。 但是,如果我们使用记忆功能从原始解决方案中获得启发,该怎么办? 好吧,现在我们到了某个地方!

As luck would have it, a solution already exists. Reselect is a simple library that provides the ability to create memoized selectors. And get this: it’s already included with Redux Toolkit!

幸运的是,已经存在解决方案。 重新选择是一个简单的库,它提供了创建备注选择器的功能。 并获得这个:Redux Toolkit已经包含了它!

The createSelector function takes an array of selectors as its first argument, and passes the values from those selectors as arguments to a selector function. The resulting selector is memoized, so the function is only executed when one of the dependent selectors returns a new value. Neat, huh?

createSelector函数将选择器数组作为第一个参数,并将这些选择器中的值作为参数传递给选择器函数。 结果选择器被记忆,因此仅当其中一个依赖选择器返回新值时才执行该功能。 整洁吧?

Let’s add some memoized selectors to our src/redux/selectors/play.ts file:

让我们在src/redux/selectors/play.ts文件中添加一些记住的选择src/redux/selectors/play.ts

import { createSelector } from '@reduxjs/toolkit';
import { RootStateOrAny } from 'react-redux';


export function selectMaxIncorrect(state: RootStateOrAny) {
	return state.play.maxIncorrect as number;
}


export function selectText(state: RootStateOrAny) {
	return state.play.text as string;
}


export function selectUsedLetters(state: RootStateOrAny) {
	return state.play.usedLetters as string[];
}


// Memoized selectors


export const selectTextLettersMap = createSelector([selectText], text =>
	(text || '').split('').reduce((obj, letter) => {
		// We only care about characters between 65-90 (A-Z)
		const code = letter.charCodeAt(0);
		if (code >= 65 && code <= 90) {
			return { ...obj, [letter]: true };
		}
		return obj;
	}, {} as { [letter: string]: boolean })
);


export const selectCorrectCount = createSelector(
	[selectTextLettersMap, selectUsedLetters],
	(textLetters, usedLetters) =>
		usedLetters.reduce(
			(correct, letter) => (textLetters[letter] ? correct + 1 : correct),
			0
		)
);


export const selectIncorrectCount = createSelector(
	[selectTextLettersMap, selectUsedLetters],
	(textLetters, usedLetters) =>
		usedLetters.reduce(
			(incorrect, letter) =>
				!textLetters[letter] ? incorrect + 1 : incorrect,
			0
		)
);

And then update our PlayContainer component to use our new memoized selectors:

然后更新我们的PlayContainer组件以使用我们新的记忆选择器:

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useHistory } from 'react-router-dom';
// ...
import {
	selectCorrectCount,
	selectIncorrectCount,
	selectMaxIncorrect,
	selectTextLettersMap,
	selectUsedLetters
} from '../redux/selectors/play';


// ...


function PlayContainer({ invalid, text }: PlayContainerProps) {
	const history = useHistory();


	useEffect(() => {
		if (!text) {
			history.push('/start');
		}
	}, [history, text]);


	const dispatch = useDispatch();
	const correctCount = useSelector(selectCorrectCount);
	const incorrectCount = useSelector(selectIncorrectCount);
	const maxIncorrect = useSelector(selectMaxIncorrect);
	const textLetters = useSelector(selectTextLettersMap);
	const usedLetters = useSelector(selectUsedLetters);


	const loser = incorrectCount === maxIncorrect;
	const winner = correctCount === Object.keys(textLetters).length;
	const step = getCurrentStep(incorrectCount, maxIncorrect);


	const handleChange = (level: number) => {
		dispatch(setMaxIncorrect(level));
	};


	const handleClick = (letter: string) => {
		dispatch(addUsedLetter(letter));
	};


	return (


		// ...


	);


}


export default PlayContainer;


// ...

As you can see, the amount of logic in our component is decreasing drastically — which is a good thing.

如您所见,组件中的逻辑量急剧减少-这是一件好事。

As an additional exercise, create a memoized selector to get the current step value.

作为附加练习,创建一个记忆选择器以获取当前 step 值。

你在想我在想什么吗? (Are you thunking what I’m thunking?)

演示地址

So now we have Redux working in our application. And we’ve reduced the computation of derived state in our components through selectors. But we’ve still got a lot going on in our components. That handleSubmit function in our App component is a prime candidate for simplification. But how can we move that logic into Redux? As you are aware, that function makes HTTP requests to the dictionary API. But one of the Three Principles of Redux states that “Changes are made with pure functions.” So how do we handle asynchronous code in Redux? One word (although it kind of sounds like two): middleware.

因此,现在我们可以在应用程序中使用Redux了。 而且我们通过选择器减少了组件中派生状态的计算。 但是,我们的组件仍有很多工作要做。 App组件中的handleSubmit函数是简化的主要候选对象。 但是我们如何才能将这种逻辑转移到Redux中呢? 如您所知,该函数向字典API发送HTTP请求。 但是Redux的三项原则之一指出:“更改是通过纯函数进行的。” 那么我们如何在Redux中处理异步代码呢? 一个词(尽管听起来听起来像两个): 中间件 。

Middleware is the suggested way to extend Redux with custom functionality. It allows you to tap into your store’s dispatch function, so you can interact with — and even modify — your actions before they reach your reducer. Any side-effects triggered from actions (such as HTTP requests) should be handled in middleware.

中间件是使用自定义功能扩展Redux的建议方法。 它使您可以利用商店的dispatch功能,以便在操作到达减速器之前就可以与其进行交互,甚至进行修改。 由操作(例如HTTP请求)触发的任何副作用都应在中间件中处理。

But writing middleware is complicated, isn’t it? Not when it already exists! And especially not when it’s already included with — and configured by — Redux Toolkit. The Redux Thunk middleware allows you to write action creators that return a function instead of an object. A thunk is just another name for a function that is returned by a function. In the case of Redux Thunk, the function returned by your action creator takes the dispatch and getState functions as parameters, which then allows you dispatch additional actions or access the current state object. The Redux Thunk middleware checks each action as it is dispatched and if the action is a function (a thunk), rather than an object, it will execute the function with the dispatch and getState functions from the store.

但是编写中间件很复杂,不是吗? 当它已经存在时不行! 特别是当Redux Toolkit已包含它并由它配置时,尤其如此。 Redux Thunk中间件使您可以编写返回函数而不是对象的动作创建者。 重击只是函数返回的函数的另一个名称。 对于Redux Thunk,操作创建者返回的函数将dispatchgetState函数作为参数,然后允许您调度其他操作或访问当前状态对象。 Redux Thunk中间件在分派每个动作时对其进行检查,如果该动作是一个函数(一个thunk),而不是一个对象,它将使用存储中的dispatchgetState函数执行该功能。

So how does that help with HTTP requests? We can dispatch an action to make the HTTP request. When the request completes, we dispatch another action with the result. Is that about as clear as mud? Well, let’s try adding it to our application and see if that brings any clarity.

那么这对HTTP请求有什么帮助呢? 我们可以调度一个动作来发出HTTP请求。 请求完成后,我们将调度另一个操作并执行结果。 像泥一样清晰吗? 好吧,让我们尝试将其添加到我们的应用程序中,看看是否带来任何清晰度。

import { createAction, createReducer, Dispatch } from '@reduxjs/toolkit';
import { lookupWord } from '../../services/dictionaryApi';


export const DEFAULT_MAX_INCORRECT = 10;


// Action creators


export const addUsedLetter = createAction('play/ADD_USED_LETTER');
export const setInvalid = createAction('play/SET_INVALID');
export const setMaxIncorrect = createAction('play/SET_MAX_INCORRECT');
export const updateText = createAction('play/UPDATE_TEXT');


// Thunks


export function setText(text: string) {
	return async (dispatch: Dispatch) => {
		try {
			// Get list of unique words
			const words = Object.keys(
				// Split on white space to get individual words
				text.split(/\s/).reduce((obj, word) => {
					// Remove non-alpha characters at beginning or end of word
					const key = word
						.replace(/^[^\w]+|[^\w]+$/, '')
						.toLowerCase();
					return { ...obj, [key]: true };
				}, {} as { [key: string]: boolean })
			);
			// Make individual requests in parallel
			const lookups = words.map(word => {
				return lookupWord(word);
			});
			// Wait for all requests, then check validity of each word
			const responses = await Promise.all(lookups);
			const invalid = !responses.reduce((valid, results) => {
				return (
					valid &&
					results.some(({ meta }, index) => {
						return meta?.stems.includes(words[index]) || false;
					})
				);
			}, true);
			dispatch(setInvalid(invalid));
		} catch (err) {
			console.error(err);
	
			// Don't want to prevent play if there is an API error, and don't
			// want to potentially show a false warning.
			dispatch(setInvalid(false));
		} finally {
			dispatch(updateText(text.toUpperCase()));
		}
	};
}


// Reducer


type PlayState = {
	invalid?: boolean;
	maxIncorrect: number;
	text?: string;
	usedLetters: string[];
};


const reducer = createReducer(
	{ maxIncorrect: DEFAULT_MAX_INCORRECT, usedLetters: [] } as PlayState,
	builder =>
		builder
			.addCase(
				addUsedLetter,
				({ usedLetters, ...state }, { payload }) => ({
					...state,
					usedLetters: [...usedLetters, payload]
				})
			)
			.addCase(setInvalid, (state, { payload: invalid }) => ({
				...state,
				invalid
			}))
			.addCase(setMaxIncorrect, (state, { payload: maxIncorrect }) => ({
				...state,
				maxIncorrect
			}))
			.addCase(updateText, (state, { payload: text }) => ({
				...state,
				text,
				usedLetters: []
			}))
);


export default reducer;

That may seem like a lot to take in, but let’s break it down.

可能要花很多钱,但让我们分解一下。

First, we refactored our setText action creator to call it updateText. This is because we want to use the setText action creator for our thunk, but we still need an action that can tell our reducer to update the text value in our store.

首先,我们重构了setText动作创建者,将其称为updateText 。 这是因为我们想使用setText动作创建器来进行修改,但是我们仍然需要一个可以告诉我们的reducer更新商店中文text值的动作。

We also added an invalid flag to our state, along with an action creator for setting the flag, and the associated reducer function.

我们还向状态添加了invalid标志,以及用于设置标志的动作创建者和相关的reducer函数。

Lastly, we added our setText thunk. The actual functionality looks almost identical to what it looked like in the App component. Notice how there are multiple places where additional actions are dispatched. That allows us to update individual pieces of our state as the asynchronous operations complete, as well as wait for certain asynchronous operations to complete before updating other values in the state.

最后,我们添加了setText thunk。 实际功能看起来几乎与App组件中的功能相同。 请注意如何在多个地方调度其他操作。 这样一来,我们就可以在异步操作完成时更新状态的各个部分,并等待某些异步操作完成后再更新状态中的其他值。

Now we can update our StartContainer component to use the new setText action creator:

现在,我们可以更新StartContainer组件以使用新的setText动作创建器:

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import TextInputForm from '../components/TextInputForm/TextInputForm';
import { setText } from '../redux/modules/play';
import { selectText } from '../redux/selectors/play';


function StartContainer() {
	const dispatch = useDispatch();
	const history = useHistory();


	const text = useSelector(selectText);


	useEffect(() => {
		if (text) {
			history.push('/play');
		}
	}, [history, text]);


	const handleSubmit = (text: string) => {
		dispatch(setText(text));
	};


	return (
		<>
			

Enter a word or phrase for other players to guess.

); } export default StartContainer;

Our PlayContainer component can also be updated, since it can now get everything it needs from the Redux store, rather than relying on the App component to manage global state.

我们的PlayContainer组件也可以进行更新,因为它现在可以从Redux存储中获取所需的一切,而不必依靠App组件来管理全局状态。

// ...
import {
	selectCorrectCount,
	selectCurrentStep,
	selectIncorrectCount,
	selectInvalid,
	selectMaxIncorrect,
	selectText,
	selectTextLettersMap,
	selectUsedLetters
} from '../redux/selectors/play';


// ...


function PlayContainer() {
	const dispatch = useDispatch();
	const history = useHistory();


	const correctCount = useSelector(selectCorrectCount);
	const incorrectCount = useSelector(selectIncorrectCount);
	const invalid = useSelector(selectInvalid);
	const maxIncorrect = useSelector(selectMaxIncorrect);
	const step = useSelector(selectCurrentStep);
	const text = useSelector(selectText);
	const textLetters = useSelector(selectTextLettersMap);
	const usedLetters = useSelector(selectUsedLetters);


	useEffect(() => {
		if (!text) {
			history.push('/start');
		}
	}, [history, text]);


	// ...


}


export default PlayContainer;

And lastly, our App component can forget about managing any state whatsoever.

最后,我们的App组件可以忘记管理任何状态。

import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import PlayContainer from './containers/PlayContainer';
import StartContainer from './containers/StartContainer';


function App() {
	return (
		

Spider

); } export default App;

Once you’ve made all these changes, you may notice that the “Start Over” button no longer works. This has to do with the fact that our StartContainer component is now triggering a redirect to the /play route if text is set in state. We’ll need to update the “Start Over” button to clear the text value, which will then trigger the navigation to the /start route (and stay there until text is set again).

完成所有这些更改后,您可能会注意到“重新开始”按钮不再起作用。 这与以下事实有关:如果text设置为状态,则我们的StartContainer组件现在会触发重定向到/play路由。 我们需要更新“重新开始”按钮以清除text值,然后将触发到/start路线的导航(并停留在该位置,直到再次设置text为止)。

// ...
import {
	addUsedLetter,
	setMaxIncorrect,
	updateText
} from '../redux/modules/play';


// ...


function PlayContainer() {
	
	// ...


	const handleClickLetter = (letter: string) => {
		dispatch(addUsedLetter(letter));
	};


	const handleClickStartOver = () => {
		dispatch(updateText(''));
	};


	return (
		<>
			
// ...
// ...
); } export default PlayContainer;

And just like that, we’ve rewritten our entire application to make it look and function exactly the same! But seriously, we’ve made some real improvements by structuring our application to have a much clearer separation of concerns, and isolating our business logic so it can be more easily shared and tested.

就像那样,我们重写了整个应用程序,以使其外观和功能完全相同! 但是,认真的说,我们已经进行了一些实际的改进,方法是将应用程序结构化为具有更清晰的关注点分离,并隔离了业务逻辑,以便可以更轻松地共享和测试它。

最多11个 (Turning it up to eleven)

At this point we have the foundation for a scalable, enterprise React application. So we’re done, right? Well, not quite. Let’s take it to the next level and get reactive!

至此,我们为可扩展的企业React应用程序奠定了基础。 这样我们就完成了,对吧? 好吧,不完全是。 让我们将其带入一个新的水平并获得React !

RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It provides types (Observable, Observer, Schedulers, Subjects) and operators (map, filter, reduce, every, etc.) to allow handling asynchronous events as collections. As the RxJS team puts it:

RxJS是一个库,用于通过使用可观察的序列来组成异步和基于事件的程序。 它提供了类型(可观察,观察者,调度程序,主题)和运算符(映射,过滤,缩小,每一个等),以允许将异步事件作为集合进行处理。 正如RxJS小组所说:

Think of RxJS as Lodash for events.

将RxJS视为事件的Lodash。

The power of RxJS comes from its ability to compose streams of data or events and transform them using pure functions to produce values that react to — or update based on — new data or events being pushed down from the streams.

RxJS的强大功能在于它能够组合数据或事件流,并使用纯函数对其进行转换以产生对(或基于)从流中向下推送的新数据或事件做出React或更新的值。

If you’re not familiar with RxJS at all, it can be a little daunting at first. But don’t let that discourage you. Hopefully you can get at least the general idea from the sample code. You can also checkout learnrxjs.io, which is a pretty great resource for getting started with RxJS.

如果您根本不熟悉RxJS,一开始可能会有些令人生畏。 但是,不要让那让你沮丧。 希望您至少可以从示例代码中获得总体思路。 您也可以签出learningrxjs.io ,这对于RxJS入门是非常有用的资源。

To incorporate RxJS into our application, we’re going to use the Redux Observable middleware. Redux Observable allows us to treat dispatched actions and current state as streams, which are handled through epics. Epics are just functions that take two Observables as parameters: one representing a stream of actions and the other representing a stream of the current state from the Redux store (they can also take a third argument, which is an object of injected dependencies, but we won’t get into that here). Within an epic you use RxJS operators to perform side-effects, transform data, and ultimately return a stream (Observable) that pushes new actions.

要将RxJS集成到我们的应用程序中,我们将使用Redux Observable中间件。 Redux Observable允许我们将调度的动作和当前状态视为流,通过史诗处理。 史诗只是将两个Observables作为参数的函数:一个代表操作流,另一个代表Redux存储中当前状态的流(它们也可以接受第三个参数,这是注入依赖项的对象,但是我们在这里不做讨论)。 在史诗中,您可以使用RxJS运算符执行副作用,转换数据并最终返回推送新操作的流(可观察到)。

But enough talk, let’s get to it! Start by installing the Redux Observable library, along with RxJS:

但是足够多的讨论,让我们开始吧! 首先安装Redux Observable库以及RxJS:

yarn add redux-observable rxjs

Now we’re going to create an epic in our src/redux/modules/play.ts module. Remember, an epic is a function that takes an Observable of actions and optional Observable of state as parameters, and returns an Observable of actions — actions in, actions out.

现在,我们将在src/redux/modules/play.ts模块中创建一个史诗。 请记住,史诗是一种将动作的Observable和状态的可选Observable作为参数,并返回动作的Observable(动作输入,动作输出)的函数。

import { createAction, createReducer, PayloadAction } from '@reduxjs/toolkit';
import { ActionsObservable, ofType } from 'redux-observable';
import { forkJoin, of, from } from 'rxjs';
import { catchError, filter, map, switchMap } from 'rxjs/operators';
import { lookupWord } from '../../services/dictionaryApi';


export const DEFAULT_MAX_INCORRECT = 10;


// Action creators


export const addUsedLetter = createAction('play/ADD_USED_LETTER');
export const setInvalid = createAction('play/SET_INVALID');
export const setMaxIncorrect = createAction('play/SET_MAX_INCORRECT');
export const setText = createAction('play/SET_TEXT');


// Epics


export function setTextEpic(action$: ActionsObservable>) {
	return action$.pipe(
		ofType(setText.type),
		filter(({ payload }) => !!payload),
		switchMap(({ payload: text }) => {
			// Get list of unique words
			const words = Object.keys(
				// Split on white space to get individual words
				text.split(/\s/).reduce((obj, word) => {
					// Remove non-alpha characters at beginning or end of word
					const key = word
						.replace(/^[^\w]+|[^\w]+$/, '')
						.toLowerCase();
					return { ...obj, [key]: true };
				}, {} as { [key: string]: boolean })
			);


			// Make individual requests in parallel
			const lookups = words.map(word => {
				return from(lookupWord(word));
			});


			// Wait for all requests, then check validity of each word
			return forkJoin(lookups).pipe(
				map(results => {
					const invalid = !results.reduce((valid, results, index) => {
						return (
							valid &&
							results.some(({ meta }) => {
								return (
									meta?.stems.includes(words[index]) || false
								);
							})
						);
					}, true);
					return setInvalid(invalid);
				}),
				catchError(err => {
					console.error(err);


					// Don't want to potentially show a false warning if there
					// is an API error.
					return of(setInvalid(false));
				})
			);
		})
	);
}


// Reducer


type PlayState = {
	invalid?: boolean;
	maxIncorrect: number;
	text?: string;
	usedLetters: string[];
};


const reducer = createReducer(
	{ maxIncorrect: DEFAULT_MAX_INCORRECT, usedLetters: [] } as PlayState,
	builder =>
		builder
			.addCase(
				addUsedLetter,
				({ usedLetters, ...state }, { payload }) => ({
					...state,
					usedLetters: [...usedLetters, payload]
				})
			)
			.addCase(setInvalid, (state, { payload: invalid }) => ({
				...state,
				invalid
			}))
			.addCase(setMaxIncorrect, (state, { payload: maxIncorrect }) => ({
				...state,
				maxIncorrect
			}))
			.addCase(setText, (state, { payload: text }) => ({
				...state,
				text: text?.toUpperCase(),
				usedLetters: []
			}))
);


export default reducer;

Isn’t that so much better?!

那不是更好吗?

演示地址

OK, OK. I’ll admit this probably just looks like a more complicated way to do exactly what we were doing before. It’s hard to demonstrate the real power of RxJS in such a trivial example. But let’s at least go through it, then explore some ways that we can start to see the benefits of RxJS.

好的好的。 我承认这可能看起来像是一种更复杂的方式来精确地完成我们之前的工作。 在如此琐碎的示例中很难证明RxJS的真正功能。 但是至少让我们经历一下,然后探索一些我们可以开始看到RxJS好处的方法。

So the first thing we did was get rid of our setText thunk, and re-renamed our updateText action creator back to setText. Then we created a setTextEpic function. For this example we don’t need to access the current state, so we only included the first parameter, the Observable of actions. Our epic needs to return an Observable of actions, so we’re just going to return the current action$ Observable, using the pipe function to chain operators for transforming the data and performing side-effects.

因此,我们要做的第一件事是摆脱了setText麻烦,并将我们的updateText动作创建者重命名为setText 。 然后,我们创建了一个setTextEpic函数。 在此示例中,我们不需要访问当前状态,因此我们仅包括第一个参数,即动作的观察值。 我们的史诗需要返回一个Observable动作,因此我们将返回当前的action$ Observable,使用pipe函数将运算符链接到链上,以转换数据并执行副作用。

Let’s step through our operators and see what they’re doing. First we use the ofType operator. This is a simple filter that will only emit actions down the chain that match the specified type. Typically an epic should be focused on handling a single action, and you write lots of epics to handle your various actions.

让我们逐步了解操作员,看看他们在做什么。 首先,我们使用ofType运算符。 这是一个简单的过滤器,将仅在与指定类型匹配的链中发出操作。 通常,史诗应该专注于处理单个动作,并且您编写许多史诗来处理各种动作。

Next, we’re using the filter operator to ensure that we have a new value for the text before we continue down the chain. Filter will only emit actions that match the specified criteria. In our case, it will prevent us from performing all the validation logic when we’re clearing our text. It’s worth mentioning we could have combined ofType and filter into a single filter, but I wanted to demonstrate the ofType operator. Also be aware that actions don’t arrive to your epics until after they’ve gone through your reducers. According to the Redux Observable documentation:

接下来,我们将使用filter运算符来确保在继续进行搜索之前,我们拥有文本的新值。 过滤器将仅发出符合指定条件的动作。 就我们而言,这将阻止我们在清除文本时执行所有验证逻辑。 值得一提的是,我们可以将ofTypefilter合并为一个单独的filter ,但是我想演示ofType运算符。 另外请注意,只有在经过简化之后 ,动作才会到达您的史诗。 根据Redux Observable文档 :

Epics run alongside the normal Redux dispatch channel, after the reducers have already received them — so you cannot “swallow” an incoming action. Actions always run through your reducers before your Epics even receive them.

在reducer已收到它们 之后 Epic会与正常的Redux分发渠道一起运行, 因此您不能“吞噬”传入的操作。 动作始终会您的Epics接收到它们之前通过减速器运行

So even though we aren’t processing any empty values in our epic, the text value in our store will still get updated when we clear it. Note that we also updated our reducer to convert our text to uppercase (rather than in our action), since it will reach the reducer before the epic.

因此,即使我们没有在史诗中处理任何空值,但清除它们后,商店中的text值仍会更新。 请注意,我们还更新了化简器以将文本转换为大写(而不是在操作中),因为文本将在史诗之前到达化简器。

After that, we use the switchMap operator which allows us to return an Observable (or Promise), which we’ll need to call our asynchronous lookupWord function. Inside the switchMap operator, things probably look very similar to our Promise-based solution. There are a couple of key differences: 1) We wrap our lookupWord calls with the from function, which creates an Observable from a Promise. And 2) we use the forkJoin function rather than Promise.all, which in a similar fashion takes an array of Observables and will give us a single Observable that emits an array of results when all of the Observables that were passed in emit a value.

之后,我们使用switchMap运算符,该switchMap符允许我们返回一个Observable(或Promise),我们将需要调用它的异步lookupWord函数。 在switchMap运算符中,事情看起来可能与我们基于Promise的解决方案非常相似。 有几个主要区别:1)我们使用from函数包装lookupWord调用,这从Promise中创建了一个Observable。 2)我们使用forkJoin函数而不是Promise.all ,它以类似的方式获取一个Observable数组,并为我们提供一个Observable,当传入的所有Observable都发出一个值时,该Observable会发出结果数组。

At the end of it all, we emit a new action by returning the setInvalid action. It’s imperative that your Observable emits a new, and different, action. Otherwise you’ll create an infinite loop within your epic, which is generally considered to be a bad thing.

最后,我们通过返回setInvalid动作来发出一个新动作。 当务之急是,您的Observable发出新的, 不同的动作。 否则,您将在史诗中创建一个无限循环,这通常被认为是一件坏事。

Now that we have an epic, we need to configure the Redux Observable middleware (at least, if we want it to work). First, we need to create a root epic. Similar to how Redux only allows a single root reducer, Redux Observable requires a single root epic. For convenience, they provide a combineEpics function which, similar to Redux’s combineReducers function, wraps any number of epics in a single root epic.

现在我们有了一个史诗,我们需要配置Redux Observable中间件(至少,如果我们希望它可以工作的话)。 首先,我们需要创建根史诗。 与Redux只允许单个根减少器的方式类似,Redux Observable需要单个根史诗。 为了方便起见,它们提供了combineEpics函数,类似于Redux的combineReducers函数,将任意数量的史诗包装在单个根史诗中。

Let’s export our root epic from the same module as our root reducer, src/redux/root.ts.

让我们从与根减速器src/redux/root.ts相同的模块中导出根史诗。

import { combineReducers } from '@reduxjs/toolkit';
import { combineEpics, Epic } from 'redux-observable';
import play, { setTextEpic } from './modules/play';


export const rootEpic = combineEpics(setTextEpic) as Epic;


const rootReducer = combineReducers({ play });


export default rootReducer;

And then we’ll configure the middleware in src/redux/configureStore.ts.

然后,我们将在src/redux/configureStore.ts中间件。

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { createEpicMiddleware } from 'redux-observable';
import reducer, { rootEpic } from './root';


const defaultMiddleware = getDefaultMiddleware();
const epicMiddleware = createEpicMiddleware();
const middleware = [...defaultMiddleware, epicMiddleware];


const store = configureStore({ middleware, reducer });


epicMiddleware.run(rootEpic);


export default store;

It’s worth pointing out here the call to getDefaultMiddleware. This allows us to get the middleware already configured by Redux Toolkit, and add the epic middleware to it. Otherwise, we’d lose the middleware provided by Redux Toolkit.

值得在这里指出对getDefaultMiddleware的调用。 这使我们可以获得Redux Toolkit已经配置的中间件,并向其添加史诗般的中间件。 否则,我们将失去Redux Toolkit提供的中间件。

You’ll also notice that after we configure our store, we make this call:

您还将注意到,在配置商店之后,我们将进行以下调用:

epicMiddleware.run(rootEpic);

That’s what starts up the actual processing of your epics alongside the normal Redux dispatch channel. And with that in place, we now have a fully functioning epic in our application!

这就是在正常Redux分发通道旁边启动对史诗的实际处理的过程。 有了这些,我们现在在我们的应用程序中有了一个功能齐全的史诗!

那么为什么要使用RxJS呢? (So why RxJS?)

演示地址

As I mentioned earlier, RxJS can be overwhelming at first, and it’s hard to see its real value in such a trivial example. So let’s try adding some functionality that has real value and hopefully shows some of the advantages of RxJS.

正如我之前提到的,RxJS最初可能会让人不知所措,并且在如此琐碎的示例中很难看到它的真正价值。 因此,让我们尝试添加一些具有实际价值的功能,并希望展示RxJS的一些优点。

Suppose, for the sake of this example, our requests to the dictionary API take a very long time … perhaps there’s some issue with the API or our user has a very slow internet connection. Then also suppose that a user enters in a sentence which ships off a bunch of HTTP requests, then decides to change their sentence. When they click “Start Over” and enter in a new sentence, it will ship off another batch of HTTP requests. But if the first batch of requests haven’t completed yet, the new batch will just get queued up and wait for the others to complete first. Not to mention, when the first batch does complete the responses will run through our reducers even though we no longer care about those responses.

假设出于这个示例的原因,我们对字典API的请求花费了很长时间……也许API出现了一些问题,或者我们的用户的互联网连接速度非常慢。 然后还假设用户输入了一个句子,该句子附带了一系列HTTP请求,然后决定更改其句子。 当他们单击“重新开始”并输入新句子时,它将发送另一批HTTP请求。 但是,如果第一批请求尚未完成,那么新的批处理将排在队列中,并等待其他请求先完成。 更不用说,当第一批完成时,即使我们不再关心那些响应,响应也将通过我们的减速器运行。

So what can we do about this? One of the big advantages of Observables over Promises is that Observables are cancellable. So in theory, we should be able to cancel our requests when the user navigates away from the /play route.

那么我们该怎么办? 相对于Promise,Observables的一大优势是Observable是可取消的。 因此,从理论上讲,当用户离开/play路线时,我们应该能够取消我们的请求。

The first thing we’ll need to do is update our dictionaryApi service to use the RxJS fromFetch function, rather than the global fetch. The fromFetch function is a simple wrapper around the global fetch function that returns an Observable rather than a promise. Additionally, it automatically sets up an AbortController, which allows us to not only cancel the subscription on the Observable, but cancel the HTTP request.

我们需要做的第一件事是更新我们的dictionaryApi服务以使用RxJS fromFetch函数,而不是全局fetchfromFetch函数是全局fetch函数的简单包装,该函数返回一个Observable而不是promise。 另外,它会自动设置一个AbortController ,它使我们不仅可以取消Observable上的订阅,还可以取消HTTP请求。

import { from, Observable } from 'rxjs';
import { fromFetch } from 'rxjs/fetch';
import { switchMap } from 'rxjs/operators';


const API_KEY = process.env.REACT_APP_DICTIONARY_API_KEY;
const BASE_URL = process.env.REACT_APP_DICTIONARY_API_URL;


type DictionaryItem = {
	meta?: { stems: string[] };
};


export function lookupWord(word: string) {
	return fromFetch(
		`${BASE_URL}/${encodeURIComponent(word)}?key=${API_KEY}`
	).pipe(
		switchMap(response => {
			if (!response.ok) {
				throw response;
			}
			return from(response.json()) as Observable;
		})
	);
}

Next, we’ll need to update our setTextEpic so it knows when to cancel any pending requests. The Redux Observable documentation gives us a recipe for cancellation that uses the takeUntil operator to trigger a cancellation when a cancellation action is dispatched.

接下来,我们需要更新setTextEpic以便它知道何时取消任何待处理的请求。 Redux Observable文档为我们提供了取消方法 ,该takeUntil可以在派出取消操作时使用takeUntil运算符触发取消。

// ...
import { catchError, filter, map, switchMap, takeUntil } from 'rxjs/operators';


// ...


// Action creators


export const addUsedLetter = createAction('play/ADD_USED_LETTER');
export const cancelSetText = createAction('play/CANCEL_SET_TEXT');
export const setInvalid = createAction('play/SET_INVALID');
export const setMaxIncorrect = createAction('play/SET_MAX_INCORRECT');
export const setText = createAction('play/SET_TEXT');


// Epics


export function setTextEpic(action$: ActionsObservable>) {
	return action$.pipe(
		ofType(setText.type),
		filter(({ payload }) => !!payload),
		switchMap(({ payload: text }) => {
			// Get list of unique words
			
			// ...


			// Make individual requests in parallel
			const lookups = words.map(word => {
				return lookupWord(word);
			});


			// Wait for all requests, then check validity of each word
			return forkJoin(lookups).pipe(
				takeUntil(action$.pipe(ofType(cancelSetText.type))),
				map(results => {
					
					// ...
					
				}),
				catchError(err => {
					
					// ...
					
				})
			);
		})
	);
}


// ...

As you can see, we’ve added a cancelSetText action creator, as well as adding the takeUntil operator to our forkJoin. The takeUntil operator takes a notifier Observable as its only parameter. It emits the values from the source Observable until a value is emitted from the notifier Observable, at which point it completes, effectively cancelling our upstream Observables including any pending HTTP requests. For our notifier Observable, we’re simply going to use our action$ stream and filter for our cancelSetText action. So now if we dispatch the cancelSetText action, it will cancel any pending requests. Pretty slick!

如您所见,我们添加了一个cancelSetText动作创建器,以及将takeUntil运算符添加到了forkJointakeUntil运算符将通告程序Observable用作其唯一参数。 它从源Observable发出值,直到从通知者Observable发出值为止,这时它完成了,从而有效地取消了我们的上游Observable,包括所有未决的HTTP请求。 对于通知者Observable,我们将仅使用action$流并为cancelSetText操作使用过滤器。 因此,现在如果我们分派cancelSetText操作,它将取消所有未决的请求。 非常漂亮!

The last thing we need to do is update our PlayContainer component to dispatch the cancelSetText action when a user navigates away. We can do this by tapping into our existing useEffect hook. The useEffect hook already has a built-in clean-up mechanism. By returning a function from our useEffect hook, React will automatically run that function any time the component unmounts (such as when navigating away). So we just need to return a function that dispatches our cancelSetText action.

我们需要做的最后一件事是更新我们的PlayContainer组件,以在用户离开时分派cancelSetText操作。 我们可以通过利用现有的useEffect挂钩来完成此操作。 useEffect挂钩已经具有内置的清除机制 。 通过从useEffect钩子返回一个函数,React将在组件卸载时(例如,在离开时)自动运行该函数。 因此,我们只需要返回一个调度我们的cancelSetText操作的函数。

// ...
import {
	addUsedLetter,
	setMaxIncorrect,
	setText,
	cancelSetText
} from '../redux/modules/play';


// ...


function PlayContainer() {
	
	// ...


	useEffect(() => {
		if (!text) {
			history.push('/start');
		}
		return () => {
			dispatch(cancelSetText());
		};
	}, [dispatch, history, text]);


	// ...
}


export default PlayContainer;

Although still a somewhat trivial example, I hope you’re starting to see the advantages we gain through RxJS.

尽管仍然是一个微不足道的示例,但我希望您开始看到我们通过RxJS获得的好处。

If you’re feeling ambitious, as an additional exercise try adding retry logic with an exponential backoff to your API requests using RxJS. (Hint: check out the retryWhen operator.)

如果您有雄心壮志,作为另一项练习,请尝试使用RxJS向API请求添加具有指数补偿的重试逻辑。 (提示:检查 retryWhen 运算符。)

我们做到了! (We did it!)

Wow, what a ride! We’ve covered making asynchronous calls to fetch data; using Redux to manage global application state; using memoized selectors to efficiently compute derived state; using Redux Thunk for dispatching simple actions with side-effects; and took things to the next level for more complex state management with RxJS and Redux Observable.

哇,太好了! 我们已经介绍了进行异步调用以获取数据。 使用Redux管理全局应用程序状态; 使用记忆选择器有效地计算派生状态; 使用Redux Thunk分发具有副作用的简单操作; 并通过RxJS和Redux Observable将状态提升到一个更复杂的状态管理。

At this point, we’ve covered most of our business logic and data layers of our application diagram.

至此,我们已经涵盖了应用程序图中的大部分业务逻辑和数据层。

For those looking for an additional challenge, take your new found knowledge of Redux middleware and try persisting your gameplay state in local storage with Redux Persist.

对于那些寻求其他挑战的人,请使用Redux中间件的新知识,并尝试通过 Redux Persist 将您的游戏状态保持在本地存储中

I hope these tutorials have helped you learn some useful patterns and practices to help you build scalable, enterprise-ready React applications in the real world. Now go and build something awesome!

我希望这些教程能够帮助您学习一些有用的模式和实践,以帮助您在现实世界中构建可扩展的,可用于企业的React应用程序。 现在去建立一些很棒的东西!

翻译自: https://medium.com/imaginelearning/building-a-react-application-part-ii-76b407607df8

创建react应用程序

你可能感兴趣的:(python,react,java)