Recoil.js is Facebook’s new state management library for React. It’s been designed to handle complex global state management with performance and asynchronous in mind.
Recoil.js是Facebook针对React的新状态管理库。 它旨在处理性能和异步方面的复杂全局状态管理。
If you haven’t heard about it, I suggest watching this excellent introduction video by the creators.
如果您还没有听说过,建议您观看创作者的精彩介绍视频 。
The goal of this article will be to build a COVID tracker app with Recoil and React. It will display the virus data on a map for different statistics and dates.
本文的目标是使用Recoil和React构建一个COVID跟踪器应用程序。 它将在地图上显示病毒数据以提供不同的统计信息和日期。
Here is a live demo of what we’ll build and the full source code.
这是我们将构建的内容和完整源代码的实时演示 。
We’ll be using the Covid-19 REST API to fetch the data we need.
我们将使用Covid-19 REST API来获取所需的数据。
While being relatively simple, this app will allow us to touch on quite a few interesting topics :
虽然相对简单,但该应用程序将使我们能够涉及许多有趣的主题:
- Representing and manipulating state with Recoil 用后座力表示和操纵状态
- Querying an API and handling asynchronous state 查询API并处理异步状态
- Prefetching data and caching previous requests 预取数据并缓存先前的请求
Displaying a map and adding data to it with Deck.gl
使用Deck.gl显示地图并向其中添加数据
Styling components with
styled-components
and dynamic styles使用
styled-components
和动态样式来styled-components
Optimization with
useMemo
使用
useMemo
优化
项目设置 (Project Setup)
Let’s start by setting up the project with create-react-app
让我们首先使用create-react-app
设置项目
yarn create react-app react-recoil-covid-tracker
cd react-recoil-covid-tracker
yarn start
Let’s cleanup the boilerplate code by deleting App.css
, App.test.js
and logo.svg
让我们通过删除App.css
, App.test.js
和logo.svg
清理样板代码
Replace App.js
替换App.js
Replace index.css
替换index.css
Basic CSS reset which helps taming default browser styles.
基本CSS重置,有助于驯服默认浏览器样式。
显示地图 (Displaying the map)
We’ll be using Deck.gl and MapBox’s React wrapper to display our map.
我们将使用Deck.gl和MapBox的React包装器来显示地图。
yarn add deck.gl react-map-gl
We’ll need a token from MapBox to display the map, follow this link to signup and create your access token, don’t worry they allow for 50k loads per month for free.
我们需要MapBox中的令牌来显示地图,点击此链接可以注册并创建您的访问令牌,不用担心它们每月免费提供5万次加载。
Create a mapbox-token.js
file in src
在src
创建mapbox-token.js
文件
export const mapboxToken = "{YOUR_TOKEN_HERE}"
And finally add MapBox’s CSS into your public/index.html
最后将MapBoxCSS添加到您的public/index.html
Now let’s create our DeckGL map component DeckGLMap.js
inside components/map
现在让我们在components/map
创建我们的DeckGL map组件DeckGLMap.js
And import it into App.js
并将其导入App.js
With that, we already have a working map of the world.
至此,我们已经有了一张可行的世界地图。
从API获取我们需要的数据 (Getting the data we need from the API)
It’s time to get some data from the API, let’s take a look at the documentation first.
现在是时候从API中获取一些数据了,让我们首先看一下文档 。
The first thing we need is the list of countries with their latitude and longitude, we can get this information by making a GET request to the /countries
endpoint.
我们需要的第一件事是其纬度和经度的国家/地区列表,我们可以通过向/countries
端点发出GET请求来获取此信息。
Next, we’ll need to get the data for each country, at first we’ll only display data for today, but then we’ll provide users with date controls so they can see the data for any given date.
接下来,我们需要获取每个国家/地区的数据,首先,我们仅显示今天的数据,但是随后,我们将为用户提供日期控件,以便他们可以查看任何给定日期的数据。
We have two possibilities here, the first one is /timelineByCountry
which would allow us to request all the data we need for a given country. With this approach, we could get all the data we need by making one request per country, and one request to /countries
.
在这里,我们有两种可能性,第一种是/timelineByCountry
,它可以让我们请求给定国家/地区所需的所有数据。 通过这种方法,我们可以通过向每个国家/地区提出一个请求,向/countries
提出一个请求来获取所需的所有数据。
The second possibility is to make a request to /statusByDate
which would return the data for all countries for a given date. With this approach, we would have to make one request per date to get all of the data we need.
第二种可能性是向/statusByDate
发出请求,该请求将返回给定日期的所有国家/statusByDate
的数据。 使用这种方法,我们将不得不在每个日期发出一个请求以获取我们需要的所有数据。
I’m going to choose to go with /statusByDate
, even though it will require more requests over all, it will allow us to make our app load the data as needed, instead of having one big initial loading.
我将选择使用/statusByDate
,即使它需要更多的请求,也可以使我们的应用程序按需加载数据,而不是进行大量初始加载。
Both approach are viable, it’s just a choice between having a longer initial loading but no loading after or having a quicker initial loading and then loading data as we need it.
两种方法都是可行的,这只是在较长的初始加载但在初始加载之后没有加载或初始加载更快之后再根据需要加载数据之间的一种选择。
Here I think it makes sense not loading everything from the start because, in cases where the user doesn’t interact with the app or doesn’t go back in time fully, we’ll end up loading less data.
在这里,我认为从一开始就不加载所有内容是有道理的,因为在用户不与应用程序交互或未完全返回时的情况下,我们最终将加载较少的数据。
More importantly, this will allow me to show you how we can use Recoil to make it seem like there is almost no loading even though we’re doing a progressive loading.
更重要的是,这将使我向您展示如何使用Recoil使其看起来几乎没有负载,即使我们正在进行渐进式负载。
使用Recoil查询API (Using Recoil to query the API)
Let’s start by displaying the data for today on our map.
让我们从在地图上显示今天的数据开始。
Add Recoil to the project
将Recoil添加到项目中
yarn add recoil
The main building blocks of Recoil are atom
and selector
反冲的主要组成部分是atom
和selector
An atom
is a small piece of state, it takes a key
and a default
value and you get a piece of state which you can retrieve and update.
atom
是一小块状态,它需要一个key
和一个default
值,您会获得一个状态,可以检索和更新。
A selector
is used to derive state from another piece of state like an atom
or even another selector
, or it can be used to get data asynchronously. It takes a key
as well, but instead of a default value, you can provide a get
function which will be executed to retrieve the value.
selector
用于从另一个状态(例如atom
甚至另一个selector
派生状态,也可以用于异步获取数据。 它也需要一个key
,但是可以提供一个get
函数,该函数将执行以检索该值,而不是默认值。
Let’s group our API requests in /src/state/api/index.js
让我们将API请求分组到/src/state/api/index.js
import { selector } from "recoil"
export const countriesQuery = selector({
key: "countries",
get: async () => {
try {
const res = await fetch("https://covid19-api.org/api/countries")
const countries = await res.json()
return countries.reduce((dict, country) => {
dict[country.alpha2] = country
return dict
}, {})
} catch (e) {
console.error("ERROR GET /countries", e)
}
},
})
Here we use a selector
, as we want to get our data asynchronously from the API.
在这里,我们使用selector
,因为我们想从API异步获取数据。
We set theget
option as an async
method that will take care of querying our API endpoint with
我们将get
选项设置为async
方法,该方法将使用以下方法查询API端点
const res = await fetch("https://covid19-api.org/api/countries")
Then we parse our JSON response to a Javascript object, an array of countries in this case.
然后,我们解析对Javascript对象(在这种情况下为国家/地区)的JSON响应。
const countries = await res.json()
Finally, we transform this array into an object so we can later access our countries directly by their alpha2
property.
最后,我们将此数组转换为对象,以便以后可以通过其alpha2
属性直接访问我们的国家/地区。
Objects returned from the /statusByDate
endpoint will also have this alpha2
property as the country
property. This will allow us to easily retrieve which country our data belongs to.
从/statusByDate
端点返回的对象也将具有此alpha2
属性作为country
属性。 这将使我们能够轻松检索数据所属的国家。
return countries.reduce((dict, country) => {
dict[country.alpha2] = country
return dict
}, {})
使用我们的异步数据 (Using our asynchronous data)
We’ll create a small component to display our countries list and see how we can use our newly created selector
我们将创建一个小的组件以显示我们的国家/地区列表,并了解如何使用新创建的selector
Create a Countries.js
component in components
在components
创建一个Countries.js
components
import React from "react"
import { useRecoilValue } from "recoil"
import { countriesQuery } from "../state/api"
const Countries = () => {
const countries = useRecoilValue(countriesQuery)
return (
{Object.keys(countries).map((alpha2) => {
return - {countries[alpha2].name}
})}
)
}
export default Countries
First, we import our countriesQuery
selector
from our state/api
首先,我们从state/api
导入countriesQuery
selector
Then, we get our actual countries
data by calling useRecoilValue
and passing it our countriesQuery
然后,我们通过调用useRecoilValue
并将其传递给我们的countriesQuery
获取实际的countries
数据
Remember countries
is an object and not an array, so we iterate over its keys
to display a list with each country’s name.
请记住, countries
是一个对象,而不是一个数组,因此我们遍历其keys
以显示包含每个国家/地区名称的列表。
And that’s all we need to do, because our component will now “suspend” while retrieving our data and we can handle this in the parent component with Suspense.
这就是我们需要做的所有事情,因为现在我们的组件将在检索数据时“挂起”,并且我们可以使用Suspense在父组件中进行处理。
Import our Countries
component in App.js
在App.js
导入我们的Countries
组件
import React, { Suspense } from "react"
import { RecoilRoot } from "recoil"
import Countries from "./components/Countries"
const App = () => {
return (
)
}
export default App
Only two things we need here :
我们这里只需要两件事:
RecoilRoot
because any component that uses Recoil needs to be a child of this component provided by RecoilRecoilRoot
因为使用Recoil的任何组件都必须是Recoil提供的该组件的子组件Suspense
to handle when our component “suspends”Suspense
处理时,我们的组件“挂起”
While we’re fetching, Suspense
will take care of displaying what’s in our fallback
prop and once we get the data our component will render with the list of countries. Pretty easy, right ?
在获取数据时, Suspense
将负责显示fallback
属性中的内容,一旦获得数据,我们的组件就会与国家/地区列表一起呈现。 很容易,对吧?
If you’re wondering how we would handle errors, you can find out in Suspense’s docs.
如果您想知道我们将如何处理错误,可以在Suspense的docs中找到。
We won’t need the Countries
component anymore so you can delete it now and remove everything inside of RecoilRoot
in App.js
, as well as the import
statements.
我们将不需要Countries
组成了,所以你现在可以删除它和上卸下里面的一切RecoilRoot
在App.js
,还有import
的语句。
从API获取我们的COVID数据 (Getting our COVID data from the API)
Now that we’re able to fetch our countries, let’s get the corresponding COVID data by making a call to the /statusByDate
endpoint which will return the number of cases, deaths and recovered for each country for a specified date.
现在我们可以获取我们的国家/地区了,让我们通过调用/statusByDate
端点来获取相应的COVID数据,该端点将返回指定日期每个国家/statusByDate
的病例,死亡人数和康复人数。
Another building block provided by Recoil is the selectorFamily
, it works like selector
but except it allows us to pass parameters as well.
Recoil提供的另一个构建块是selectorFamily
,它的作用类似于selector
但它也允许我们传递参数。
We’ll use it to create a query which accepts the requested date as a parameter.
我们将使用它来创建一个查询,该查询接受请求的日期作为参数。
Add the new query in state/api/index.js
在state/api/index.js
添加新查询
import { selector, selectorFamily } from "recoil"
export const API_DATE_FORMAT = "yyyy-MM-dd"
export const countriesQuery = selector({
key: "countries",
get: async () => {
try {
const res = await fetch("https://covid19-api.org/api/countries")
const countries = await res.json()
return countries.reduce((dict, country) => {
dict[country.alpha2] = country
return dict
}, {})
} catch (e) {
console.error("ERROR GET /countries", e)
}
},
})
export const statusByDateQuery = selectorFamily({
key: "status-by-date",
get: (formattedDate) => async ({ get }) => {
try {
const res = await fetch(`https://covid19-api.org/api/status?date=${formattedDate}`)
const status = await res.json()
return status
} catch (e) {
console.error("ERROR GET /statusByDate", e)
}
},
})
We define and export a constant API_DATE_FORMAT
which will hold the date format that our API expects. We’ll use this to do the conversion from a Date
object to the correct corresponding string later in the project.
我们定义并导出一个常量API_DATE_FORMAT
,它将保存我们的API期望的日期格式。 我们稍后将在项目中使用它来将Date
对象转换为正确的对应字符串。
We use selectorFamily
so we can pass our formattedDate
as a parameter of our query.
我们使用selectorFamily
因此我们可以将我们的formattedDate
传递为查询的参数。
Our statusByDateQuery
’s get
option is now a function that accepts the date and returns a new async
function.
现在,我们的statusByDateQuery
的get
选项是一个接受日期并返回新的async
函数的函数。
This async function
will then make the actual call to our GET statusByDate
endpoint using our formattedDate
.
然后,此async function
将使用我们的formattedDate
实际调用我们的GET statusByDate
端点。
Here is an important thing to know about Recoil : the result of our get
function in a selectorFamily
is memoized.
这是有关Recoil的重要一件事:将selectorFamily
中的get
函数的结果记录下来。
This means that if we already called our query with the same formattedDate
before, it won’t make another API call and wait for the result.
这意味着,如果我们之前已经使用相同的formattedDate
调用了查询,则不会进行其他API调用并等待结果。
Instead, it will return our result directly because it already “knows” what the result will be for this specific parameter value.
相反,它将直接返回我们的结果,因为它已经“知道”该特定参数值的结果。
This is also why it’s important that we use a string
, which is a primitive type, as our parameter here and not a Date
object, which is a reference type.
这也是为什么我们使用string
(这是原始类型)作为此处的参数,而不是使用Date
对象(作为引用类型)作为参数的重要原因。
Because "" === "" // true
but {} === {} // false
因为"" === "" // true
但是{} === {} // false
If your previousParam === newParam
, and only in this case, Recoil will not rerun the code inside the get
function and return the memoized value instead.
如果您的previousParam === newParam
,并且仅在这种情况下,Recoil不会重新运行get
函数中的代码,而是返回记录的值。
If you need Recoil to actually make the API call every time, you could do so by using a reference type as your parameter, such as an object. When calling the selectorFamily
, you’ll have to pass a new object every time to make sure the query actually reruns every time.
如果您需要Recoil每次都实际进行API调用,则可以通过使用引用类型作为参数来实现,例如对象。 调用selectorFamily
,您每次必须传递一个新对象,以确保每次查询实际上都在重新运行。
将我们的数据添加到地图 (Adding our data to the map)
First we’ll need the date-fns
npm package to help us work with dates
首先,我们需要date-fns
npm软件包来帮助我们处理日期
yarn add date-fns
Inside components/map
, create a new DataMap.js
component
在components/map
内部,创建一个新的DataMap.js
组件
import React from "react"
import DeckGLMap from "./DeckGLMap"
import { ScatterplotLayer } from "@deck.gl/layers"
import { useRecoilValue } from "recoil"
import { statusByDateQuery, countriesQuery, API_DATE_FORMAT } from "../../state/api"
import { format } from "date-fns"
const DataMap = () => {
const date = new Date()
const formattedDate = format(date, API_DATE_FORMAT)
const todayStatus = useRecoilValue(statusByDateQuery(formattedDate))
const countries = useRecoilValue(countriesQuery)
const data = todayStatus.map((status) => {
const country = countries[status.country]
return {
name: country.name,
coordinates: [country.longitude, country.latitude],
...status,
}
})
const covidDataLayer = new ScatterplotLayer({
id: "covid-data-layer",
data,
stroked: false,
filled: true,
getPosition: (d) => d.coordinates,
getRadius: (d) => (d.cases > 0 ? (Math.log10(d.cases) + d.cases / 100000) * 20000 : 0),
getFillColor: (d) => [255, 0, 0],
})
const layers = [covidDataLayer]
return
}
export default DataMap
Here we start by creating a new Date
object, which will be set to today’s date by default.
在这里,我们首先创建一个新的Date
对象,默认情况下会将其设置为今天的日期。
We then use formattedDate = format(date, API_DATE_FORMAT)
to get the corresponding string that we can pass to our API.
然后,我们使用formattedDate = format(date, API_DATE_FORMAT)
获得可以传递给我们的API的相应字符串。
We fetch today’s status with todayStatus = useRecoilValue(statusByDateQuery(formattedDate))
我们使用todayStatus = useRecoilValue(statusByDateQuery(formattedDate))
获取今天的状态
We get our countries in the same way as before with countries = useRecoilValue(countryQuery)
我们以与以前相同的方式获取国家countries = useRecoilValue(countryQuery)
What is returned by our statusByDateQuery
will be an array of objects containing our data, each object will look like this :
我们的statusByDateQuery
返回的将是包含我们的数据的对象数组,每个对象将如下所示:
{
cases: 5244238
country: "US"
deaths: 167029
last_update: "2020-08-13T23:27:24"
recovered: 1774648
}
We map over our todayStatus
array to add the country’s information to each data object.
我们映射到todayStatus
数组上,以将国家/地区的信息添加到每个数据对象中。
We get our country’s info with countries[status.country]
and return our previous data object with the additional longitude
and latitude
as a coordinates
tuple, and we also add in the country’s name
.
我们使用countries[status.country]
获取我们国家的信息,并返回带有附加longitude
和latitude
先前数据对象作为coordinates
元组,并且还添加了国家的name
。
So now our data is an array of objects that look like this :
所以现在我们的数据是一个看起来像这样的对象数组:
{
cases: 5244238
coordinates: [(-97, 38)]
country: "US"
deaths: 167029
last_update: "2020-08-13T23:27:24"
name: "United States of America"
recovered: 1774648
}
We can pass this array to a ScatterplotLayer
provided by deck.gl
, which allows us to display dots on the map.
我们可以将此数组传递给ScatterplotLayer
通过提供deck.gl
,这使我们能够在地图上显示点。
The ScatterplotLayer
accepts functions to define the corresponding position, radius and color of each dot. Those function all receive our data object as a parameter.
ScatterplotLayer
接受用于定义每个点的相应位置,半径和颜色的函数。 这些函数都将我们的数据对象作为参数。
We use getPosition
to return our data’s coordinates
as the dot’s position.
我们使用getPosition
返回数据的coordinates
作为点的位置。
The radius is determined by first calculating the base 10 logarithm of our number of cases, adding a our cases divided by 100,000 and multiplying the result by 20,000.
首先通过计算案例数的底数10对数,再将案例数除以100,000,再将结果乘以20,000,即可确定半径。
The logarithm helps us smooth out the differences between countries, we do this because the USA and Brazil have numbers in the millions whereas other countries are in the hundreds or tens of thousand cases.
对数可帮助我们消除国家之间的差异,因为美国和巴西的数字为数百万,而其他国家的数字为数十万或数万。
As the logarithm removes too much difference, we also add a fraction of our cases back in. Then we scale by an arbitrary number, here 20,000, so the dots get a visible size.
由于对数消除了太大的差异,因此我们还增加了一部分案例。然后我们按任意数字缩放,这里为20,000,因此点的大小可见。
I’m no data visualization expert, so I’ve come up with this formula mainly by experimenting and I wouldn’t trust my math too much here.
我不是数据可视化专家,所以我主要通过实验得出了这个公式,在这里我不会太相信我的数学。
This formula works nicely for the current data but it won’t adapt, but for now it’ll do. We’ll improve it later.
该公式对于当前数据非常有效,但无法适应,但现在可以了。 我们稍后会对其进行改进。
getFillColor
should return an array of red, green and blue with values between 0 and 255. You can add a fourth value to define the opacity.
getFillColor
应该返回一个红色,绿色和蓝色的数组,其值在0到255之间。您可以添加第四个值来定义不透明度。
Here we simply make our dots red by returning [255,0,0]
every time. We’ll also improve this later.
在这里,我们只需通过每次返回[255,0,0]
来使点变成红色。 我们稍后也会对此进行改进。
Update App.js
更新App.js
import React, { Suspense } from "react"
import { RecoilRoot } from "recoil"
import DataMap from "./components/map/DataMap"
const App = () => {
return (
)
}
export default App
The map now displays red dots of varying size corresponding to the number of cases for each country !
现在,该地图会显示不同大小的红点,对应于每个国家的案件数量!
赋予用户穿越时空的能力 (Giving users the ability to travel through time)
It’s time to build the time machine, and by that I mean we’re going to first add buttons to travel forward or backward in time. Later, we’ll also add a slider.
现在该建造时间机器了,我的意思是,我们首先要添加按钮以使时间向前或向后移动。 稍后,我们还将添加一个滑块。
We’re going to need some state to keep track of the date we’re currently viewing data for.
我们将需要一些状态来跟踪当前正在查看数据的日期。
We’ll also need a range of dates, if you look at the WHO’s timeline of COVID-19, you’ll see the first cases were on January 10th, this will be our start date. And the end will be today.
我们还需要一定的日期范围,如果您看一下WHO的COVID-19时间表 ,您会看到第一个病例是1月10日,这就是我们的开始日期。 到今天结束。
Inside our src/state
folder, create a new app
folder, then create a new index.js
file inside
在我们的src/state
文件夹中,创建一个新的app
文件夹,然后在其中创建一个新的index.js
文件
import { atom } from "recoil"
export const DATE_RANGE = [new Date("10 Jan 2020"), new Date()]
export const currentDateState = atom({
key: "current-date-state",
default: new Date(),
})
That’s actually all the state code we need to make this work.
实际上,这是我们完成这项工作所需的所有状态代码。
Let’s change our DataMap
and remove all the dependencies to our app’s state so this becomes purely a display component.
让我们更改DataMap
并删除对应用程序状态的所有依赖关系,以使它完全成为显示组件。
We’ll also have to pass down the mapboxToken
我们还必须传递mapboxToken
Modify DataMap
修改DataMap
import React from "react"
import DeckGLMap from "./DeckGLMap"
import { ScatterplotLayer } from "@deck.gl/layers"
const DataMap = ({ mapboxToken = "", data = [] }) => {
const scatterplotLayer = new ScatterplotLayer({
id: "scatterplot-layer",
data,
stroked: false,
filled: true,
getPosition: (d) => d.coordinates,
getRadius: (d) => (d.cases > 0 ? (Math.log10(d.cases) + d.cases / 100000) * 20000 : 0),
getFillColor: (d) => [255, 0, 0],
})
const layers = [scatterplotLayer]
return
}
export default DataMap
Remove the dependency on our token file from DeckGLMap
as well
DeckGLMap
从DeckGLMap
删除对令牌文件的依赖
import React from "react"
import DeckGL from "@deck.gl/react"
import { StaticMap } from "react-map-gl"
// Viewport settings
const INITIAL_VIEW_STATE = {
longitude: -20,
latitude: 0,
pitch: 0,
zoom: 2,
bearing: 0,
}
const DeckGLMap = ({ mapboxToken = "", layers = [] }) => {
return (
)
}
export default DeckGLMap
We’ll create a new TimelineMap
container component that will handle our app specific state manipulation.
我们将创建一个新的TimelineMap
容器组件,该组件将处理应用程序特定的状态操作。
components/map/TimelineMap.js
components/map/TimelineMap.js
import React from "react"
import { useRecoilValue } from "recoil"
import { format } from "date-fns"
import { mapboxToken } from "../../mapbox-token"
import { currentDateState } from "../../state/app"
import { API_DATE_FORMAT, statusByDateQuery, countriesQuery } from "../../state/api"
import DataMap from "./DataMap"
const TimelineMap = () => {
const viewDate = useRecoilValue(currentDateState)
const formattedDate = format(viewDate, API_DATE_FORMAT)
const dateStatus = useRecoilValue(statusByDateQuery(formattedDate))
const countries = useRecoilValue(countriesQuery)
const data = dateStatus.map((status) => {
const country = countries[status.country]
return {
name: country.name,
coordinates: [country.longitude, country.latitude],
...status,
}
})
return
}
export default TimelineMap
Update App.js
更新App.js
import React, { Suspense } from "react"
import { RecoilRoot } from "recoil"
import TimelineMap from "./components/map/TimelineMap"
const App = () => {
return (
)
}
export default App
Before we create our controls, let’s create a simple button with some styles.
在创建控件之前,让我们创建一个具有某些样式的简单按钮。
We’ll use the styled-components
npm package to style it, which allows us to write CSS-in-JS.
我们将使用styled-components
npm包对其进行样式设置,这允许我们编写CSS-in-JS。
yarn add styled-components
styled-components
allows us to define React components that are used only for styling.
styled-components
允许我们定义仅用于样式的React组件。
You write your CSS in a template literal and it supports SASS like nesting. Let’s see how it works with our Button
component.
您用模板文字编写CSS,它支持SASS(例如嵌套)。 让我们看看它如何与Button
组件一起工作。
Create a components/ui
folder and a new Button.js
file inside
在其中创建一个components/ui
文件夹和一个新的Button.js
文件
import React from "react"
import styled from "styled-components"
// styled component
const StyledButton = styled("button")`
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: center;
background-color: #4299e1;
color: #bee3f8;
padding: 0.75rem 1.5rem;
outline: none;
border: none;
font-size: 1rem;
cursor: pointer;
&:hover {
background-color: #63b3ed;
color: #ebf8ff;
}
`
// actual button component
const Button = ({ children, ...props }) => {
return {children}
}
export default Button
To keep this simple, I won’t use a theme for color management and just copy colors from the TailwindCSS palette.
为简单起见,我不会使用主题进行颜色管理,而只是从TailwindCSS调色板中复制颜色。
Here we use styled("button")
to create a styled component using a button HTML element.
在这里,我们使用styled("button")
使用按钮HTML元素创建样式化的组件。
Our actual Button
component just returns its children
wrapped in that styled component.
我们的实际Button
组件刚刚返回它的children
包裹在风格的组成部分。
We also pass down the rest of our original props to our root element so that we can bind an onClick
event listener for example.
我们还将其余的原始道具传递给根元素,例如,可以绑定onClick
事件监听器。
Now we’re ready to use those buttons to make time travel real.
现在,我们准备使用这些按钮来使时间旅行变得真实。
Create a components/time
folder and a TimeTravelButtons.js
component inside.
在其中创建一个components/time
文件夹和一个TimeTravelButtons.js
组件。
import React from "react"
import styled from "styled-components"
import { useRecoilState } from "recoil"
import { currentDateState } from "../../state/app"
import { addDays, subDays } from "date-fns"
import Button from "../ui/Button"
const StyledTimeTravelButtons = styled("div")`
display: flex;
flex-flow: row nowrap;
align-items: center;
position: absolute;
left: 0;
top: 0;
z-index: 50;
.separator {
height: 100%;
width: 1px;
background-color: #bee3f8;
}
`
const TimeTravelButtons = () => {
const DAYS_JUMP = 10
const [currentDate, setCurrentDate] = useRecoilState(currentDateState)
const jumpForward = (e) => {
setCurrentDate(addDays(currentDate, DAYS_JUMP))
}
const jumpBackward = (e) => {
setCurrentDate(subDays(currentDate, DAYS_JUMP))
}
return (
)
}
export default TimeTravelButtons
We use the same pattern as before with styled("div")
to create our styled container as an HTML div element this time.
这次,我们使用与styled("div")
相同的模式来将样式化的容器创建为HTML div元素。
We setup absolute positioning to the top left corner of our container and augment the z-index
to make sure it appears on top of the map.
我们将绝对定位设置到容器的左上角,并扩大z-index
以确保其出现在地图顶部。
Inside the component, we define a constant for the number of days to skip forwards or backwards.
在组件内部,我们定义了向前或向后跳过的天数的常量。
Note the use of useRecoilState
here instead of useRecoilValue
to get our currentDateState
atom
注意这里使用useRecoilState
而不是useRecoilValue
来获取我们的currentDateState
atom
This is because we don’t want just the value of our state, we also want to be able to set it. useRecoilState
works like useState
from React, it will return an array with [value, setValue]
这是因为我们不仅想要状态的值,还希望能够对其进行设置。 useRecoilState
作用类似于React的useState
,它将返回一个带有[value, setValue]
的数组
We define a jumpForward
function and a jumpBackward
function, these will addDays
or subtractDays
from our currentDate
and then use the setCurrentDate
function to update our currentDateState
我们定义了一个jumpForward
函数和一个jumpBackward
函数,它们将从我们的currentDate
addDays
或subtractDays
,然后使用setCurrentDate
函数更新我们的currentDateState
Update App.js
更新App.js
import React, { Suspense } from "react"
import { RecoilRoot } from "recoil"
import TimelineMap from "./components/map/TimelineMap"
import TimeTravelButtons from "./components/time/TimeTravelButtons"
const App = () => {
return (
)
}
export default App
With that, when our button is clicked, our current date state is updated and our TimelineMap
rerenders.
这样,单击我们的按钮时,我们的当前日期状态将更新,并且我们的TimelineMap
。
TimelineMap
then requests the new status data by calling the API with our new currentDate
and we rerender our map with the updated data.
然后, TimelineMap
通过使用新的currentDate
调用API来请求新的状态数据,然后使用更新的数据重新渲染地图。
Notice that if you click “Backward” a few times, the loading screen always flashes. But if you start clicking “Forward”, going back to dates that we already made the request for, there is no loading screen anymore.
请注意,如果多次单击“向后”,加载屏幕将始终闪烁。 但是,如果您开始单击“转发”,返回到我们已经提出要求的日期,则不再有加载屏幕。
This is because Recoil has memoized our API’s response for those dates and is not making a new API call since it already knows the result. This means we have automatic caching of our API requests !
这是因为Recoil已记住了这些日期我们API的响应,并且由于已经知道结果而没有进行新的API调用。 这意味着我们可以自动缓存我们的API请求!
Let’s also display the current date above our buttons so we know which date we’re looking at.
让我们还在按钮上方显示当前日期,以便我们知道要查看的日期。
import React from "react"
import styled from "styled-components"
import { useRecoilState } from "recoil"
import { currentDateState } from "../../state/app"
import { addDays, subDays, format } from "date-fns"
import Button from "../ui/Button"
const StyledTimeTravelButtons = styled("div")`
display: flex;
flex-flow: column;
align-items: center;
position: absolute;
left: 0;
top: 0;
z-index: 50;
.current-date {
padding: 1rem;
width: 100%;
background-color: #667eea;
color: #c3dafe;
text-align: center;
}
.buttons {
display: flex;
flex-flow: row-nowrap;
align-items: center;
.separator {
height: 100%;
width: 1px;
background-color: #bee3f8;
}
}
`
const TimeTravelButtons = () => {
const DAYS_JUMP = 10
const [currentDate, setCurrentDate] = useRecoilState(currentDateState)
const jumpForward = (e) => {
setCurrentDate(addDays(currentDate, DAYS_JUMP))
}
const jumpBackward = (e) => {
setCurrentDate(subDays(currentDate, DAYS_JUMP))
}
return (
{format(currentDate, "LLLL do, yyyy")}
)
}
export default TimeTravelButtons
We wrapped our buttons in a new buttons
container div and moved its styles inside a buttons
class in the styled component.
我们将按钮包装在新的buttons
容器div中,并将其样式移动到样式组件中的buttons
类中。
We changed our styled component’s flow to column
我们将样式化组件的流程更改为column
We added a new current-date
div to display our formatted date.
我们添加了一个新的current-date
div以显示格式化日期。
If you’d like to know more about how to format the date, check out this documentation.
如果您想进一步了解日期格式,请查阅此文档 。
修复闪烁的加载屏幕 (Fixing the flashing loading screen)
Every time we are fetching data, our whole map is replaced by a white loading screen. It would be much better to have our map always displayed and instead have the loading indicator pop onto it.
每次获取数据时,我们的整个地图都会被白色加载屏幕替换。 始终显示我们的地图,然后在其上弹出加载指示器会更好。
That means we can’t use Suspense anymore and we’ll have to manually handle the loading state.
这意味着我们不能再使用Suspense,而必须手动处理加载状态。
Fortunately, Recoil doesn’t require Suspense and you can just as easily take advantage of useRecoilValueLoadable
.
幸运的是,Recoil不需要Suspense,您可以轻松使用useRecoilValueLoadable
。
Instead of our value, we’ll get an object with a state
which will be "loading"
when we’re loading, "hasValue"
when we got our data or "hasError"
if it failed to load.
取而代之的是,我们获得的对象的state
为:正在加载时将处于"loading"
正在加载"hasValue"
当获得数据时将处于"hasValue"
状态;如果加载失败,则状态为"hasError"
。
Let’s start by building our loading indicator, to keep things simple, we’ll just use react-loader-spinner
npm package.
让我们从构建加载指示器开始,为简单起见 ,我们仅使用react-loader-spinner
npm package 。
yarn add react-loader-spinner
Inside components/ui
, create a LoadingIndicator.js
component
在components/ui
内部,创建一个LoadingIndicator.js
组件
import React from "react"
import styled from "styled-components"
import Loader from "react-loader-spinner"
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"
const StyledLoadingIndicator = styled("div")`
position: absolute;
top: 1rem;
left: 50%;
transform: translateX(-50%);
z-index: 50;
`
const LoadingIndicator = () => {
return (
)
}
export default LoadingIndicator
Modify our TimelineMap
to use useRecoilValueLoadable
instead of useRecoilValue
for our asynchronous queries.
修改我们的TimelineMap
以将useRecoilValueLoadable
代替useRecoilValue
用于异步查询。
import React from "react"
import { useRecoilValue, useRecoilValueLoadable } from "recoil"
import { format } from "date-fns"
import { mapboxToken } from "../../mapbox-token"
import { currentDateState } from "../../state/app"
import { API_DATE_FORMAT, statusByDateQuery, countriesQuery } from "../../state/api"
import DataMap from "./DataMap"
import LoadingIndicator from "../ui/LoadingIndicator"
const TimelineMap = () => {
const viewDate = useRecoilValue(currentDateState)
const formattedDate = format(viewDate, API_DATE_FORMAT)
const dateStatus = useRecoilValueLoadable(statusByDateQuery(formattedDate))
const countries = useRecoilValueLoadable(countriesQuery)
const isLoading = dateStatus.state === "loading" || countries.state === "loading"
let data = []
if (!isLoading) {
data = dateStatus.contents.map((status) => {
const country = countries.contents[status.country]
return {
name: country.name,
coordinates: [country.longitude, country.latitude],
...status,
}
})
}
return (
{isLoading ? : null}
)
}
export default TimelineMap
Now we check if we’re loading with dateStatus.state
and countries.state
现在我们检查,如果我们用装载dateStatus.state
和countries.state
When we’re not loading, we build our data as before but using dateStatus.contents
and countries.contents
to access the value of our state.
当我们不加载,我们建立我们的数据之前,但使用dateStatus.contents
和countries.contents
访问我们国家的价值。
When we are loading, our data is just an empty array.
当我们加载时,我们的数据只是一个空数组。
Now, the map doesn’t disappear anymore and the loading indicator only shows up on top when needed, much better.
现在,地图不再消失,加载指示器仅在需要时显示在顶部,更好。
及时限制我们的用户 (Bounding our users in time)
Another problem is that we can actually go into the future and past our start date.
另一个问题是,我们实际上可以进入未来,也可以超过开始日期。
We’d like to make sure we always stay between our start date and end date instead.
我们想确保我们始终停留在开始日期和结束日期之间。
Right now, we’re using an atom
for our currentDateState
, this works well but it doesn’t allow us to put restrictions on the possible values of currentDate
.
现在,我们为我们的currentDateState
使用一个atom
,这很好用,但是它不允许我们对currentDate
的可能值施加限制。
We’ll need to make our atom
inaccessible by not exporting it anymore and we’ll instead export a new selector
that will allow us to control how we set
our atom
‘s value.
我们需要通过不再导出atom
来使其不可访问,而我们将导出一个新的selector
,该selector
将使我们能够控制如何set
atom
的值。
Modify state/app/index.js
修改state/app/index.js
import { selector, atom } from "recoil"
import { isAfter, isBefore } from "date-fns"
export const DATE_RANGE = [new Date("10 Jan 2020"), new Date()]
const dateState = atom({
key: "date-state",
default: DATE_RANGE[1],
})
export const currentDateState = selector({
key: "current-date-state",
get: ({ get }) => {
return get(dateState)
},
set: ({ set }, nextDate) => {
let newDate = nextDate
if (isAfter(newDate, DATE_RANGE[1])) newDate = DATE_RANGE[1]
if (isBefore(newDate, DATE_RANGE[0])) newDate = DATE_RANGE[0]
set(dateState, newDate)
},
})
We can see our atom
‘s code didn’t change, but we renamed it so we can create a new selector
with the previous name of our atom, this is so we don’t have to refactor every component that’s using it right now.
我们可以看到atom
的代码没有改变,但是我们对其进行了重命名,因此我们可以使用atom的先前名称创建一个新的selector
,这样就不必重新构造每个正在使用它的组件。
When we define our selector
, the get
option should be a function which returns our value.
当我们定义selector
, get
选项应该是一个返回我们值的函数。
selector({
key: "example",
get: ({ get }) => {}
})
This function receives an object as parameter, this object contains a get
property.
该函数接收一个对象作为参数,该对象包含一个get
属性。
This get
property is in fact a function that allows us to get
the value of another atom
or selector
实际上,此get
属性是一个函数,允许我们get
另一个atom
或selector
的值
Here we simply use it to return the value of our dateState
atom
with get(dateState)
在这里,我们简单地使用它通过get(dateState)
返回我们的dateState
atom
的值。
We will control the value of our dateState
by adding some checks on our set
option.
我们将通过在set
选项上添加一些检查来控制dateState
的值。
The set
selector
option expects a similar function which will also receive a get
property (but we don’t use it here) and a set
property.
set
selector
选项需要一个类似的函数,该函数还将接收一个get
属性(但在此不使用它)和一个set
属性。
selector({
key: "example",
get: ({ get }) => {},
set: ({ get, set }, newValue) => {}
})
The set
property is also a function, it allows us to set the state of another atom
or selector
(provided that this selector
is writable, meaning it also has a set
option defined)
set
属性也是一个函数,它允许我们设置另一个atom
或selector
的状态(前提是该selector
是可写的,这意味着它也定义了set
选项)
The second argument, after the { get, set }
object, is the new value we are trying to set.
{ get, set }
对象之后的第二个参数是我们尝试设置的新值。
We first create a new variable newDate
to hold on to the new value, then we use isAfter
and isBefore
functions from date-fns
to check that the date is within our date range, if any of those if
statements pass, they will update the newDate
to be the corresponding allowed date instead.
我们首先创建一个新变量newDate
来保持新值,然后使用date-fns
isAfter
和isBefore
函数检查日期是否在我们的日期范围内,如果任何if
语句通过,它们将更新newDate
改为对应的允许日期。
Then we simply set
our dateState
atom
to the newDate
value.
然后,我们只需set
dateState
atom
设置为newDate
值。
Now there is no way to go outside of our bounds.
现在没有办法走出我们的界限。
Let’s also make sure our forward button is disabled when we’re at the end date, and our backward button is disabled when we’re at the start bound.
我们还要确保在结束日期时禁用前进按钮,而在开始时禁用后退按钮。
Our button currently doesn’t have proper styles for it’s disabled state so we need to add them first.
我们的按钮目前没有合适的样式,因为它处于禁用状态,因此我们需要先添加它们。
Modify components/ui/Button
修改components/ui/Button
import React from "react"
import styled from "styled-components"
const StyledButton = styled("button")`
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: center;
background-color: #4299e1;
color: #bee3f8;
padding: 0.75rem 1.5rem;
outline: none;
border: none;
font-size: 1rem;
cursor: pointer;
&:hover {
background-color: #63b3ed;
color: #ebf8ff;
}
&:disabled {
background-color: #e2e8f0;
color: #edf2f7;
cursor: initial;
}
`
const Button = ({ children, ...props }) => {
return {children}
}
export default Button
Note that we added our styles for the disabled state after the rest, this is to make sure that they override anything else. If you put them before the &:hover
styles, your button will look disabled, but still get the hover styles when you hover over it.
请注意,我们在其余部分之后为禁用状态添加了样式,以确保它们覆盖其他所有样式。 如果将它们放在&:hover
样式之前,则按钮将显示为禁用状态,但将鼠标悬停在其上时仍会获得悬停样式。
Modify our TimeTravelButtons
to set the disabled
state accordingly.
修改我们的TimeTravelButtons
以TimeTravelButtons
地设置disabled
状态。
import React from "react"
import styled from "styled-components"
import { useRecoilState } from "recoil"
import { currentDateState, DATE_RANGE } from "../../state/app"
import { addDays, subDays, format } from "date-fns"
import Button from "../ui/Button"
const StyledTimeTravelButtons = styled("div")`
display: flex;
flex-flow: column;
align-items: center;
position: absolute;
left: 0;
top: 0;
z-index: 50;
.current-date {
padding: 1rem;
width: 100%;
background-color: #667eea;
color: #c3dafe;
text-align: center;
}
.buttons {
display: flex;
flex-flow: row-nowrap;
align-items: center;
.separator {
height: 100%;
width: 1px;
background-color: #bee3f8;
}
}
`
const TimeTravelButtons = () => {
const DAYS_JUMP = 10
const [currentDate, setCurrentDate] = useRecoilState(currentDateState)
const jumpForward = (e) => {
setCurrentDate(addDays(currentDate, DAYS_JUMP))
}
const jumpBackward = (e) => {
setCurrentDate(subDays(currentDate, DAYS_JUMP))
}
return (
{format(currentDate, "LLLL do, yyyy")}
)
}
export default TimeTravelButtons
We imported our DATE_RANGE
and set the disabled
attribute of each button by comparing our currentDate
to our bounds.
我们导入了DATE_RANGE
并通过将currentDate
与边界进行比较来设置每个按钮的disabled
属性。
Be aware that we’re dealing with reference types here and it is important that our set
selector
function is actually using the same reference to DATE_RANGE[0]
and DATE_RANGE[1]
as our comparison.
请注意,我们在这里处理引用类型,并且重要的是,我们的set
selector
函数实际上使用与DATE_RANGE[0]
和DATE_RANGE[1]
相同的引用作为我们的比较。
添加时间旅行滑块 (Adding a time travel slider)
We already have all the state we need to add our slider.
我们已经拥有添加滑块所需的所有状态。
Create a new component inside components/time
called TimeTravelSlider.js
在components/time
内部创建一个名为TimeTravelSlider.js
的新组件
import React from "react"
import styled from "styled-components"
import { useRecoilState } from "recoil"
import { currentDateState, DATE_RANGE } from "../../state/app"
import { differenceInCalendarDays, addDays } from "date-fns"
const StyledTimeTravelSlider = styled("div")`
display: flex;
flex-flow: column;
align-items: center;
position: absolute;
bottom: 5rem;
left: 0;
width: 100%;
z-index: 50;
input {
width: 96%;
margin: 0 2%;
}
`
const TimeTravelSlider = () => {
const [currentDate, setCurrentDate] = useRecoilState(currentDateState)
const maxSliderPos = differenceInCalendarDays(DATE_RANGE[1], DATE_RANGE[0])
const currentSliderPos = differenceInCalendarDays(currentDate, DATE_RANGE[0])
const handleDateChange = (e) => {
const sliderPos = e.target.value
const nextDate = addDays(DATE_RANGE[0], sliderPos)
setCurrentDate(nextDate)
}
return (
)
}
export default TimeTravelSlider
We style our component to be at the bottom left of our container, with some space from the bottom so it’s not hard to reach.
我们将组件的样式设置为位于容器的左下方,并在其底部留出一些空间,这样就不难达到目的。
It will take the full width with a bit of space on the sides with width:96%;
and margin:2%;
(margin will be applied on both sides, so 4%
in total).
它将占据整个宽度,并在侧面上留出一些width:96%;
, width:96%;
margin:2%;
(保证金将同时应用于两边,因此总计为4%
)。
We use useRecoilState
because we also need to be able to set our state when the slider position changes.
我们使用useRecoilState
是因为当滑块位置更改时我们还需要能够设置状态。
We use the differenceInCalendarDays
between our end date and start date to calculate how many days we could potentially go to, this will be the max
attribute of our range
input
.
我们使用differenceInCalendarDays
我们的最终日期间和起始日期计算,我们有多少天可能会去,这将是max
我们的属性range
input
。
Note that the order of the parameters in differenceInCalendarDays
matters as we need a positive number here, so we need to always pass the date that is the furthest first.
请注意, differenceInCalendarDays
参数的顺序很重要,因为这里需要一个正数,因此我们需要始终传递最远的日期。
We determine our current slider position by calculating the difference in days between our currentDate
and our start date DATE_RANGE[0]
我们通过计算currentDate
和开始日期DATE_RANGE[0]
之间的天数差来确定当前的滑块位置
We listen to the slider’s value change with handleDateChange
, which gets the current slider position with e.target.value
. Then we calculate the new corresponding date by adding the slider’s new value to our start date DATE_RANGE[0]
and use the setter function to update currentDateState
我们使用handleDateChange
监听滑块的值更改,该参数通过e.target.value
获取当前滑块的位置。 然后,我们通过将滑块的新值添加到开始日期DATE_RANGE[0]
来计算新的对应日期,并使用setter函数更新currentDateState
Update App.js
更新App.js
import React, { Suspense } from "react"
import { RecoilRoot } from "recoil"
import TimelineMap from "./components/map/TimelineMap"
import TimeTravelButtons from "./components/time/TimeTravelButtons"
import TimeTravelSlider from "./components/time/TimeTravelSlider"
const App = () => {
return (
)
}
export default App
And with that we can move through time by sliding !
这样,我们就可以通过滑动来穿越时间!
Once again, once you’ll have loaded each day, the loading won’t show up anymore and our map updates instantly as you move the slider.
同样,每天加载一次后,加载将不再显示,并且在您移动滑块时我们的地图会立即更新。
Since we’re manipulating the same state, our display of the date in the top left corner also updates accordingly.
由于我们正在处理相同的状态,因此我们在左上角的日期显示也会相应地更新。
在滑块上显示时间距离 (Showing distance in time on the slider)
Let’s also add the distance in time to now above the slider’s handle, this will make the slider’s purpose more obvious.
我们还要及时添加到滑块手柄上方的距离,这将使滑块的目的更加明显。
import React, { useMemo } from "react"
import styled from "styled-components"
import { useRecoilState } from "recoil"
import { currentDateState, DATE_RANGE } from "../../state/app"
import { differenceInCalendarDays, addDays, formatDistanceToNow } from "date-fns"
const StyledTimeTravelSlider = styled("div")`
display: flex;
flex-flow: column;
align-items: center;
position: absolute;
bottom: 5rem;
left: 0;
width: 100%;
z-index: 50;
.slider-container {
position: relative;
width: 100%;
.time-info {
position: absolute;
bottom: 1.5rem;
background-color: #4299e1;
color: #f7fafc;
padding: 1rem;
white-space: nowrap;
border-radius: 0.25rem;
}
input {
width: 96%;
margin: 0 2%;
}
}
`
const TimeTravelSlider = () => {
const [currentDate, setCurrentDate] = useRecoilState(currentDateState)
const maxSliderPos = differenceInCalendarDays(DATE_RANGE[1], DATE_RANGE[0])
const currentSliderPos = differenceInCalendarDays(currentDate, DATE_RANGE[0])
const sliderPercentage = (currentSliderPos * 96) / maxSliderPos + 2
const handleDateChange = (e) => {
const sliderPos = e.target.value
const nextDate = addDays(DATE_RANGE[0], sliderPos)
setCurrentDate(nextDate)
}
const timeInfoStyles = useMemo(
() => ({
left: `${sliderPercentage}%`,
transform: `translateX(${sliderPercentage > 15 ? "-100" : "0"}%`,
}),
[sliderPercentage]
)
return (
{formatDistanceToNow(currentDate, { addSuffix: true })}
)
}
export default TimeTravelSlider
Here we’ve added a new slider-container
div to hold both the slider and the new time information in the new time-info
div.
在这里,我们添加了一个新的slider-container
div,以将滑块和新的时间信息都保存在新的time-info
div中。
We set this container to position: relative;
so it’ll be easy to position our time-info
relative to the slider-container
instead of relative to the whole document.
我们将此容器设置为position: relative;
因此,将我们的time-info
相对于slider-container
而不是相对于整个文档放置起来很容易。
With that, we can add some styles to our time-info
, we set it to sit above our slider by specifying bottom: 1.5rem;
and use white-space: nowrap;
to make sure its content displays on one line.
这样,我们可以为time-info
添加一些样式,通过指定bottom: 1.5rem;
来将其设置为位于滑块上方bottom: 1.5rem;
并使用white-space: nowrap;
确保其内容显示在一行上。
We use formatDistanceToNow
to get our current date expressed as a distance in time from now, such as “a month ago”.
我们使用formatDistanceToNow
来获取当前日期,该日期以距现在的时间formatDistanceToNow
表示,例如“一个月前”。
We then need to make time-info
follow our slider’s current position, we do this by calculating the current percentage position with currentSliderPos * 96 / maxSliderPos + 2
, we use 96
because we set our input
to 96% width and add our 2% margin
with +2
然后,我们需要使time-info
跟随滑块的当前位置,这是通过使用currentSliderPos * 96 / maxSliderPos + 2
计算当前百分比位置来实现的,我们使用96
因为我们将input
设置为96%的宽度并添加了2%的margin
+2
This will give us the exact left percentage we need to apply so the left side of our time-info
so it will be exactly at our slider’s handle.
这将为我们提供需要应用的确切左侧百分比,因此time-info
的左侧将恰好在滑块的手柄处。
We then use translateX(-100%)
so that it’s our that right side that will be exactly at our handle’s position, but we only apply this if our sliderPercentage
is over 15, which will make sure we don’t overflow to the left when we slide near the left side of the screen.
然后,我们使用translateX(-100%)
,使它的右侧恰好在手柄的位置,但是仅当sliderPercentage
大于15时才应用此值,这将确保在出现以下情况时不会溢出到左侧我们在屏幕左侧滑动。
We put all this in a new timeInfoStyles
object that we wrap in useMemo
with a unique dependency on sliderPercentage
我们将所有这些放入一个新的timeInfoStyles
对象中,该对象包装在useMemo
,并且对sliderPercentage
具有唯一的依赖性
This ensures that we only recalculate our styles when sliderPercentage
changes, and we haven’t performed the calculation for this particular value yet.
这样可以确保仅在sliderPercentage
更改时才重新计算样式,并且尚未针对该特定值执行计算。
Then we pass our timeInfoStyles
as the styles
prop of our time-info
div.
然后,我们将timeInfoStyles
作为time-info
div的styles
道具。
I avoid doing it with styled-components
, even though it’s possible, because it seems to degrade performance quite a lot.
即使可能,我也避免使用styled-components
,因为这似乎会大大降低性能。
预取数据以保持UX流畅 (Prefetching data to keep our UX smooth)
Right now, we’re fetching each day one by one. It would be nice if we could prefetch the days that our user has a high chance to move to next in the background so they don’t have to see the loading every time they move.
现在,我们每天都在逐一获取。 如果我们可以预取用户有很大机会在后台移动到下一个站点的日子,那很好,那么他们不必每次移动时都看到负载。
And we can repeat this process for every day they’re looking at, so that every time we’re at a given day, the next days will start prefetching.
我们可以在他们每天看的每一天都重复此过程,以便每次我们在给定的一天,接下来的几天都将开始预取。
This way, if the user doesn’t move to fast for the prefetching to happen in the background, it will actually seem like there is no loading at all.
这样,如果用户没有快速移动以在后台进行预取,则实际上似乎根本没有加载。
We’ll do this by using waitForNone
, you can read about it here.
我们将通过使用waitForNone
完成此waitForNone
,您可以在此处阅读有关内容。
waitForNone
will allow us to also run other queries without waiting for the result, and since Recoil will memoize them, they will already be cached when we move to one of the days that we prefetched, hence no loading.
waitForNone
将允许我们也运行其他查询而无需等待结果,并且由于Recoil会记住它们,因此当我们移至预取的某一天时,它们将已经被缓存,因此不会加载。
Modify our state/api/index.js
修改我们的state/api/index.js
import { selector, selectorFamily, waitForNone } from "recoil"
import { format, subDays, differenceInCalendarDays } from "date-fns"
import { currentDateState, DATE_RANGE } from "../app"
export const API_DATE_FORMAT = "yyyy-MM-dd"
export const countriesQuery = selector({
key: "countries",
get: async () => {
try {
const res = await fetch("https://covid19-api.org/api/countries")
const countries = await res.json()
return countries.reduce((dict, country) => {
dict[country.alpha2] = country
return dict
}, {})
} catch (e) {
console.error("ERROR GET /countries", e)
}
},
})
export const statusByDateQuery = selectorFamily({
key: "status-by-date-query",
get: (formattedDate) => async ({ get }) => {
try {
const res = await fetch(`https://covid19-api.org/api/status?date=${formattedDate}`)
const status = await res.json()
return status
} catch (e) {
console.error("status by date error", e)
}
},
})
const PREFETCH_DAYS = 90
export const currentDateStatusState = selector({
key: "current-date-status-state",
get: async ({ get }) => {
const currentDate = get(currentDateState)
const formattedDate = format(currentDate, API_DATE_FORMAT)
const status = await get(statusByDateQuery(formattedDate))
// prefetch previous days
const toPrefetchDates = new Array(PREFETCH_DAYS)
.fill(0)
.map((_, i) => {
const date = subDays(currentDate, i + 1)
const diff = differenceInCalendarDays(date, DATE_RANGE[0])
return diff >= 0 ? format(date, API_DATE_FORMAT) : null
})
.filter((date) => date)
const prefetchQueries = toPrefetchDates.map((date) => statusByDateQuery(date))
get(waitForNone(prefetchQueries))
return status
},
})
We start by defining a constant for the number of days to prefetch PREFETCH_DAYS
, ideally you would set this to the smallest number possible, I think 90 works well in our case.
我们首先为预取PREFETCH_DAYS
天数定义一个常数,理想情况下,您PREFETCH_DAYS
设置为尽可能小的数字,我认为90在我们的情况下效果很好。
Then we create a new currentDateStatusState
selector
that will be responsible for returning the status data for the current date.
然后,我们创建一个新的currentDateStatusState
selector
,该selector
将负责返回当前日期的状态数据。
First, we get
our currentDateState
‘s value which will be our current Date
, then we format it for our API in formattedDate
.
首先,我们get
currentDateState
的值(即当前的Date
,然后使用formattedDate
为我们的API设置其formattedDate
。
We get our data by calling our statusByDateQuery(formattedDate)
as before and make sure we await
the result so we can return our status data for the current date as soon as it is ready.
我们通过像以前一样调用statusByDateQuery(formattedDate)
来获取数据,并确保我们await
结果,以便可以在准备好日期后立即返回当前日期的状态数据。
We then create a prefetchDates
array by using new Array(PREFETCH_DAYS).fill(0)
so we get an array of length PREFETCH_DAYS
然后,我们使用new Array(PREFETCH_DAYS).fill(0)
创建一个prefetchDates
数组,以便获得长度为PREFETCH_DAYS
的数组
We map over this array to fill it with the proper dates by subtracting our current index +1 days off the current date and format it for our API.
我们在该数组上映射,以通过从当前日期减去当前索引+1天来填充适当的日期,并为我们的API设置格式。
Since the index starts at 0, we add 1 to avoid fetching the current date’s data again.
由于索引从0开始,因此我们加1以避免再次获取当前日期的数据。
To avoid fetching unnecessary dates in the past, we also return null
in case the calculated date’s differenceInCalendarDays
with our start date is negative and filter out the falsy values from the array.
为了避免过去取不必要的日期,我们也返回null
,如果计算日期的differenceInCalendarDays
我们的开始日期为负,并且过滤掉从数组中falsy值。
We obtain an array of the dates we need to prefetch in our API’s expected format.
我们以API的预期格式获取了需要预取的日期的数组。
We create a new prefetchQueries
array by mapping over our dates array and returning a statusByDateQuery(date)
instead.
我们通过映射我们的date数组并返回一个statusByDateQuery(date)
来创建一个新的prefetchQueries
数组。
We then use get(waitForNone(prefetchQueries))
to actually make the requests happen. Because waitForNone
won’t wait for the result of the queries, this will happen in the background and be transparent to the user.
然后,我们使用get(waitForNone(prefetchQueries))
实际使请求发生。 因为waitForNone
不会等待查询结果,所以这将在后台发生并且对用户透明。
We’ll need to modify our TimelineMap
to use our new currentDateStatusState
我们需要修改我们的TimelineMap
以使用新的currentDateStatusState
import React from "react"
import { useRecoilValueLoadable } from "recoil"
import { mapboxToken } from "../../mapbox-token"
import { countriesQuery, currentDateStatusState } from "../../state/api"
import DataMap from "./DataMap"
import LoadingIndicator from "../ui/LoadingIndicator"
const TimelineMap = () => {
const dateStatus = useRecoilValueLoadable(currentDateStatusState)
const countries = useRecoilValueLoadable(countriesQuery)
const isLoading = dateStatus.state === "loading" || countries.state === "loading"
let data = []
if (!isLoading) {
data = dateStatus.contents.map((status) => {
const country = countries.contents[status.country]
return {
name: country.name,
coordinates: [country.longitude, country.latitude],
...status,
}
})
}
return (
{isLoading ? : null}
)
}
export default TimelineMap
We cleaned up a few lines by the way since our currentDateStatusState
already depends on our currentDateState
, we don’t need to get it’s value and make the conversion to our API’s date format string anymore.
由于currentDateStatusState
已经依赖于currentDateState
,因此我们清理了几行,我们不需要获取它的值并将其转换为API的日期格式字符串。
Every time our current date changes, we’ll get our data for the current date and also prefetch the 90 previous days.
每次当前日期更改时,我们都会获取当前日期的数据,并预取前90天的数据。
Now, if our user doesn’t move the slider too fast and is on a good connection, the previous days will progressively get prefetched every time he moves to a new date and he might not even see any loading at all.
现在,如果我们的用户没有将滑块移动得太快并保持良好的连接状态,则前几天将在每次移动到新的日期时逐渐被预取,甚至根本看不到任何负载。
And since our requests are cached, after the user has moved a bit through time, all the data will be loaded and we won’t have any loading anymore.
而且由于我们的请求已被缓存,因此在用户经过一段时间后,所有数据都将被加载,而我们将不再有任何加载。
显示不同的统计信息 (Displaying different statistics)
Right now, we only display the number of cases but the API also returns to us the number of deaths and recovered.
目前,我们仅显示案件数,但API还会向我们返回死亡人数和已恢复的数目。
We’ll let the user choose the statistic they want to see with a select in the top right corner.
我们将让用户通过右上角的选择来选择他们想要查看的统计信息。
Let’s add new state to hold the available stats and the currently selected stat.
让我们添加新状态来保存可用的统计信息和当前选择的统计信息。
In components/api/index.js
在components/api/index.js
import { selector, atom } from "recoil"
import { isAfter, isBefore } from "date-fns"
export const CASES = "cases"
export const DEATHS = "deaths"
export const RECOVERED = "recovered"
export const availableStats = [CASES, DEATHS, RECOVERED]
export const currentStatState = atom({
key: "current-stat-state",
default: availableStats[0],
})
export const DATE_RANGE = [new Date("10 Jan 2020"), new Date()]
const dateState = atom({
key: "date-state",
default: DATE_RANGE[1],
})
export const currentDateState = selector({
key: "current-date-state",
get: ({ get }) => {
return get(dateState)
},
set: ({ set }, nextDate) => {
let newDate = nextDate
if (isAfter(newDate, DATE_RANGE[1])) newDate = DATE_RANGE[1]
if (isBefore(newDate, DATE_RANGE[0])) newDate = DATE_RANGE[0]
set(dateState, newDate)
},
})
We create a few constants to hold the name of our stats as the API provides them.
当API提供它们时,我们创建了一些常量来保存统计信息的名称。
We create an array of the available stats and an atom
to hold the selected stat that we initialise to be the first stat from the array.
我们创建一个包含可用统计信息的数组和一个atom
以保存选定的统计信息,我们将其初始化为该数组中的第一个统计信息。
Now let’s create a new folder components/stats
and a new CurrentStatSelect.js
component inside
现在让我们在其中创建一个新的文件夹components/stats
和一个新的CurrentStatSelect.js
组件
import React from "react"
import styled from "styled-components"
import { useRecoilState } from "recoil"
import { currentStatState, availableStats } from "../../state/app"
const StyledCurrentStatSelect = styled("div")`
display: flex;
flex-flow: column;
align-items: center;
position: absolute;
top: 1rem;
right: 1rem;
z-index: 50;
width: 10%;
select {
padding: 0.5rem;
border: none;
width: 100%;
}
`
const CurrentStatSelect = () => {
const [currentStat, setCurrentStat] = useRecoilState(currentStatState)
const handleStatChange = (e) => {
const newStat = e.target.value
setCurrentStat(newStat)
}
return (
)
}
export default CurrentStatSelect
Again we get our value and setter function with useRecoilState
in currentStat
andsetCurrentStat
再次使用currentStat
和setCurrentStat
useRecoilState
获取值和设置函数
We define an handleStatChange
handler that will simply set our currentStatState
to be what the user selected.
我们定义一个handleStatChange
处理程序,该处理程序将简单地将currentStatState
设置为用户选择的内容。
We use a select
element with our currentStat
as thevalue
prop and our handleStatChange
listener as the onChange
prop.
我们将select
元素与currentStat
用作value
prop,将handleStatChange
侦听器用作onChange
prop。
We use our array of availableStats
to display an option
for each available stat with it’s value
attribute set to the stat’s name.
我们使用availableStats
数组来显示每个可用统计信息的option
,并将其value
属性设置为统计信息的名称。
We also take care to capitalize our option’s label since the API stat names are all lowercase.
由于API统计信息名称均为小写字母,因此我们也要注意将选项标签大写。
Now we’re ready to display the current stat in our TimelineMap
but before that, we need to modify DataMap
to remove the last dependencies that we have.
现在我们准备在TimelineMap
显示当前状态,但是在此之前,我们需要修改DataMap
以删除我们拥有的最后一个依赖项。
DataMap
is still relying on the fact that we pass a data
object with cases
and coordinates
and that’s not good.
DataMap
仍然依赖于这样的事实,即我们传递带有cases
和coordinates
的data
对象,这不好。
We want the component to allow choosing which key is used for the actual represented value and which key is used to get the coordinates of our dots.
我们希望组件允许选择将哪个键用于实际表示的值,以及哪个键用于获取点的坐标。
import React from "react"
import DeckGLMap from "./DeckGLMap"
import { ScatterplotLayer } from "@deck.gl/layers"
const DataMap = ({ mapboxToken = "", data = [], dotCoordinates = "coordinates", displayStat = "cases" }) => {
const scatterplotLayer = new ScatterplotLayer({
id: "scatterplot-layer",
data,
stroked: false,
filled: true,
getPosition: (d) => d[dotCoordinates],
getRadius: (d) => (d[displayStat] > 0 ? (Math.log10(d[displayStat]) + d[displayStat] / 100000) * 20000 : 0),
getFillColor: (d) => [255, 0, 0],
})
const layers = [scatterplotLayer]
return
}
export default DataMap
Then in TimelineMap
, pass down the current stat
然后在TimelineMap
,传递当前状态
import React from "react"
import { useRecoilValueLoadable, useRecoilValue } from "recoil"
import { mapboxToken } from "../../mapbox-token"
import { countriesQuery, currentDateStatusState } from "../../state/api"
import DataMap from "./DataMap"
import LoadingIndicator from "../ui/LoadingIndicator"
import { currentStatState } from "../../state/app"
const TimelineMap = () => {
const dateStatus = useRecoilValueLoadable(currentDateStatusState)
const countries = useRecoilValueLoadable(countriesQuery)
const currentStat = useRecoilValue(currentStatState)
const isLoading = dateStatus.state === "loading" || countries.state === "loading"
let data = []
if (!isLoading) {
data = dateStatus.contents.map((status) => {
const country = countries.contents[status.country]
return {
name: country.name,
coordinates: [country.longitude, country.latitude],
...status,
}
})
}
return (
{isLoading ? : null}
)
}
export default TimelineMap
We get our currentStat
with useRecoilValue
and pass it down to our DataMap
我们使用useRecoilValue
获取currentStat
并将其传递给我们的DataMap
Update App.js
更新App.js
import React, { Suspense } from "react"
import { RecoilRoot } from "recoil"
import TimelineMap from "./components/map/TimelineMap"
import TimeTravelButtons from "./components/time/TimeTravelButtons"
import TimeTravelSlider from "./components/time/TimeTravelSlider"
import CurrentStatSelect from "./components/stats/CurrentStatSelect"
const App = () => {
return (
)
}
export default App
Now when the user selects a different stat, currentStat
will update and our DataMap
will render the new stat.
现在,当用户选择其他统计信息时, currentStat
将更新,并且我们的DataMap
将呈现新的统计信息。
However, there will be scaling issues as our radius
is pretty much hard coded for the cases data.
但是,将存在缩放问题,因为对于案例数据,我们的radius
几乎是硬编码的。
To fix that, we’ll have to take into account each stat’s max value and change our hard coded 100 000
with a fraction of our max value.
要解决此问题,我们必须考虑每个统计信息的最大值,并用最大值的一小部分更改我们的硬编码100 000
。
We’ll use a selector
to create a new derived state from our currentStatState
.
我们将使用selector
从currentStatState
创建一个新的派生状态。
Since the API returns the accumulation of each statistic, we know our maximum value for each stat is always going to be the maximum value for that stat at the latest date.
由于API返回了每个统计信息的累加,因此我们知道每个统计信息的最大值始终是该统计信息在最新日期的最大值。
import { selector, atom } from "recoil"
import { isAfter, isBefore, format } from "date-fns"
import { API_DATE_FORMAT, statusByDateQuery } from "../api"
export const DATE_RANGE = [new Date("10 Jan 2020"), new Date()]
export const CASES = "cases"
export const DEATHS = "deaths"
export const RECOVERED = "recovered"
export const availableStats = [CASES, DEATHS, RECOVERED]
export const currentStatState = atom({
key: "current-stat",
default: availableStats[0],
})
// new selector
export const currentStatMaxState = selector({
key: "current-stat-max",
get: async ({ get }) => {
const currentStat = get(currentStatState)
const formattedDate = format(DATE_RANGE[1], API_DATE_FORMAT)
const status = await get(statusByDateQuery(formattedDate))
const max = status.reduce((max, countryData) => {
const countryStat = countryData[currentStat]
return countryStat > max ? countryStat : max
}, 0)
return max
},
})
const dateState = atom({
key: "date",
default: DATE_RANGE[1],
})
export const currentDateState = selector({
key: "current-date",
get: ({ get }) => {
return get(dateState)
},
set: ({ set }, nextDate) => {
let newDate = nextDate
if (isAfter(newDate, DATE_RANGE[1])) newDate = DATE_RANGE[1]
if (isBefore(newDate, DATE_RANGE[0])) newDate = DATE_RANGE[0]
set(dateState, newDate)
},
})
export const currentFormattedDateState = selector({
key: "current-formatted-date",
get: ({ get }) => {
const currentDate = get(currentDateState)
return format(currentDate, API_DATE_FORMAT)
},
})
We add a currentStatMaxState
selector
, this is asynchronous as it relies on the status data for the latest date DATE_RANGE[1]
, in practice this should always have been memoized by the time we call this selector
‘s value.
我们添加了currentStatMaxState
selector
,它是异步的,因为它依赖于最新日期DATE_RANGE[1]
的状态数据,实际上,在调用此selector
的值时,应该始终已将其记住。
We get
our currentStatState
to know which statistic we need to calculate the maximum for and then use reduce
on our latest date status to return the biggest value contained for our currentStat
.
我们get
currentStatState
知道我们需要为哪个统计信息计算最大值,然后在我们的最新日期状态使用reduce
来返回currentStat
包含的最大值。
Now we update DataMap
to take a maxStat
prop
现在我们更新DataMap
以使用maxStat
import React from "react"
import DeckGLMap from "./DeckGLMap"
import { ScatterplotLayer } from "@deck.gl/layers"
const DataMap = ({
mapboxToken = "",
data = [],
dotCoordinates = "coordinates",
displayStat = "cases",
statMax = 1,
}) => {
const scatterplotLayer = new ScatterplotLayer({
id: "scatterplot-layer",
data,
stroked: false,
filled: true,
getPosition: (d) => d[dotCoordinates],
getRadius: (d) => {
const radius = (Math.log10(d[displayStat]) + d[displayStat] / (statMax / 60)) * 20000
return d[displayStat] > 0 ? radius : 0
},
getFillColor: (d) => [255, 0, 0],
})
const layers = [scatterplotLayer]
return
}
export default DataMap
We simply replace our 100 000
with the corresponding fraction, we knew from the data that the maximum value for the cases was 5M so that’s 5M/100K = 50, I chose 60 as it makes the dots a bit more visible.
我们只用相应的分数替换了100 000
,从数据中我们知道案例的最大值是5M,所以5M / 100K = 50,我选择60是因为它使点更加明显。
Lastly, we pass down the maximum from TimelineMap
最后,我们从TimelineMap
传递最大值
import React from "react"
import { useRecoilValueLoadable, useRecoilValue } from "recoil"
import { mapboxToken } from "../../mapbox-token"
import { countriesQuery, currentDateStatusState } from "../../state/api"
import { currentStatState, currentStatMaxState } from "../../state/app"
import DataMap from "./DataMap"
import LoadingIndicator from "../ui/LoadingIndicator"
const TimelineMap = () => {
const dateStatus = useRecoilValueLoadable(currentDateStatusState)
const countries = useRecoilValueLoadable(countriesQuery)
const currentStat = useRecoilValue(currentStatState)
const currentStatMax = useRecoilValueLoadable(currentStatMaxState)
const isLoading =
dateStatus.state === "loading" || countries.state === "loading" || currentStatMax.state === "loading"
let data = []
if (!isLoading) {
data = dateStatus.contents.map((status) => {
const country = countries.contents[status.country]
return {
name: country.name,
coordinates: [country.longitude, country.latitude],
...status,
}
})
}
return (
{isLoading ? : null}
)
}
export default TimelineMap
We simply import our currentStatMaxState
and get its value with useRecoilValueLoadable
since it is asynchronous.
我们只需导入currentStatMaxState
并使用useRecoilValueLoadable
获取其值,因为它是异步的。
Then we pass the value down to DataMap
with currentStatMax.contents
然后,将值通过currentStatMax.contents
传递给DataMap
We add currentStatMax.state === "loading"
to our loading check for good measure. In practice, the first query for our status is going to be the same query as for our max and it will have been memoized already so there won’t be any loading.
我们将currentStatMax.state === "loading"
添加到加载检查中以取得良好的效果。 实际上,关于我们状态的第一个查询将与关于我们的max的查询相同,并且已经被记忆,因此不会加载任何内容。
改善地图的外观 (Improving our map’s appearance)
We can also use our new statMax
prop to change the color and opacity of our dots.
我们还可以使用新的statMax
来更改点的颜色和不透明度。
We’ll make the biggest dots tend toward a full red color and the smallest ones to a green color. We’ll also reduce the opacity of the biggest dots so we can see the map behind them, this will help show the map and country names behind the biggest dots.
我们将使最大的点趋向于全红色,最小的点趋向于绿色。 我们还将减少最大点的不透明度,以便我们可以看到它们后面的地图,这将有助于显示最大点后面的地图和国家/地区名称。
import React from "react"
import DeckGLMap from "./DeckGLMap"
import { ScatterplotLayer } from "@deck.gl/layers"
const DataMap = ({
mapboxToken = "",
data = [],
dotCoordinates = "coordinates",
displayStat = "cases",
statMax = 1,
}) => {
const scatterplotLayer = new ScatterplotLayer({
id: "scatterplot-layer",
data,
stroked: false,
filled: true,
getPosition: (d) => d[dotCoordinates],
getRadius: (d) => {
const radius = (Math.log10(d[displayStat]) + d[displayStat] / (statMax / 60)) * 20000
return d[displayStat] > 0 ? radius : 0
},
getFillColor: (d) => {
const red = (d[displayStat] * 255) / statMax
const green = 255 - (d[displayStat] * 255) / statMax
const blue = 0
const opacity = 255 - (d[displayStat] * 170) / statMax
return [red, green, blue, opacity]
},
})
const layers = [scatterplotLayer]
return
}
export default DataMap
We’ve changed our getFillColor
function to return different red
, green
and opacity
values.
我们更改了getFillColor
函数,以返回不同的red
, green
和opacity
值。
Remember here that 255
is 100%
, so we simply set red
to be our statistic multiplied by 255 and then divide by our statMax
so we get a value between 0 and 255 for the current statistic.
记住这里255
是100%
,因此我们只需将red
设置为统计值乘以255,然后除以statMax
,就可以得到当前统计值的0到255之间的值。
For green
, we do the inverse, we start with 255 and subtract our stat’s value.
对于green
,我们做相反的事情,我们从255开始并减去我们的stat值。
For opacity, we do the same as for green
, but we cap the max value to subtract from 255 to 170 so we won’t get a fully transparent dot when we’re at max value.
对于不透明度,我们与green
相同,但是我们将最大值限制为从255减到170,因此当达到最大值时,我们将不会获得完全透明的点。
结论 (Conclusion)
If you made it this far, you are amazing !
如果您做到了这一点,那就太神奇了!
We’ve covered a lot of ground, I hope you’ve learned as much as I did by building this and that you found the project interesting.
我们已经覆盖了很多领域,希望您通过构建它学到的知识和我学到的一样多,并且您发现该项目很有趣。
Personally, I feel like Recoil might just become my new state management library. Once you’ve understood the underlying principles, using it is to manage state feels so painless, doing asynchronous tasks is an absolute breeze.
就个人而言,我觉得Recoil可能会成为我的新状态管理库。 一旦了解了基本原理,使用它来管理状态就很轻松了,执行异步任务绝对是一件轻而易举的事情。
Just imagine how much more code and how much harder it would have been to implement this app using Redux.
试想一下,使用Redux来实现此应用程序需要多少代码,以及要付出多少努力。
I’m now wondering if it would still provide such a great experience with an application that has to work with relational data and keep it in sync with the backend.
我现在想知道它是否仍将为必须处理关系数据并使其与后端保持同步的应用程序提供如此出色的体验。
My next stories will probably be about that and maybe how to use it with GraphQL as well.
我的下一个故事可能与此有关,也可能与GraphQL一起使用。
Thank you for reading this long article and I’d love to hear about your experience with Recoil, so leave a comment if you feel like sharing.
感谢您阅读这篇长文章,我很想听听您使用Recoil的经历,因此,如果您想分享,请发表评论。
翻译自: https://medium.com/swlh/learn-to-build-a-covid-tracker-with-react-and-recoil-208446971276