React 教程
React 是由 Facebook 开发的一个用于构建用户界面的 JavaScript 库。它采用组件化模式,声明式编程,可以提高开发效率,创建交互式 UI。
一、 React 特点
- 声明式设计:React 使创建交互式 UI 变得简单
- 组件化:构建管理各自状态的组件,然后组合它们构成复杂 UI
- 一次学习,随处编写:React 不仅可以开发 Web 应用,还可以开发移动应用(React Native)
- 高效:React 通过虚拟 DOM 最小化 DOM 操作
- 灵活:React 可以与已知的库或框架很好地配合
二、环境搭建
1. 使用 create-react-app 创建项目
npx create-react-app my-app
cd my-app
npm start
2. 项目目录结构
my-app/
README.md
node_modules/
package.json
public/
index.html
favicon.ico
src/
App.css
App.js
App.test.js
index.css
index.js
logo.svg
三、JSX 语法
JSX 是 JavaScript 的语法扩展,它允许我们在 JavaScript 中编写类似 HTML 的代码。
基本用法
jsx
const element = <h1>Hello, world!</h1>;
嵌入表达式
jsx
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
JSX 也是一个表达式
jsx
function getGreeting(user) {
if (user) {
return <h1>Hello, {user.name}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
指定属性
jsx
const element = <img src={user.avatarUrl}></img>;
JSX 防止注入攻击
jsx
const title = response.potentiallyMaliciousInput;
// 安全
const element = <h1>{title}</h1>;
四、组件
1. 函数组件
jsx
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
2. 类组件
jsx
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
3. 组件渲染
jsx
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);
4. 组合组件
jsx
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
五、Props
Props 是组件之间传递数据的方式,它是只读的。
基本使用
jsx
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
默认 Props
jsx
class Welcome extends React.Component {
static defaultProps = {
name: 'Stranger'
}
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
Props 类型检查
npm install prop-types
jsx
import PropTypes from 'prop-types';
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
Welcome.propTypes = {
name: PropTypes.string
};
六、State
State 是组件内部的状态,可以变化并触发重新渲染。
类组件中的 State
jsx
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
函数组件中的 State (Hooks)
jsx
import { useState, useEffect } from 'react';
function Clock() {
const [date, setDate] = useState(new Date());
useEffect(() => {
const timerID = setInterval(() => tick(), 1000);
return () => {
clearInterval(timerID);
};
}, []);
function tick() {
setDate(new Date());
}
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {date.toLocaleTimeString()}.</h2>
</div>
);
}
七、事件处理
基本语法
jsx
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
类组件中的事件处理
jsx
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
传递参数
jsx
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
八、条件渲染
if 语句
jsx
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
与运算符 &&
jsx
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
}
三目运算符
jsx
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>
);
}
阻止组件渲染
jsx
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
九、列表 & Key
渲染多个组件
jsx
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
在组件中使用列表
jsx
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
十、表单
受控组件
jsx
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('提交的名字: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
textarea 标签
jsx
class EssayForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '请撰写一篇关于你喜欢的 DOM 元素的文章.'
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('提交的文章: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
文章:
<textarea value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
select 标签
jsx
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('你喜欢的风味是: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的风味:
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
十一、状态提升
当多个组件需要共享状态时,可以将状态提升到它们最近的共同父组件中。
jsx
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
handleFahrenheitChange(temperature) {
this.setState({scale: 'f', temperature});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
十二、组合 vs 继承
React 推荐使用组合而非继承来实现组件间的代码重用。
包含关系
jsx
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
特殊实例
jsx
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
{props.children}
</FancyBorder>
);
}
class SignUpDialog extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.state = {login: ''};
}
render() {
return (
<Dialog title="Mars Exploration Program"
message="How should we refer to you?">
<input value={this.state.login}
onChange={this.handleChange} />
<button onClick={this.handleSignUp}>
Sign Me Up!
</button>
</Dialog>
);
}
handleChange(e) {
this.setState({login: e.target.value});
}
handleSignUp() {
alert(`Welcome aboard, ${this.state.login}!`);
}
}
十三、React Hooks
Hooks 是 React 16.8 新增的特性,让你在不编写 class 的情况下使用 state 以及其他 React 特性。
useState
jsx
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect
jsx
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useContext
jsx
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
useReducer
jsx
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useRef
jsx
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
十四、React Router
React Router 是一个流行的 React 路由库。
基本使用
npm install react-router-dom
jsx
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
function BasicExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
</ul>
<hr />
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/dashboard" component={Dashboard} />
</div>
</Router>
);
}
function Home() {
return (
<div>
<h2>Home</h2>
</div>
);
}
function About() {
return (
<div>
<h2>About</h2>
</div>
);
}
function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
</div>
);
}
十五、Redux
Redux 是一个可预测的状态容器,用于 JavaScript 应用。
基本概念
- Store:保存应用状态
- Action:描述发生了什么
- Reducer:根据 action 更新 state
基本使用
npm install redux react-redux
jsx
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';
// Reducer
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
// Store
let store = createStore(counter);
// React component
function Counter({ value, onIncrement, onDecrement }) {
return (
<div>
<h1>{value}</h1>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
);
}
// Map Redux state to component props
function mapStateToProps(state) {
return {
value: state
};
}
// Map Redux actions to component props
function mapDispatchToProps(dispatch) {
return {
onIncrement: () => dispatch({ type: 'INCREMENT' }),
onDecrement: () => dispatch({ type: 'DECREMENT' })
};
}
// Connected Component
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
// Render
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
十六、性能优化
使用 shouldComponentUpdate
jsx
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
使用 React.memo
jsx
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});
使用 useMemo 和 useCallback
jsx
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []); // 依赖项数组为空,表示这个函数不会改变
return (
<div>
<button onClick={() => setCount(count + 1)}>Increase</button>
<ChildComponent onClick={handleClick} />
</div>
);
}
十七、测试
Jest 测试
jsx
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
React Testing Library
jsx
import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
test('button click', () => {
render(<App />);
const button = screen.getByText(/click me/i);
fireEvent.click(button);
expect(screen.getByText(/clicked/i)).toBeInTheDocument();
});
十八、部署
构建生产版本
npm run build
使用 serve 测试构建版本
npm install -g serve
serve -s build
部署到 GitHub Pages
- 安装 gh-pages
npm install gh-pages --save-dev
- 在 package.json 中添加
"homepage": "https://yourusername.github.io/your-repo-name",
"scripts": {
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
}
- 运行部署命令
npm run deploy
十九、最佳实践
- 组件设计:保持组件小而专一
- 状态管理:合理使用本地状态和全局状态
- 性能优化:避免不必要的渲染
- 代码组织:按功能而非类型组织代码
- 测试:为关键功能编写测试
- 代码风格:保持一致的代码风格
- 文档:为组件编写清晰的文档和示例
二十、常见问题
1. 为什么我的组件没有更新?
- 检查是否正确地调用了 setState
- 检查 shouldComponentUpdate 是否阻止了更新
- 检查 props 是否真的发生了变化
2. 如何获取 DOM 节点?
- 使用 refs
- 不要直接操作 DOM,尽量使用 React 的方式
3. 如何处理异步操作?
- 在 componentDidMount 中发起请求
- 使用 async/await 或 Promise
- 考虑使用 Redux Thunk 或 Redux Saga 处理复杂异步逻辑
4. 如何优化性能?
- 使用 React.memo 或 PureComponent
- 避免在 render 方法中创建新对象或函数
- 使用虚拟化长列表(react-window 或 react-virtualized)
结语
React 是一个强大而灵活的库,适用于构建各种规模的应用程序。通过本教程,你应该已经掌握了 React 的核心概念和常用技术。要成为 React 专家,最好的方法是不断实践和构建项目。React 生态系统非常丰富,还有许多高级主题值得探索,如服务器端渲染、静态站点生成、React Native 等。