原文: Learn React by Building a Mortgage Calculator
今天我们将通过创建一个贷款计算器来学习和实践 ReactJS。这就是我们要创建的项目 👇
目标
在创建这个项目时,我们将涉及的主题是:
- React 功能组件
- Material UI
- 用户输入
- 处理 Props
- Props 解构赋值
- useState Hook
还有更多!这个教程对于想通过创建一个真实世界的项目来学习 ReactJS 的初学者来说是非常好的。
如果你喜欢,你也可以在 YouTube 上观看这个教程:
目录
- 项目设置
- 文件夹结构
- Material UI 主题
- 如何创建导航条
- Material UI 网格系统
- 如何创建 Slider 组件
- 休息一下
- 如何使用 useState Hook
- 如何创建 SliderSelect 组件
- 如何创建 TenureSelect 组件
- 如何创建 Result 组件
- 总结
- 我的社交媒体链接
项目设置
为了设置该项目,我们需要安装 eact
、material-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 浏览器上应该是完全空白的。
开始编程
一切都设置好了,可以开始了。现在,我们将开始构建该项目 :)
文件夹结构
我们的文件夹结构应该是这样的,这样我们就可以轻松地管理和维护文件和文件夹:
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
如果你感到困惑,这里有一张我们的项目文件夹结构的图片:
Material UI 主题
我们将使用 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 的深色主题。
这是目前的结果: 👇
整个屏幕将是黑的。这意味着我们的项目已经应用了 Material UI 深色模式。
如何创建 Navbar
接下来,我们将创建一个非常简单的导航条来显示 logo。为此,我们需要在 src/Components
文件夹中创建一个名为 Navbar.js
的文件,并添加以下代码:
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 中使用的组件的简单解释:
- AppBar:Material UI 的 AppBar 组件用于在用户界面上创建一个顶部导航栏。点击这里了解更多。
- Container:Material UI 的 Container 组件用于创建一个容器元素,该元素可用于创建一个响应式布局,并在用户界面中集中和包含其他元素。点击这里了解更多。
- ToolBar:Toolbar 组件可以包含诸如按钮、文本和图标等元素,也可以用来创建一个适应不同屏幕尺寸的响应式布局。点击这里了解更多。
- Typography:Material 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;
这是目前为止的结果: 👇
Material UI 网格系统
在最终完成的项目中,我们可以看到内容被分成了两部分。左边有滑块组件,右边有饼图。这是用 Material UI 的网格系统实现的。
不仅如此,我们还可以看到,内容在较小的屏幕尺寸上是响应式的。这也是通过使用 Material UI 网格系统实现的。
为了复制这一点,我们需要在 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>
对这段代码的解释:
- Container:在
Container
上,我们写了sx={{marginTop:4}}
。这是在 Material UI 中为组件添加内联样式的方法。 - Grid:Grid 组件被用来创建一个适应不同屏幕尺寸的响应式布局。
Grid container
代表父元素,Grid item
代表子元素。 - 在
Grid
组件上,我们写了spacing={5}
。这是在网格项之间添加间距的方法。 - 在
Grid
组件上,我们还写了xs={12}
,这意味着在超小屏幕上,网格项将占据整个屏幕的宽度。同样地,md={6}
意味着在中等和较大的屏幕上,网格项将占到屏幕的一半。这就是我们如何使组件具有响应性。
这是目前为止的结果:👇
如何创建 Slider 组件
接下来,我们将创建一个滑块组件来获取用户的输入金额。它看起来将是这样的: 👇
为此,我们需要在 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 组件来垂直堆叠组件。my
是 marginY
[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>
到目前为止,干得不错!
休息一下
休息一会儿吧——你值得!🎉
如何使用 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
}
然后,我们将把 data
和 setData
状态作为一个 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 组件
所以现在我们已经准备好了可重复使用的 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>
);
};
这是目前为止的结果:👇
我们将以同样的方式为 Down Payment
和 Loan 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>
);
这是目前为止的结果:👇
最后,我们将为 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>
);
结果如下:👇
如何创建 TenureSelect 组件
接下来,我们将创建 TenureSelect
组件。这个组件将被用来选择贷款的期限。它看起来像这样:👇
App.js
首先,像这样把 data
和 setData
状态作为一个 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>
);
结果如下:👇
如何创建 Result 组件
最后,我们将创建 Result
组件。这个组件将用于显示每月的贷款分期付款和饼图。它看起来像这样:👇
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>
);
结果如下:👇
总结
祝贺你读到最后!现在你可以自信地、有效地使用 React JS 和 Material UI 来创建很酷的项目。
你还学会了如何使用 React 的 useState hook,以及如何处理 props。我希望你喜欢这个教程。
导师计划
如果你有兴趣学习更多关于 React JS 和 web 开发的知识,我正在进行一个导师计划。你可以在这里查看细节👉Mentor Labs Academy。