React 学习笔记

47 阅读4分钟

环境搭建

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 {
  /* 提取common */
  composes: common from './common.module.css';
  color: red
}

css管理工具

styled-components
classnames // css modules解决方案 

// 解决多个classname的使用 
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

// 重新渲染时, render方法执行前被调用 
  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
  }
  
// 需要比对多个state, props会比较繁琐 => PureComponent 
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');

箭头

&#10094;
&#10095;
&#10005;

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, setCurrentUser] = useState(null)
  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 })