学习用React和反冲力来构建一个covid追踪器

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.cssApp.test.jslogo.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

反冲的主要组成部分是atomselector

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 (
        <ul>
            {Object.keys(countries).map((alpha2) => {
                return <li key={alpha2}>{countries[alpha2].name}</li>
            })}
        </ul>
    )
}


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 (
        <RecoilRoot>
            <Suspense fallback="Loading...">
                <Countries />
            </Suspense>
        </RecoilRoot>
    )
}


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 Recoil

    RecoilRoot因为使用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组成了,所以你现在可以删除它和上卸下里面的一切RecoilRootApp.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.

现在,我们的statusByDateQueryget选项是一个接受日期并返回新的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 <DeckGLMap layers={layers} />
}


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]获取我们国家的信息,并返回带有附加longitudelatitude先前数据对象作为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 (
        <RecoilRoot>
            <Suspense fallback="Loading...">
                <DataMap />
            </Suspense>
        </RecoilRoot>
    )
}


export default App

The map now displays red dots of varying size corresponding to the number of cases for each country !

现在,该地图会显示不同大小的红点,对应于每个国家的案件数量!

Image for post

赋予用户穿越时空的能力 (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 <DeckGLMap mapboxToken={mapboxToken} layers={layers} />
}


export default DataMap

Remove the dependency on our token file from DeckGLMap as well

DeckGLMapDeckGLMap删除对令牌文件的依赖

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 (
        <DeckGL initialViewState={INITIAL_VIEW_STATE} controller={true} layers={layers}>
            <StaticMap mapboxApiAccessToken={mapboxToken} />
        </DeckGL>
    )
}


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 <DataMap mapboxToken={mapboxToken} data={data} />
}


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 (
        <RecoilRoot>
            <Suspense fallback="Loading...">
                <TimelineMap />
            </Suspense>
        </RecoilRoot>
    )
}


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 <StyledButton {...props}>{children}</StyledButton>
}


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 (
        <StyledTimeTravelButtons>
            <Button onClick={jumpBackward}>&lt;&lt; Backward</Button>
            <div className="separator"></div>
            <Button onClick={jumpForward}>Forward &gt;&gt;</Button>
        </StyledTimeTravelButtons>
    )
}


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 addDayssubtractDays ,然后使用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 (
        <RecoilRoot>
            <Suspense fallback="Loading...">
                <TimeTravelButtons />
                <TimelineMap />
            </Suspense>
        </RecoilRoot>
    )
}


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请求!

Image for post

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 (
        <StyledTimeTravelButtons>
            <div className="current-date">{format(currentDate, "LLLL do, yyyy")}</div>
            <div className="buttons">
                <Button onClick={jumpBackward}>&lt;&lt; Backward</Button>
                <div className="separator"></div>
                <Button onClick={jumpForward}>Forward &gt;&gt;</Button>
            </div>
        </StyledTimeTravelButtons>
    )
}


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 (
        <StyledLoadingIndicator>
            <Loader type="Oval" color="#667EEA" height={40} width={40} />
        </StyledLoadingIndicator>
    )
}


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 (
        <div className="timeline-map">
            {isLoading ? <LoadingIndicator /> : null}
            <DataMap mapboxToken={mapboxToken} data={data} />
        </div>
    )
}


export default TimelineMap

Now we check if we’re loading with dateStatus.state and countries.state

现在我们检查,如果我们用装载dateStatus.statecountries.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.contentscountries.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.

当我们定义selectorget选项应该是一个返回我们值的函数。

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另一个atomselector的值

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属性也是一个函数,它允许我们设置另一个atomselector的状态(前提是该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 isAfterisBefore函数检查日期是否在我们的日期范围内,如果任何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 <StyledButton {...props}>{children}</StyledButton>
}


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.

修改我们的TimeTravelButtonsTimeTravelButtons地设置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 (
        <StyledTimeTravelButtons>
            <div className="current-date">{format(currentDate, "LLLL do, yyyy")}</div>
            <div className="buttons">
                <Button disabled={currentDate === DATE_RANGE[0]} onClick={jumpBackward}>
                    &lt;&lt; Backward
                </Button>
                <div className="separator"></div>
                <Button disabled={currentDate === DATE_RANGE[1]} onClick={jumpForward}>
                    Forward &gt;&gt;
                </Button>
            </div>
        </StyledTimeTravelButtons>
    )
}


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 (
        <StyledTimeTravelSlider>
            <input type="range" value={currentSliderPos} min={0} max={maxSliderPos} onChange={handleDateChange} />
        </StyledTimeTravelSlider>
    )
}


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 (
        <RecoilRoot>
            <Suspense fallback="Loading...">
                <TimeTravelButtons />
                <TimelineMap />
                <TimeTravelSlider />
            </Suspense>
        </RecoilRoot>
    )
}


export default App

And with that we can move through time by sliding !

这样,我们就可以通过滑动来穿越时间!

Image for post

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 (
        <StyledTimeTravelSlider sliderPercentage={sliderPercentage}>
            <div className="slider-container">
                <div className="time-info" style={timeInfoStyles}>
                    {formatDistanceToNow(currentDate, { addSuffix: true })}
                </div>
                <input type="range" value={currentSliderPos} min={0} max={maxSliderPos} onChange={handleDateChange} />
            </div>
        </StyledTimeTravelSlider>
    )
}


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.

这样,如果用户没有快速移动以在后台进行预取,则实际上似乎根本没有加载。

Image for post

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 (
        <div className="timeline-map">
            {isLoading ? <LoadingIndicator /> : null}
            <DataMap mapboxToken={mapboxToken} data={data} />
        </div>
    )
}


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.

而且由于我们的请求已被缓存,因此在用户经过一段时间后,所有数据都将被加载,而我们将不再有任何加载。

Image for post

显示不同的统计信息 (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 (
        <StyledCurrentStatSelect>
            <select value={currentStat} onChange={handleStatChange}>
                {availableStats.map((stat) => {
                    return (
                        <option key={stat} value={stat}>
                            {stat[0].toUpperCase() + stat.slice(1)}
                        </option>
                    )
                })}
            </select>
        </StyledCurrentStatSelect>
    )
}


export default CurrentStatSelect

Again we get our value and setter function with useRecoilState in currentStat andsetCurrentStat

再次使用currentStatsetCurrentStat 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仍然依赖于这样的事实,即我们传递带有casescoordinatesdata对象,这不好。

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 <DeckGLMap mapboxToken={mapboxToken} layers={layers} />
}


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 (
        <div className="timeline-map">
            {isLoading ? <LoadingIndicator /> : null}
            <DataMap mapboxToken={mapboxToken} data={data} displayStat={currentStat} />
        </div>
    )
}


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 (
        <RecoilRoot>
            <Suspense fallback="Loading...">
                <TimeTravelButtons />
                <CurrentStatSelect />
                <TimelineMap />
                <TimeTravelSlider />
            </Suspense>
        </RecoilRoot>
    )
}


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 .

我们将使用selectorcurrentStatState创建一个新的派生状态。

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 <DeckGLMap mapboxToken={mapboxToken} layers={layers} />
}


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 (
        <div className="timeline-map">
            {isLoading ? <LoadingIndicator /> : null}
            <DataMap
                mapboxToken={mapboxToken}
                data={data}
                displayStat={currentStat}
                statMax={currentStatMax.contents}
            />
        </div>
    )
}


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 <DeckGLMap mapboxToken={mapboxToken} layers={layers} />
}


export default DataMap

We’ve changed our getFillColor function to return different red , green and opacity values.

我们更改了getFillColor函数,以返回不同的redgreenopacity值。

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.

记住这里255100% ,因此我们只需将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)

Image for post

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值