e
给React应用加点样式
我们目前的笔记应用的外观是相当简陋的。在练习0.2中,作业是阅读Mozilla的CSS教程。
让我们看一下如何在React应用中添加样式。有几种不同的方法,我们将在后面看一下其他的方法。首先,我们将以传统的方式向我们的应用添加CSS;把CSS写入单个文件中,不使用CSS预处理器(尽管我们将在后面学习到,实际上我们并非完全没有使用)。
让我们在src目录下添加一个新的index.css文件,然后在main.jsx文件中导入它来将其添加到应用:
import './index.css'让我们在index.css文件中添加以下CSS规则:
h1 {
color: green;
}CSS规则包括选择器和声明。选择器定义了规则应该应用于哪些元素。上面的选择器是h1,因此选择器将匹配我们应用中所有h1标题的标签。
声明将color属性设为值green。
一条CSS规则可以包含任意数量的属性。让我们修改前面的规则,通过定义字体样式为italic,把文字变成草体。
h1 {
color: green;
font-style: italic;}通过使用不同类型的CSS选择器,可以实现许多匹配元素的方法。
如果我们想把我们的样式应用于,比方说,每一条笔记,我们可以使用选择器li,因为所有的笔记都被包裹在li标签里:
const Note = ({ note, toggleImportance }) => {
const label = note.important
? 'make not important'
: 'make important';
return (
<li>
{note.content}
<button onClick={toggleImportance}>{label}</button>
</li>
)
}让我们在我们的样式表中加入以下规则(因为我对优雅的网页设计的知识接近于零,所以这些样式并没有什么意义):
li {
color: grey;
padding-top: 3px;
font-size: 15px;
}使用元素类型来定义CSS规则是有点问题的。如果我们的应用包含其他的li标签,它们也会应用同样的样式规则。
如果我们想把我们的样式专门应用于笔记,那么最好使用类选择器。
在通常的HTML中,类的定义是class属性的值:
<li class="note">some text...</li>在React中,我们必须使用className属性而不是class属性。考虑到这一点,让我们对我们的Note组件做如下修改:
const Note = ({ note, toggleImportance }) => {
const label = note.important
? 'make not important'
: 'make important';
return (
<li className='note'> {note.content}
<button onClick={toggleImportance}>{label}</button>
</li>
)
}类选择器是用.classname语法定义的:
.note {
color: grey;
padding-top: 5px;
font-size: 15px;
}现在如果你在应用中添加其他li元素,它们就不会受上述样式规则的影响了。
改进错误信息
我们之前用alert方法实现了当用户试图切换已删除笔记的重要性时显示的错误信息。让我们把这个错误信息实现为它自己的React组件。
组件很简单:
const Notification = ({ message }) => {
if (message === null) {
return null
}
return (
<div className='error'>
{message}
</div>
)
}如果message props的值是null,那么就不会在屏幕上渲染信息,而在其他情况下,会把信息渲染到一个div元素中。
让我们向App组件中添加一个叫做errorMessage的新状态片段。让我们用一些错误信息来初始化它,这样我们就可以立即测试我们的组件:
const App = () => {
const [notes, setNotes] = useState([])
const [newNote, setNewNote] = useState('')
const [showAll, setShowAll] = useState(true)
const [errorMessage, setErrorMessage] = useState('some error happened...')
// ...
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage} /> <div>
<button onClick={() => setShowAll(!showAll)}>
show {showAll ? 'important' : 'all' }
</button>
</div>
// ...
</div>
)
}然后让我们添加一个适合错误信息的样式规则:
.error {
color: red;
background: lightgrey;
font-size: 20px;
border-style: solid;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}现在我们准备添加显示错误信息的逻辑。让我们将toggleImportanceOf函数改成:
const toggleImportanceOf = id => {
const note = notes.find(n => n.id === id)
const changedNote = { ...note, important: !note.important }
noteService
.update(id, changedNote).then(returnedNote => {
setNotes(notes.map(note => note.id !== id ? note : returnedNote))
})
.catch(error => {
setErrorMessage( `Note '${note.content}' was already removed from server` ) setTimeout(() => { setErrorMessage(null) }, 5000) setNotes(notes.filter(n => n.id !== id))
})
}当发生错误时,我们把错误信息的描述添加到errorMessage状态中。同时,我们启动一个定时器,在五秒后将errorMessage状态设为null。
结果看起来是这样的:

我们应用当前状态的代码可以在GitHub上的part2-7分支找到。
内联样式
在React中也可以直接在代码中编写样式,即所谓的内联样式。
定义内联样式的想法非常简单。可以将一组CSS属性作为JavaScript对象通过style属性提供给任何React组件或元素。
JavaScript中的CSS规则定义与普通的CSS文件略有不同。比方说,我们想给某个元素加上绿色和斜体。CSS中会是这样的:
{
color: green;
font-style: italic;
}但定义成React内联样式的对象是这样的:
{
color: 'green',
fontStyle: 'italic'
}每个CSS属性都被定义为JavaScript对象的一个单独的属性。像素的数字值可以简单地用整数定义。和普通CSS相比的一个主要区别是,CSS属性是用连字符-连接的(烤串命名法),JavaScript对象的属性是用驼峰式命名法(camelCase)的。
让我们向我们的应用中添加一个脚注组件,Footer,并为其定义内联样式。组件像下面定义在文件components/Footer.jsx中,并在App.jsx文件中使用:
const Footer = () => {
const footerStyle = {
color: 'green',
fontStyle: 'italic'
}
return (
<div style={footerStyle}>
<br />
<p>
Note app, Department of Computer Science, University of Helsinki 2025
</p>
</div>
)
}
export default Footerimport { useState, useEffect } from 'react'
import Footer from './components/Footer'import Note from './components/Note'
import Notification from './components/Notification'
import noteService from './services/notes'
const App = () => {
// ...
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage} />
// ...
<Footer /> </div>
)
}内联样式有某些限制。例如,不能直接使用所谓的伪类。
内联样式以及一些其他为React组件添加样式的方法完全违背了旧的惯例。传统上,人们认为最好的做法是将CSS与内容(HTML)和功能(JavaScript)完全分开。根据这一传统的思想,我们要将CSS、HTML和JavaScript分开写成单独的文件。
React的哲学事实上与此截然相反。由于将CSS、HTML和JavaScript分离到不同的文件中,似乎会使大型应用不能很好地扩展,所以React将应用的划分建立在其逻辑功能实体的基础上。
构成应用功能实体的结构单元是React组件。一个React组件定义了构造内容的HTML,决定功能的JavaScript函数,以及组件的样式;所有内容都在一个地方定义。这是为了创建尽可能独立和可重复使用的单个组件。
我们应用的最终版本的代码可以在GitHub上的part2-8分支中找到。
一些重要的注意事项
在本部分的最后,有一些更具挑战性的练习。如果这些练习让你感到头疼,可以先跳过,我们后面会再次回到这些主题。无论如何,这部分的材料都值得阅读。
在我们的应用中,我们做的一件事掩盖了一个非常典型的错误来源。
我们将状态notes的初始值设为一个空数组:
const App = () => {
const [notes, setNotes] = useState([])
// ...
}这是一个非常自然的初始值,因为notes是一组笔记,也就是说,状态将存储许多笔记。
如果状态只保存“一个东西”,更合适的初始值是 null,表示一开始状态中什么都没有。让我们看看如果使用null为初始值会发生什么:
const App = () => {
const [notes, setNotes] = useState(null)
// ...
}应用崩溃了:

错误信息给出了错误的原因和位置。导致问题的代码如下:
// notesToShow gets the value of notes
const notesToShow = showAll
? notes
: notes.filter(note => note.important)
// ...
{notesToShow.map(note => <Note key={note.id} note={note} />
)}错误信息是
Cannot read properties of null (reading 'map')变量notesToShow首先被赋值为状态notes的值,然后代码尝试对一个不存在的对象,也就是null,调用map方法。
这是什么原因呢?
Effect Hook使用函数setNotes将notes设为后端返回的笔记:
useEffect(() => {
noteService
.getAll()
.then(initialNotes => {
setNotes(initialNotes) })
}, [])然而,问题在于Effect只在第一次渲染之后执行。
并且因为notes的初始值是null:
const App = () => {
const [notes, setNotes] = useState(null)
// ...在第一次渲染时,以下代码被执行:
notesToShow = notes
// ...
notesToShow.map(note => ...)这导致应用崩溃,因为我们不能对null值调用map方法。
当我们将notes的初始值设为空数组时,就不会出现错误,因为空数组是可以调用map的。
因此,状态的初始化“掩盖”了由于数据尚未从后端获取而导致的问题。
另一种解决问题的方法是使用条件渲染,如果组件状态未被正确初始化,则返回null:
const App = () => {
const [notes, setNotes] = useState(null) // ...
useEffect(() => {
noteService
.getAll()
.then(initialNotes => {
setNotes(initialNotes)
})
}, [])
// do not render anything if notes is still null
if (!notes) { return null }
// ...
}因此,在第一次渲染时,不会渲染任何内容。当笔记从后端到达时,Effect使用函数setNotes来设状态notes的值。这会导致组件再次渲染,于是在第二次渲染时,笔记会被渲染到屏幕上。
基于条件渲染的方法适用于无法定义状态以致无法首次渲染的情况。
另一件我们还需要进一步观察的事情是useEffect的第二个参数:
useEffect(() => {
noteService
.getAll()
.then(initialNotes => {
setNotes(initialNotes)
})
}, [])useEffect的第二个参数用于指定Effect的运行频率。原则是Effect总是在组件第一次渲染后以及第二个参数的值发生变化时执行。
如果第二个参数是一个空数组[],它的内容永远不会改变,于是Effect只会在组件第一次渲染后运行。这正是我们在从服务端初始化应用状态时所需要的。
然而,有些情况下我们还希望在其他时候执行 Effect,例如当组件的状态以特定方式发生变化时。
考虑以下用于从Exchange rate API查询货币汇率的简单应用:
import { useState, useEffect } from 'react'
import axios from 'axios'
const App = () => {
const [value, setValue] = useState('')
const [rates, setRates] = useState({})
const [currency, setCurrency] = useState(null)
useEffect(() => {
console.log('effect run, currency is now', currency)
// skip if currency is not defined
if (currency) {
console.log('fetching exchange rates...')
axios
.get(`https://open.er-api.com/v6/latest/${currency}`)
.then(response => {
setRates(response.data.rates)
})
}
}, [currency])
const handleChange = (event) => {
setValue(event.target.value)
}
const onSearch = (event) => {
event.preventDefault()
setCurrency(value)
}
return (
<div>
<form onSubmit={onSearch}>
currency: <input value={value} onChange={handleChange} />
<button type="submit">exchange rate</button>
</form>
<pre>
{JSON.stringify(rates, null, 2)}
</pre>
</div>
)
}
export default App该应用的用户界面有一个表单,用户可以在输入框中输入想要查询的货币的名称。如果货币存在,应用会渲染该货币和其他货币的汇率:

在按下按钮时,应用将表单中输入的货币名称设为状态currency。
当currency得到新值时,应用会在Effect函数中从API获取其汇率:
const App = () => {
// ...
const [currency, setCurrency] = useState(null)
useEffect(() => {
console.log('effect run, currency is now', currency)
// skip if currency is not defined
if (currency) {
console.log('fetching exchange rates...')
axios
.get(`https://open.er-api.com/v6/latest/${currency}`)
.then(response => {
setRates(response.data.rates)
})
}
}, [currency]) // ...
}现在useEffect Hook的第二个参数是[currency]。因此,Effect函数会在第一次渲染后执行,并且总是在表发生变化,也就是函数的第二个参数[currency]的值发生变化时执行。也就是说,每当状态currency获得新值时,表的内容发生变化,Effect函数被执行。
选择null作为变量currency的初始值是很自然的,因为currency只代表一件物品。初始值null表示状态中尚无任何内容,并且可以简单通过if语句检查变量是否已被赋值。Effect有以下条件:
if (currency) {
// exchange rates are fetched
}这可以防止首次渲染后在变量currency仍然是初始值null的时候立即请求汇率。
所以如果用户在搜索框中输入例如eur,应用会使用axios向地址https://open.er-api.com/v6/latest/eur发送HTTP GET请求,并将响应存储在rates状态中。
当用户随后在搜索框中输入另一个值,例如usd,Effect函数会再次执行,并通过API请求新货币的汇率。
这里展示的进行API请求的方法可能看起来有点不方便。
这个特定的应用完全可以不使用useEffect,而直接在表单的提交处理函数中进行API请求:
const onSearch = (event) => {
event.preventDefault()
axios
.get(`https://open.er-api.com/v6/latest/${value}`)
.then(response => {
setRates(response.data.rates)
})
}然而在有些情况下,这种方法是行不通的。例如,你可能会在练习2.20中遇到这种行不通的情况,此时可以使用useEffect来解决。注意这在很大程度上取决于你选择的方法,例如,model solution就没有使用这一技巧。







