环境搭建
npx create-react-app my-app
yarn eject
在线创建
https://codesandbox.io/
simple react snippets
简写sfc imrc cc impc
jsx
不是模板引擎语言
声明式方式创建ui, 处理ui逻辑
遵循js语法
jsx规则
在jsx中嵌入表达式, 用{}包裹
大写开头作为组件定义, 小写tag为原生dom节点
jsx标签可以有特定属性和子元素
jsx只能有一个根元素
fragments
可以包含并列的子元素
function App() {
return (
<React.Fragment> // <>
<div className='container'>
<ListItem />
<ListItem />
<ListItem />
</div>
<div className='container'>
<ListItem />
<ListItem />
<ListItem />
</div>
</React.Fragment>
)
}
函数组件
无状态组件
没有this
没有生命周期
纯函数
css module
react打包会将所有css样式打包到一个文件中
<div style=“{{color: ‘red’}}” />
cssModule
消除全局污染
消除命名混乱
import style from './listItem.module.css'
className={style.title}
.title {
composes: common from './common.module.css';
color: red
}
css管理工具
styled-components
classnames
import classNames from 'classnames/bind'
const cls = classNames.bind(style)
<div className={cls('title’, ‘xx’)}>css module</div>
import cn from 'classnames'
const _cn = cn({
title: true
})
事件
<button onClick={(e) => this.handleClick(e, this.props.data.id)}>Button</button>
React事件是合成时间, 不是DOM原生事件
在document监听所有支持事件
使用统一的分发函数dispatchEvent
State
是否通过props从父组件获取
是否可以通过其他state和props计算得到
是否在render方法中使用
constructor(props) {
super(props)
this.state = {
count: 0
}
}
this.setState({
count: this.state.count + 1
})
this.setState(
{
count: this.state.count + 1
},
() => {
console.log('count: ', this.state.count)
}
)
State的更新是一个浅合并 shallow merge 对相应属性做修改
state和props
State
可变的
组件内部
交互或其他ui造成的数据更新, 一般造成页面重新渲染
Props
在组件内部不可变
父组件传入
从上而下的简单数据流
shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState) {
console.log('props', this.props, nextProps)
console.log('state', this.state, nextState)
if (this.state.count !== nextState.count) {
return true
}
if (this.props.id !== nextProps.id) {
return true
}
return false
}
class Xxx extends PureComponent{}
生命周期
创建阶段
constructor (props, state)
componentWillMount
render
componentDidMount
更新阶段
componentWillReceiveProps
shouldComponentUpdate
render
componentDidUpdate
卸载阶段
componentWillUnmount
创建阶段
初始化阶段
constructor
初始化内部函数, 显性设置合隐形设置
需要使用super()调用基类的构造方法
可以直接修改state
componentWillMount
ui渲染完成前调用
只执行一次
这里调用setState不会触发render
render
一个组件必有的方法
返回一个顶级的react元素
渲染的是Dom Tree的一个React对象
componentDidMount
UI渲染完成后调用
只执行一次
获取一些外部数据资源
更新阶段
componentWillReceiveProps
组件接受到新props的时候触发(不包括state)
再次对新props和原来的props对比
不推荐使用
shouldComponentUpdate
是否要继续执行render方法
可以由PureComponent自动实现
卸载阶段
componentWillUnmount
组件移除时使用
可以用来做资源释放
组件设计模式
高阶组件
component -> Function -> component
const NewComponent = higherOrderComponent(OldComponent)
withToolTip.js
import React from 'react'
const withToolTip = (Component) => {
class HOC extends React.Component {
state = {
showToolTip: false,
content: ''
}
handleOver = (e) => {
this.setState({
showToolTip: true,
content: e.target.innerText
})
}
handleOut = (e) => {
this.setState({
showToolTip: false,
content: ''
})
}
render() {
return (
<div onMouseOver={this.handleOver} onMouseOut={this.handleOut}>
<Component action={this.state} />
</div>
)
}
}
return HOC
}
export default withToolTip
itemA.jsx
import React from 'react'
import withToolTip from './withToolTip'
const ItemA = (props) => {
return (
<div className='container'>
<button className='btn btn-primary' type='btn'>
ToolTip A
</button>
{props.action.showToolTip && (
<span className='badge badge-pill badge-primary ml-2'>
{props.action.content}
</span>
)}
</div>
)
}
export default withToolTip(ItemA)
一个函数, 传入一个组件, 返回一个新的组件
一般不会有ui展示
提供一些可复用的功能
函数作为子组件
render props
1. 定义子组件
2. 使用函数作为props
withToolTip.js
import React from 'react'
class WithToolTip extends React.Component {
state = {
showToolTip: false,
content: ''
}
handleOver = (e) => {
this.setState({
showToolTip: true,
content: e.target.innerText
})
}
handleOut = (e) => {
this.setState({
showToolTip: false,
content: ''
})
}
render() {
return (
<div onMouseOver={this.handleOver} onMouseOut={this.handleOut}>
{this.props.render(this.state)}
</div>
)
}
}
export default WithToolTip
itemB.jsx
import React from 'react'
import WithToolTip from './withToolTip'
const ItemB = (props) => {
return (
<div className='container'>
<WithToolTip
render={({ showToolTip, content }) => (
<>
<button className='btn btn-primary' type='btn'>
ToolTip B
</button>
{showToolTip && <span className='badge badge-pill badge-primary ml-2'>{content}</span>}
</>
)}
></WithToolTip>
</div>
)
}
export default ItemB
可以使用 this.props.children 替代 this.props.render 类似于Vue的插槽
Virtual DOM
UI节点抽象
跨平台性
React通过render方法, 渲染Virtual DOM 从而渲染出真实的DOM
通过调用setState方法触发VDOM更新
Virtual DOM Diff
组件级别比较
元素级别比较
移动子节点 创建子节点 删除子节点
React 特点
不需要手动操作DOM, 框架会去做(声明式)
components
单向数据流 undirectional data flow
only ui, the next to u 跨平台 library
basics
upgrade
pnpm upgrade react react-dom --latest
npx
运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
使用npx可以避免全局安装模块,比如,create-react-app这个模块是全局安装,npx 可以运行它,而且不进行全局安装。
npx 将create-react-app下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载create-react-app。
下载全局模块时,npx 允许指定版本。
npx webpack@4.44.1 ./src/index.js -o ./dist/main.js
利用 npx 指定某个版本的 Node 运行脚本。
npx node@14.10.0 -v
上面命令会使用 14.10.0 版本的 Node 执行脚本。原理是从 npm 下载这个版本的 node,使用后再删掉。
setState
异步更新: 出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用
要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数
this.setState(
(state, props) => {
return {
name: { firstName: 'Jane', lastName: 'Doe' },
company: 'Google'
}
},
() => {
console.log(this.state)
}
)
与state进行浅合并 shallow merge setState()会重新执行一次render
Mapping Array
import React, { Component } from 'react'
class Demo extends Component {
constructor() {
super()
this.state = {
monsters: [
{
name: 'Frankenstein'
},
{
name: 'Dracula'
},
{
name: 'Zombie'
}
]
}
}
render() {
return (
<main
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
{this.state.monsters.map((item) => {
return <h1 key={item.name}>{item.name}</h1>
})}
</main>
)
}
}
export default Demo
lifecycle Method
constructor() render() componentDidMount()
当props改变 调用setState或forceUpdate 时, 组件会执行render
storing original Data
this.state = {
monsters: [],
searchField: ‘’
}
render() {
const filteredMonsters = this.state.monsters.filter(...)
return <div>{filteredMonsters}</div>
}
optimizations
<input
className='search-box'
type='search'
placeholder='search monsters'
onChange={(e) => { // => this.onSearchChange
const searchField = e.target.value.toLocaleLowerCase()
this.setState(() => {
return { searchField }
})
}}
/>
在render中使用匿名函数, 每次更新会重新绑定, 浪费性能
可以提到render外面, 只构建一次, 无论何时渲染, 它总是会引用这个已被初始化的方法
render() {
const { monsters, searchField } = this.state
const { onSearchChange } = this
}
cardList Component
components
-card-list
-card-list.component.jsx
img
https://robohash.org/${monster.id}?set=set&size=180x180
pure & impure Functions
function pureFunc(a, b) {
return a + b
}
let c = 1
function funcA(a, b) {
return a + b + c
}
c = 2
function funcB(a, b) {
c = a + b // side effect
}
pure: 函数的返回应该完全依赖于传递的参数
hooks
react 每次需要重新渲染时都会运行整个函数
const [monsters, setMonsters] = useState([])
useEffect
useEffect(() => {}, [])
会在初次渲染及依赖项发生变化时执行
strict mode changes
会双重渲染组件, 以便可以捕捉到任何可能发生的奇怪行为以及其中的副作用
在react开发者模式下 变灰的打印是strick模式下的输出
virtual DOM
<script>
const Person = props => {
return React.createElement('div', null, [
React.createElement('h1', { key: 'h1' }, `Hi, I'm ${props.name}`),
React.createElement('p', { key: 'p' }, `I am ${props.age} years old`)
])
}
const App = () => {
return React.createElement('div', null, [
React.createElement('h1', { className: 'title', key: 'h1' }, 'Hello World'),
React.createElement(Person, { name: 'Max', age: 28, key: 'person1' }),
]
)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
devtools rendering
paint flashing DOM使用它来确定实际正在渲染或重新渲染得内容
create branch
git checkout -b ‘xxx’
react clothing
category-item
渲染网络图片
backgroundImage: `url(${imageUrl})`
routes 6
import { BrowserRouter } from ‘react-router-dom’
<BrowserRouter>
<App />
</BrowserRouter>
import { routes, router } from ‘react-router-dom’
const App = () => {
return (
<Routes>
<Route path=‘/’ index element={<Home />}
</Routes>
)
}
Outlet
const Navigation = () => {
return (
<div>
<div>
<h1>I am the navigation page</h1>
</div>
<Outlet /> // 渲染的出口, 呈现元素的位置
</div>
)
}
const App = () => {
return (
<Routes>
<Route path='/' element={<Navigation />}>
<Route index element={<Home />} />
<Route path='shop' element={<Shop />} />
</Route>
</Routes>
)
}
Link
const Navigation = () => {
return (
<>
<div className='navigation'>
<Link className='logo-container' to='/'>
<CrwnLogo className='logo' />
</Link>
<div className='nav-links-container'>
<Link className='nav-link' to='/shop'>
SHOP
</Link>
<Link className='nav-link' to='/sign-in'>
SIGN IN
</Link>
</div>
</div>
<Outlet />
</>
)
}
svg
import { ReactComponent as CrwnLogo } from '../../assets/crown.svg'
<CrwnLogo />
firebase
pnpm i firebase
Auth
发送登录信息给google, google返回auth_token给客户端, 客户端将auth_token发送给Firebase, Firebase向google验证auth_token, 确认后Firebase生成access_token并返回给客户端, 客户端发送请求时携带此access_token
sign-in.component.jsx
import {
signInWithGooglePopup,
createUserDocumentFromAuth
} from '../../utils/firebase/firebase.utils'
const SignIn = () => {
const logGoogleUser = async () => {
const { user } = await signInWithGooglePopup()
const userDocRef = await createUserDocumentFromAuth(user)
console.log('userDocRef: ', userDocRef)
}
return (
<div>
<h1>Sign In Page</h1>
<button onClick={logGoogleUser}>Sign in with Google Popup</button>
</div>
)
}
export default SignIn
form-input
const FormInput = ({ label, ...otherProps }) => {
return (
<div>
<label>{label}</label>
<input {...otherProps} />
</div>
)
}
export default FormInput
Context
import { createContext, useState } from "react";
export const UserContext = createContext({
setCurrentUser: () => null,
currentUser: null,
});
export const UserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const value = { currentUser, setCurrentUser };
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
index.js
root.render(
<BrowserRouter>
<UserProvider>
<App />
</UserProvider>
</BrowserRouter>
)
sign-up.component.jsx
const { setCurrentUser } = useContext(UserContext)
setCurrentUser(user)
re-render
调用useContext()函数的组件会re-render
import { createContext, useState } from 'react'
import PRODUCTS from '../shop-data.json'
export const ProductsContext = createContext({
products: []
})
export const ProductsProvider = ({ children }) => {
const [products] = useState(PRODUCTS)
const value = { products }
return <ProductsContext.Provider value={value}>{children}</ProductsContext.Provider>
}
useNavigate()
const navigate = useNavigate()
navigate('/checkout')
箭头
&
&
&
firesore no-sql
不必遵守任何数据结构
nested Routes
<Route path='shop/*' element={<Shop />} />
<Routes>
<Route index element={<CategoriesPreview />}></Route>
<Route path=':category' element={<Category />}></Route>
</Routes>
const { category } = useParams()
styled-components
动态构建唯一类名, 防止类名冲突
styles.jsx
import styled from 'styled-components';
export const CategoryContainer = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
column-gap: 20px;
row-gap: 50px;
`;
export const Title = styled.h2`
font-size: 38px;
margin-bottom: 25px;
text-align: center;
`;
export const LogoContainer = styled(Link)`
height: 100%;
width: 70px;
padding: 25px;
`
<NavLink as='span' onClick={signOutUser}>
SIGN OUT
</NavLink>
import { NavigationContainer } from './navigation.styles'
<NavigationContainer>...</NavigationContainer>
export const GoogleSignInButton = styled(BaseButton)`
background-color: #4285f4;
color: white;
&:hover {
background-color: #357ae8;
border: none;
}
`
export const InvertedButton = styled(BaseButton)`
background-color: white;
color: black;
border: 1px solid black;
&:hover {
background-color: black;
color: white;
border: none;
}
`
export const CartDropdownContainer = styled.div`
position: absolute;
width: 240px;
height: 340px;
display: flex;
flex-direction: column;
padding: 20px;
border: 1px solid black;
background-color: white;
top: 90px;
right: 40px;
z-index: 5;
${BaseButton},
${GoogleSignInButton},
${InvertedButton} {
margin-top: auto;
}
`
<BackgroundImage imageUrl={imageUrl} />
export const BackgroundImage = styled.div`
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-image: ${({ imageUrl }) => `url(${imageUrl})`};
`
const shrinkLabelStyles = css`
top: -14px;
font-size: 12px;
color: ${mainColor};
`;
&:focus ~ ${FormInputLabel} {
${shrinkLabelStyles};
}
User reducer
export const UserContext = createContext({
setCurrentUser: () => null,
currentUser: null
})
export const USER_ACTION_TYPES = {
SET_CURRENT_USER: 'SET_CURRENT_USER'
}
const userReducer = (state, action) => {
const { type, payload } = action
switch (type) {
case USER_ACTION_TYPES.SET_CURRENT_USER:
return {
...state,
currentUser: payload
}
default:
throw new Error(`Unhandled type ${type} in userReducer`)
}
}
const INITIAL_STATE = {
currentUser: null
}
export const UserProvider = ({ children }) => {
const [{ currentUser }, dispatch] = useReducer(userReducer, INITIAL_STATE)
const setCurrentUser = (user) => {
dispatch({ type: USER_ACTION_TYPES.SET_CURRENT_USER, payload: user })
}
const value = { currentUser, setCurrentUser }
useEffect(() => {
const unsubcribe = onAuthStateChangedListener((user) => {
if (user) {
createUserDocumentFromAuth(user)
}
setCurrentUser(user)
})
return unsubcribe
}, [])
return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}
cartReducer
reducer内部不处理任何逻辑
export const createAction = (type, payload) => ({ type, payload })