原文: Learn React by Building a Mortgage Calculator

今天我们将通过创建一个贷款计算器来学习和实践 ReactJS。这就是我们要创建的项目 👇

Project Image

目标

在创建这个项目时,我们将涉及的主题是:

  • React 功能组件
  • Material UI
  • 用户输入
  • 处理 Props
  • Props 解构赋值
  • useState Hook

还有更多!这个教程对于想通过创建一个真实世界的项目来学习 ReactJS 的初学者来说是非常好的。

如果你喜欢,你也可以在 YouTube 上观看这个教程:

目录

  • 项目设置
  • 文件夹结构
  • Material UI 主题
  • 如何创建导航条
  • Material UI 网格系统
  • 如何创建 Slider 组件
  • 休息一下
  • 如何使用 useState Hook
  • 如何创建 SliderSelect 组件
  • 如何创建 TenureSelect 组件
  • 如何创建 Result 组件
  • 总结
  • 我的社交媒体链接

项目设置

Project Setup

为了设置该项目,我们需要安装 eactmaterial-ui 和其他必要的软件包。

首先创建一个名为 mortgage-calculator 的文件夹,在 VS Code 上打开它,然后在终端运行以下命令:

npx create-react-app .
npm install @mui/material @emotion/react @emotion/styled
npm install --save chart.js react-chartjs-2

App.js

我们将删除 app.js 中所有的模板代码,保留这部分 👇

import React from "react";

function App() {
  return <div className="App">Hello everyone</div>;
}

export default App;

然后在终端运行这个命令来启动服务器:

npm start

该项目现在在 web 浏览器上应该是完全空白的。

开始编程

Lets start coding

一切都设置好了,可以开始了。现在,我们将开始构建该项目 :)

文件夹结构

Folder Structure

我们的文件夹结构应该是这样的,这样我们就可以轻松地管理和维护文件和文件夹:

mortgage-calculator/
├── src/
│   ├── Components/
│   │   ├── Common/
│   │   │   └── SliderComponent.js
│   │   ├── Navbar.js
│   │   ├── Result.js
│   │   ├── SliderSelect.js
│   │   ├── TenureSelect.js
│   ├── theme.js
│   ├── App.js
│   ├── index.js
├── .gitignore
├── package.json
└── package-lock.json

如果你感到困惑,这里有一张我们的项目文件夹结构的图片:

Folder Structure

Material UI 主题

MUI Theme

我们将使用 Material UI 的深色主题。为此,我们需要在 src 文件夹中创建一个名为 theme.js 的文件,并添加以下代码:

theme.js

import { createTheme } from '@mui/material/styles';

export const theme = createTheme({
  palette: {
    mode: 'dark',
  },
})

index.js

接下来,我们需要在 index.js 文件中导入 theme,并用 ThemeProvider 来包含应用程序。下面就跟着做吧:👇

import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import { theme } from "./theme";

<React.StrictMode>
  <ThemeProvider theme={theme}>
    <App />
    <CssBaseline />
  </ThemeProvider>
</React.StrictMode>

注意如果你不传递 CssBaseline 组件,我们将无法看到 MUI 的深色主题。

这是目前的结果: 👇

Result so far

整个屏幕将是黑的。这意味着我们的项目已经应用了 Material UI 深色模式。

如何创建 Navbar

Navbar Setup

接下来,我们将创建一个非常简单的导航条来显示 logo。为此,我们需要在 src/Components 文件夹中创建一个名为 Navbar.js 的文件,并添加以下代码:

import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import { Container } from "@mui/system";

const Navbar = () => {
  return (
    <AppBar position="static">
      <Container maxWidth='xl'>
        <Toolbar>
          <Typography variant="h5">
            Bank of React
          </Typography>
        </Toolbar>
      </Container>
    </AppBar>
  );
};

export default Navbar;

下面是对 Material UI 中使用的组件的简单解释:

  • AppBarMaterial UI 的 AppBar 组件用于在用户界面上创建一个顶部导航栏。点击这里了解更多。
  • ContainerMaterial UI 的 Container 组件用于创建一个容器元素,该元素可用于创建一个响应式布局,并在用户界面中集中和包含其他元素。点击这里了解更多。
  • ToolBarToolbar 组件可以包含诸如按钮、文本和图标等元素,也可以用来创建一个适应不同屏幕尺寸的响应式布局。点击这里了解更多。
  • TypographyMaterial UI 的 typography 组件用于将预定义的排版样式应用于文本元素。它可以帮助创建一个一致的、视觉上赏心悦目的布局,具有可定制的字体、大小、粗细和间距。点击这里了解更多。

App.js

最后,将其导入 App.js,并这样写代码:👇

import React from "react";
import Navbar from "./Components/Navbar";

function App() {
  return (
    <div className="App">
      <Navbar />
    </div>
  );
}

export default App;

这是目前为止的结果: 👇

Navbar result

Material UI 网格系统

MUI Grid System

在最终完成的项目中,我们可以看到内容被分成了两部分。左边有滑块组件,右边有饼图。这是用 Material UI 的网格系统实现的。

Finalized project image

不仅如此,我们还可以看到,内容在较小的屏幕尺寸上是响应式的。这也是通过使用 Material UI 网格系统实现的。

Responsive content

为了复制这一点,我们需要在 App.js 文件上写上这些东西。你可以在这里跟着做。👇

首先,我们需要从 Material UI 和组件文件夹中导入所有需要的组件:

import React, { useState } from "react";
import { Grid } from "@mui/material";
import { Container } from "@mui/system";
import Navbar from "./Components/Navbar";
import Result from "./Components/Result";
import SliderSelect from "./Components/SliderSelect";
import TenureSelect from "./Components/TenureSelect";

接下来,我们在 return 语句里面写这段代码:👇

<div className="App">
  <Navbar />
  <Container maxWidth="xl" sx={{marginTop:4}}>
    <Grid container spacing={5} alignItems="center">
      <Grid item xs={12} md={6}>
        <SliderSelect />
        <TenureSelect />
      </Grid>
      <Grid item xs={12} md={6}>
        <Result/>
      </Grid>
    </Grid>
  </Container>
</div>

对这段代码的解释:

  • ContainerContainer 上,我们写了 sx={{marginTop:4}}。这是在 Material UI 中为组件添加内联样式的方法。
  • Grid:Grid 组件被用来创建一个适应不同屏幕尺寸的响应式布局。Grid container 代表父元素,Grid item 代表子元素。
  • Grid 组件上,我们写了 spacing={5}。这是在网格项之间添加间距的方法。
  • Grid 组件上,我们还写了 xs={12},这意味着在超小屏幕上,网格项将占据整个屏幕的宽度。同样地,md={6} 意味着在中等和较大的屏幕上,网格项将占到屏幕的一半。这就是我们如何使组件具有响应性。

这是目前为止的结果:👇

Result image of Grid system

如何创建 Slider 组件

Slider Component

接下来,我们将创建一个滑块组件来获取用户的输入金额。它看起来将是这样的: 👇

Slider Component

为此,我们需要在 src/Components/Common 文件夹中创建一个名为 SliderComponent.js 的文件。首先,让我们列出所有需要传递给可重用的滑块组件的 props:

  • label
  • min
  • max
  • defaultValue
  • unit
  • value
  • steps
  • amount
  • onChange

SliderComponent.js

我们开始吧。首先,在 SliderComponent.js 文件中从 MUI 导入以下组件:

import React from "react";
import Slider from "@mui/material/Slider";
import { Typography } from "@mui/material";
import { Stack } from "@mui/system";

我们将使用 MUI 的 Stack 组件来垂直堆叠组件。mymarginY [margin-top & margin-bottom] 的缩写。我们将使用 MUI 的 Typography 组件来显示标签、单位和其他数据。我们将使用 MUI 的 Slider 组件来显示滑块。

先写这一小段代码,解构 props:

const SliderComponent = ({
  defaultValue,
  min,
  max,
  label,
  unit,
  onChange,
  amount,
  value,
  steps
}) => {
  return (
    <Stack my={1.4}>

    </Stack>
  )
}

export default SliderComponent

我们将编写这段代码来显示标签、单位和金额。

<Stack gap={1}>
  <Typography variant="subtitle2">{label}</Typography>
  <Typography variant="h5">
    {unit} {amount}
  </Typography>
</Stack>

编写这段代码来显示滑块,并像这样把 props 传递给滑块组件: 👇

<Slider
  min={min}
  max={max}
  defaultValue={defaultValue}
  aria-label="Default"
  valueLabelDisplay="auto"
  onChange={onChange}
  value={value}
  marks
  step={steps}
/>

我们将编写这段代码来显示滑块的最小和最大值。我们将使用 MUI 的 Stack 组件来水平堆叠组件。direction="row"flex-direction: row 的缩写。justifyContent="space-between"justify-content: space-between 的缩写。

<Stack direction="row" justifyContent="space-between">
  <Typography variant="caption" color="text.secondary">
    {unit} {min}
  </Typography>
  <Typography variant="caption" color="text.secondary">
    {unit} {max}
  </Typography>
</Stack>

到目前为止,干得不错!

休息一下

Take a break

休息一会儿吧——你值得!🎉

如何使用 useState Hook

useState Hook

我们需要在我们的项目中使用 useState hook。但在此之前,我们需要了解它是什么以及为什么我们需要使用它。

useState hook 是一个内置的 React 函数,允许你向功能组件添加状态。它返回一个包含两个元素的数组:当前状态值和一个更新该值的函数。useState hook 的一般语法如下:

const [state, setState] = useState(initialState);

其中👇

  • state:将存储状态的常量或变量的名称
  • setState:一个更新状态的函数
  • initialState:状态的初始值

useState hook 的例子

我们将创建一个切换按钮,点击它时,它的文本在 “ON” 和 “OFF” 之间切换。

import React, { useState } from 'react';

const ToggleButton = () => {
  const [isOn, setIsOn] = useState(false);

  const toggle = () => setIsOn(!isOn)

  return (
      <button onClick={toggle}>{isOn ? 'ON' : 'OFF'}</button>
  );
};

export default ToggleButton;

在这里,我们初始化 isOn 状态的初始值为 false。当用户点击按钮时,toggle 函数将 isOn 状态更新为其相反的值。我们使用一个三元操作符,根据 isOn 的当前值来渲染按钮内的文本。

App.js

现在让我们回到我们的项目中来。首先,在 App.js 文件中,从 React 导入 useState hook。

import React, { useState } from 'react';

接下来,我们将使用 useState hook 声明一个状态来存储滑块的值。我们将在 useState hook 中以 {} 的形式传递状态的初始值,将数据存储为一个对象。

function App() {
  const [data, setData] = useState({})

  // other codes are here
}

我们使用  useState hook 来创建一个名为 data 的新状态变量和一个名为 setData 的函数,我们可以用它来更新这个状态。

接下来,我们将把这些值作为默认值传递给滑块组件。

function App() {
  const [data, setData] = useState({
    homeValue: 3000,
    downPayment: 3000 * 0.2,
    loanAmount: 3000 * 0.8,
    loanTerm: 5,
    interestRate: 5,
  })

  // other codes are here
}

然后,我们将把 datasetData 状态作为一个 prop 传递给 SliderSelect 组件,像这样:👇

<div className="App">
  <Navbar />
  <Container maxWidth="xl" sx={{marginTop:4}}>
    <Grid container spacing={5} alignItems="center">
      <Grid item xs={12} md={6}>

        {/* this is where we write the code  👇 */}
        <SliderSelect data={data} setData={setData}/>

        <TenureSelect />
      </Grid>
      <Grid item xs={12} md={6}>
        <Result/>
      </Grid>
    </Grid>
  </Container>
</div>

如何创建 SliderSelect 组件

SliderSelect.js component

所以现在我们已经准备好了可重复使用的 SliderComponent,将在 SliderSelect.js 组件中使用它。首先,从 Common 文件夹中导入 SliderComponent 组件。

SliderSelect.js

import SliderComponent from "./Common/SliderComponent";

接下来,我们将对从 App.js 收到的 prop 进行解构。同时,创建一个名为 bank_limit 的变量,并给它一个 10000 的值。这代表了一个人可以从我们的银行借到的最大数额的钱。

import React from "react";
import SliderComponent from "./Common/SliderComponent";

const SliderSelect = ({ data, setData }) => {
  const bank_limit = 10000;
  return (
    <div>
      
    </div>
  );
};

export default SliderSelect;

接下来,我们将使用 SliderComponent 来显示名为 Home Value 的滑块。在这里,我们将像这样把 props 传递给 SliderComponent 组件。

const SliderSelect = ({ data, setData }) => {
  const bank_limit = 10000;
  return (
    <div>
      <SliderComponent
        onChange={(e, value) => {
          setData({
            ...data,
            homeValue: value.toFixed(0),
            downPayment: (0.2 * value).toFixed(0),
            loanAmount: (0.8 * value).toFixed(0),
          });
        }}
        defaultValue={data.homeValue}
        min={1000}
        max={bank_limit}
        steps={100}
        unit="$"
        amount={data.homeValue}
        label="Home Value"
        value={data.homeValue}
      />
    </div>
  );
};

这是目前为止的结果:👇

Home Value Slider

我们将以同样的方式为 Down PaymentLoan Amount 创建滑块,像这样:👇

  return (
    <div>
      {/* other codes are here */}

      <SliderComponent
        onChange={(e, value) =>
          setData({
            ...data,
            downPayment: value.toFixed(0),
            loanAmount: (data.homeValue - value).toFixed(0),
          })
        }
        defaultValue={data.downPayment}
        min={0}
        max={data.homeValue}
        steps={100}
        unit="$"
        amount={data.downPayment}
        label="Down Payment"
        value={data.downPayment}
      />

      <SliderComponent
        onChange={(e, value) =>
          setData({
            ...data,
            loanAmount: value.toFixed(0),
            downPayment: (data.homeValue - value).toFixed(0),
          })
        }
        defaultValue={data.loanAmount}
        min={0}
        max={data.homeValue}
        steps={100}
        unit="$"
        amount={data.loanAmount}
        label="Loan Amount"
        value={data.loanAmount}
      />
    </div>
  );

这是目前为止的结果:👇

the result so far

最后,我们将为 Interest Rate 创建滑块。你可以在这里跟着做:👇

return (
    <div>
      {/* other codes are here */}

      <SliderComponent
        onChange={(e, value) =>
          setData({
            ...data,
            interestRate: value,
          })
        }
        defaultValue={data.interestRate}
        min={2}
        max={18}
        steps={0.5}
        unit="%"
        amount={data.interestRate}
        label="Interest Rate"
        value={data.interestRate}
      />
    </div>
  );

结果如下:👇

Interest Rate slider

如何创建 TenureSelect 组件

接下来,我们将创建 TenureSelect 组件。这个组件将被用来选择贷款的期限。它看起来像这样:👇

Image description

App.js

首先,像这样把 datasetData 状态作为一个 prop 传递给 TenureSelect 组件:👇

return (
  <div className="App">
    <Navbar />
    <Container maxWidth="xl" sx={{marginTop:4}}>
      <Grid container spacing={5} alignItems="center">
        <Grid item xs={12} md={6}>
          <SliderSelect data={data} setData={setData} />

          {/* this is where we write the code  👇 */}
          <TenureSelect data={data} setData={setData}/>

        </Grid>
        <Grid item xs={12} md={6}>
          <Result data={data}/>
        </Grid>
      </Grid>
    </Container>
  </div>
);

TenureSelect.js

然后,从 MUI 库中导入这些所需的组件:

import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";

然后解构我们从 App.js 接收的 props,同时创建一个名为 handleChange 的函数,它将被用来设置 tenure 状态,像这样:👇

const TenureSelect = ({ data, setData }) => {

  const handleChange = (event) => {
    setData({...data, loanTerm: event.target.value});
  };

  return ()
};

export default TenureSelect;

接下来,我们将创建 TenureSelect 组件。它看起来像这样:👇

return (
  <FormControl fullWidth>
    <InputLabel id="demo-simple-select-label">Tenure</InputLabel>
    <Select
      labelId="demo-simple-select-label"
      id="demo-simple-select"
      value={data.loanTerm}
      label="Tenure"
      defaultValue={5}
      onChange={handleChange}
    >
      <MenuItem value={5}>5 years</MenuItem>
      <MenuItem value={10}>10 years</MenuItem>
      <MenuItem value={15}>15 years</MenuItem>
      <MenuItem value={20}>20 years</MenuItem>
      <MenuItem value={25}>25 years</MenuItem>
    </Select>
  </FormControl>
);

结果如下:👇

The result so far

如何创建 Result 组件

最后,我们将创建 Result 组件。这个组件将用于显示每月的贷款分期付款和饼图。它看起来像这样:👇

Result component

App.js

首先,像这样把 data 状态作为一个 prop 传递给 Result 组件:👇

return (
  <div className="App">
    <Navbar />
    <Container maxWidth="xl" sx={{marginTop:4}}>
      <Grid container spacing={5} alignItems="center">
        <Grid item xs={12} md={6}>
          <SliderSelect data={data} setData={setData} />
          <TenureSelect data={data} setData={setData}/>
        </Grid>
        <Grid item xs={12} md={6}>

          {/* this is where we write the code  👇 */}
          <Result data={data}/>
          
        </Grid>
      </Grid>
    </Container>
  </div>
);

Result.js

接下来,像这样导入所需的组件:👇

import React from "react";
import { Stack, Typography } from "@mui/material";
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
import { Pie } from "react-chartjs-2";

ChartJS.register(ArcElement, Tooltip, Legend);

然后像这样解构我们从 App.js`接收的数据状态:👇

const Result = ({ data }) => {
  const { homeValue, loanAmount, loanTerm, interestRate } = data;
  return ();
};

export default Result;

接下来我们将写出所有帮助我们进行计算的东西:👇

  const totalLoanMonths = loanTerm * 12;
  const interestPerMonth = interestRate / 100 / 12;
  const monthlyPayment =
    (loanAmount *
      interestPerMonth *
      (1 + interestPerMonth) ** totalLoanMonths) /
    ((1 + interestPerMonth) ** totalLoanMonths - 1);

  const totalInterestGenerated = monthlyPayment * totalLoanMonths - loanAmount;

然后我们需要这个变量来存储饼图的所有数据,像这样:👇

const pieChartData = {
  labels: ["Principle", "Interest"],
  datasets: [
    {
      label: "Ratio of Principle and Interest",
      data: [homeValue, totalInterestGenerated],
      backgroundColor: ["rgba(255, 99, 132, 0.2)", "rgba(54, 162, 235, 0.2)"],
      borderColor: ["rgba(255, 99, 132, 1)", "rgba(54, 162, 235, 1)"],
      borderWidth: 1,
    },
  ],
};

最后,我们将创建 Result 组件,它是这样的:👇

return (
  <Stack gap={3}>
    <Typography textAlign="center" variant="h5">
      Monthly Payment: $ {monthlyPayment.toFixed(2)}
    </Typography>
    <Stack direction="row" justifyContent="center">
      <div>
        <Pie data={pieChartData} />
      </div>
    </Stack>
  </Stack>
);

结果如下:👇

The result so far

总结

Congratulations

祝贺你读到最后!现在你可以自信地、有效地使用 React JS 和 Material UI 来创建很酷的项目。

你还学会了如何使用 React 的 useState hook,以及如何处理 props。我希望你喜欢这个教程。

导师计划

如果你有兴趣学习更多关于 React JS 和 web 开发的知识,我正在进行一个导师计划。你可以在这里查看细节👉Mentor Labs Academy

Mentorship Program

我的社交媒体链接