-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
React #10
Comments
React 最佳实践在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。
在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。不建议在 getDefaultProps、getInitialState、shouldComponentUpdate、componentWillUpdate、render 和 componentWillUnmount 中调用 setState,特别注意:不能在 shouldComponentUpdate 和 componentWillUpdate中调用 setState,会导致循环调用。组件接口设计的三个原则:
|
react diff
传统 diff 算法
React diff
diff 策略
ree diff
updateChildren: function(nextNestedChildrenElements, transaction, context) {
updateDepth++;
var errorThrown = true;
try {
this._updateChildren(nextNestedChildrenElements, transaction, context);
errorThrown = false;
} finally {
updateDepth--;
if (!updateDepth) {
if (errorThrown) {
clearQueue();
} else {
processQueue();
}
}
}
}
component diff
element diff
|
React 的设计思想
UI = f(data)
在 React 中一切皆为组件。这是因为:
用户界面就是组件>在上面的图中,一个 Button 是一个界面元素,对应的就是一个 React 组件。在 React 中,一个组件可以是一个类,也可以是一个函数,这取决于这个组件是否有自己的状态。 组件可以嵌套包装组成复杂功能
组件可以用来实现副作用
class Beacon extends React.Component {
render() {
return null;
}
componentDidMount() {
const beacon = new Image();
beacon.src = 'https://domain.name/beacon.gif';
}
}
<div>
<Beacon />
</div> 组件之间的语言:props
|
定义清晰可维护的接口
React 组件设计原则
|
秒表设计class StopWatch extends React.Component {
render() {
return (
<div>
<MajorClock>
<ControlButtons>
<SplitTimes>
</div>
);
}
}
const MajorClock = (props) => {
//TODO: 返回数字时钟的JSX
};
const ControlButtons = (props) => {
//TODO: 返回两个按钮的JSX
};
const SplitTimes = (props) => {
//TODO: 返回所有计次时间的JSX
} state 的位置
组件 props 的设计
const MajorClock = ({milliseconds}) => {
//TODO: 返回数字时钟的JSX
};
MajorClock.propTypes = {
milliseconds: PropTypes.number.isRequired
};
const ControlButtons = (props) => {
//TODO: 返回两个按钮的JSX
};
ControlButtons.propTypes = {
activated: PropTypes.bool,
onStart: PropTypes.func.isRquired,
onPause: PropTypes.func.isRquired,
onSplit: PropTypes.func.isRquired,
onReset: PropTypes.func.isRquired,
};
const SplitTimes = (props) => {
//TODO: 返回所有计次时间的JSX
}
SplitTimes.propTypes = {
splits: PropTypes.arrayOf(PropTypes.number)
};
构建ControlButtons
import React from 'react';
const ControlButtons = () => {
//TODO: 实现ControlButtons
};
export default ControlButtons; import ControlButtons from './ControlButtons';
const ControlButtons = (props) => {
const {activated, onStart, onPause, onReset, onSplit} = props;
if (activated) {
return (
<div>
<button onClick={onSplit}>计次</button>
<button onClick={onPause}>停止</button>
</div>
);
} else {
return (
<div>
<button onClick={onReset}>复位</button>
<button onClick={onStart}>启动</button>
</div>
);
}
};
const ControlButtons = ({activated, onStart, onPause, onReset, onSplit}) => {
}
MajorClock
const MajorClock = ({milliseconds=0}) => {
return <h1>{ms2Time(milliseconds)}</h1>
};
SplitTimes
import MajorClock from './MajorClock';
const SplitTimes = ({value=[]}) => {
return value.map((v, k) => (
<MajorClock key={k} milliseconds={v} />
));
}; StopWatch 状态管理
class StopWatch extends React.Component {
render() {
return (
<Fragment>
<MajorClock />
<ControlButtons />
<SplitTimes />
</Fragment>
);
}
}
constructor() {
super(...arguments);
this.state = {
isStarted: false,
startTime: null,
currentTime: null,
splits: [],
};
} 属性初始化方法
不用 constructor,可以这样初始化 state,效果是完全一样的: class StopWatch extends React.Component {
state = {
isStarted: false,
startTime: null,
currentTime: null,
splits: [],
}
}
constructor() {
super(...arguments);
this.onSplit = this.onSplit.bind(this);
}
this.onSplit = ::this.onSplit;
onSplit = () => {
this.setState({
splits: [...this.state.splits, this.state.currentTime - this.state.startTime]
});
}
|
组件化样式const clockStyle = {
'font-family': 'monospace'
};
const MajorClock = ({milliseconds=0}) => {
return <h1 style={clockStyle}>{ms2Time(milliseconds)}</h1>
}
import "./ControlButtons.css"; 组件化样式的实现方式很多,这里我们介绍最容易理解的一个库,叫做 styled-jsx。npm install react-app-rewired styled-jsx
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",
"eject": "react-scripts eject"
}
const { injectBabelPlugin } = require('react-app-rewired');
module.exports = function override(config, env) {
config = injectBabelPlugin(['styled-jsx/babel'], config);
return config;
}; 使用 styled-jsx 定制样式
const MajorClock = ({milliseconds=0}) => {
return (
<React.Fragment>
<style jsx>{`
h1 {
font-family: monospace;
}
`}</style>
<h1>
{ms2Time(milliseconds)}
</h1>
</React.Fragment>
);
}; 动态 styled jsx
const MajorClock = ({milliseconds=0, activated=false}) => {
return (
<React.Fragment>
<style jsx>{`
h1 {
color: ${activated? 'red' : 'black'};
font-family: monospace;
}
`}</style>
<h1>
{ms2Time(milliseconds)}
</h1>
</React.Fragment>
);
}; |
组件设计模式 :聪明组件和傻瓜组件这种模式的本质,就是把一个功能分配到两个组件中,形成父子关系,外层的父组件负责管理数据状态,内层的子组件只负责展示。
随机笑话样例傻瓜组件
import SmileFace from './yaoming_simile.png';
const Joke = ({value}) => {
return (
<div>
<img src={SmileFace} />
{value || 'loading...' }
</div>
);
} 聪明组件
export default class RandomJoke extends React.Component {
state = {
joke: null
}
render() {
return <Joke value={this.state.joke} />
}
componentDidMount() {
fetch('https://icanhazdadjoke.com/',
{headers: {'Accept': 'application/json'}}
).then(response => {
return response.json();
}).then(json => {
this.setState({joke: json.joke});
});
}
}
PureComponent
class Joke extends React.PureComponent {
render() {
return (
<div>
<img src={SmileFace} />
{this.props.value || 'loading...' }
</div>
);
}
}
React.memo
const Joke = React.memo(() => (
<div>
<img src={SmileFace} />
{this.props.value || 'loading...' }
</div>
)); |
高阶组件(HoC)
基本形式
const withDoNothing = (Component) => {
const NewComponent = (props) => {
return <Component {...props} />;
};
return NewComponent;
}; 用高阶组件抽取共同逻辑
const LogoutButton = () => {
if (getUserId()) {
return ...; // 显示”退出登录“的JSX
} else {
return null;
}
};
const ShoppintCart = () => {
if (getUserId()) {
return ...; // 显示”购物车“的JSX
} else {
return null;
}
};
const withLogin = (Component) => {
const NewComponent = (props) => {
if (getUserId()) {
return <Component {...props} />;
} else {
return null;
}
}
return NewComponent;
};
const LogoutButton = withLogin((props) => {
return ...; // 显示”退出登录“的JSX
});
const ShoppingCart = withLogin(() => {
return ...; // 显示”购物车“的JSX
});
高阶组件的高级用法
const withLoginAndLogout = (ComponentForLogin, ComponentForLogout) => {
const NewComponent = (props) => {
if (getUserId()) {
return <ComponentForLogin {...props} />;
} else {
return <ComponentForLogout{...props} />;
}
}
return NewComponent;
};
const TopButtons = withLoginAndLogout(
LogoutButton,
LoginButton
); 链式调用高阶组件
const X1 = withOne(X);
const X2 = withTwo(X1);
const X3 = withThree(X2);
const SuperX = X3; //最终的SuperX具备三个高阶组件的超能力
const SuperX = withThree(withTwo(withOne(X)));
const hoc = compose(withThree, withTwo, withOne);
const SuperX = hoc(X);
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
不要滥用高阶组件
const withExample = (Component) => {
const NewComponent = (props) => {
return <Component {...props} />;
}
NewComponent.displayName = `withExample(${Component.displayName || Component.name || 'Component'})`;
return NewCompoennt;
};
const Example = () => {
const EnhancedFoo = withExample(Foo);
return <EnhancedFoo />
}
const EnhancedFoo = withExample(Foo);
const Example = () => {
return <EnhancedFoo />
} |
render props 模式
const RenderAll = (props) => {
return(
<React.Fragment>
{props.children(props)}
</React.Fragment>
);
};
<RenderAll>
{() => <h1>hello world</h1>}
</RenderAll> 传递 props
const Login = (props) => {
const userName = getUserName();
if (userName) {
const allProps = {userName, ...props};
return (
<React.Fragment>
{props.children(allProps)}
</React.Fragment>
);
} else {
return null;
}
};
<Login>
{({userName}) => <h1>Hello {userName}</h1>}
</Login>
const Auth= (props) => {
const userName = getUserName();
if (userName) {
const allProps = {userName, ...props};
return (
<React.Fragment>
{props.login(allProps)}
</React.Fragment>
);
} else {
<React.Fragment>
{props.nologin(props)}
</React.Fragment>
}
};
<Auth
login={({userName}) => <h1>Hello {userName}</h1>}
nologin={() => <h1>Please login</h1>}
/> 依赖注入
render props 和高阶组件的比较
const withLogin = (Component) => {
const NewComponent = (props) => {
const userName= getUserName();
if (userName) {
return <Component {...props} userName={userName}/>;
} else {
return null;
}
}
return NewComponent;
};
<Login>
{
(props) => {
const {userName} = props;
return <TheComponent {...props} name={userName} />
}
}
</Login>
|
React 中的“提供者模式”(Provider Pattern)
如何实现提供者模式
React v16.3.0 之前的提供者模式
一个实现“提供者”的例子,组件名为 ThemeProviderclass ThemeProvider extends React.Component {
getChildContext() {
return {
theme: this.props.value
};
}
render() {
return (
<React.Fragment>
{this.props.children}
</React.Fragment>
);
}
}
ThemeProvider.childContextTypes = {
theme: PropTypes.object
};
{
theme: {
//一个对象
}
}
把 Subject 实现为一个类class Subject extends React.Component {
render() {
const {mainColor} = this.context.theme;
return (
<h1 style={{color: mainColor}}>
{this.props.children}
</h1>
);
}
}
Subject.contextTypes = {
theme: PropTypes.object
}
也可以把消费者实现为一个纯函数组件,只不过访问“上下文”的方式有些不同,我们用纯函数的方式实现另一个消费者 Paragraphconst Paragraph = (props, context) => {
const {textColor} = context.theme;
return (
<p style={{color: textColor}}>
{props.children}
</p>
);
};
Paragraph.contextTypes = {
theme: PropTypes.object
};
结合”提供者“和”消费者“。
const Page = () => (
<div>
<Subject>这是标题</Subject>
<Paragraph>
这是正文
</Paragraph>
</div>
);
<ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
<Page />
</ThemeProvider>
React v16.3.0 之后的提供者模式
首先,要用新提供的 createContext 函数创造一个“上下文”对象。
const ThemeContext = React.createContext();
const ThemeProvider = ThemeContext.Provider;
const ThemeConsumer = ThemeContext.Consumer; 使用“消费者”
class Subject extends React.Component {
render() {
return (
<ThemeConsumer>
{
(theme) => (
<h1 style={{color: theme.mainColor}}>
{this.props.children}
</h1>
)
}
</ThemeConsumer>
);
}
}
const Paragraph = (props, context) => {
return (
<ThemeConsumer>
{
(theme) => (
<p style={{color: theme.textColor}}>
{props.children}
</p>
)
}
</ThemeConsumer>
);
}; 实现 Page 的方式并没有变化,而应用 ThemeProvider 的代码和之前也完全一样<ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
<Page />
</ThemeProvider>
|
组合组件
TabItem
const TabItem = (props) => {
const {active, onClick} = props;
const tabStyle = {
'max-width': '150px',
color: active ? 'red' : 'green',
border: active ? '1px red solid' : '0px',
};
return (
<h1 style={tabStyle} onClick={onClick}>
{props.children}
</h1>
);
};
<Tabs>
<TabItem>One</TabItem>
<TabItem>Two</TabItem>
<TabItem>Three</TabItem>
</Tabs>
Tabsclass Tabs extends React.Component {
state = {
activeIndex: 0
}
render() {
const newChildren = React.Children.map(this.props.children, (child, index) => {
if (child.type) {
return React.cloneElement(child, {
active: this.state.activeIndex === index,
onClick: () => this.setState({activeIndex: index})
});
} else {
return child;
}
});
return (
<Fragment>
{newChildren}
</Fragment>
);
}
}
实际应用
|
React 单元测试
Jest
npm test Enzyme
npm i --save-dev enzyme enzyme-adapter-react-16
单元测试
import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
``
>我们对 ControlButtons 组件的测试,就是要渲染它一次,看看渲染结果如何,enzyme 就能帮助我们做这件事。
>比如,我们想要保证渲染出来的内容必须包含两个按钮,其中一个按钮的 class 名是 left-btn,另一个是 right-btn,那么我们就需要下面的单元测试用例:
```ts
import {shallow} from 'enzyme';
it('renders without crashing', () => {
const wrapper = shallow(<ControlButtons />);
expect(wrapper.find('.left-btn')).toHaveLength(1);
expect(wrapper.find('.right-btn')).toHaveLength(1);
});
const Foo = () => ()
<div>
{/* other logic */
<Bar />
</div>
)
代码覆盖率
npm test -- --coverage
|
组件状态
state 不会被同步修改
为什么 setTimeout 能够强迫 setState 同步更新 statesetTimeout(() => {
this.setState({count: 2}); //这会立刻引发重新渲染
console.log(this.state.count); //这里读取的count就是2
}, 0);
setState 的第二个参数
console.log(this.state.count); // 0
this.setState({count: 1}, () => {
console.log(this.state.count); // 这里就是1了
})
console.log(this.state.count); // 依然为0 函数式 setStatethis.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
function increment(state, props) {
return {count: state.count + 1};
}
this.setState(increment);
this.setState(increment);
this.setState(increment);
|
Redux 使用模式
代码组织方式
react-redux
npm install redux react-redux
import {createStore} from 'redux';
import {Provider} from 'react-redux';
const store = createStore(...);
// JSX
<Provider store={store}>
{ // Provider之下的所有组件都可以connect到给定的store }
</Provider>
const CounterView = ({count, onIncrement}) => {
return (
<div>
<div>{count}</div>
<button onClick={onIncrement}>+</button>
</div>
);
};
import {connect} from 'react-redux';
const mapStateToProps = (state) => {
return {
count: state.count
};
}
const mapDispatchToProps = (dispatch) => ({
onIncrement: () => dispatch({type: 'INCREMENT'})
});
const Counter = connect(mapStateToProps, mapDispatchToProps)(CounterView);
Redux 和 React 结合的最佳实践
如何实现异步操作
|
React Router
react router v4 的动态路由
npm install react-router-dom
BrowserRouter
HashRouter
import {HashRouter} from 'react-router-dom';
ReactDOM.render(
<HashRouter>
<App />
</HashRouter>,
document.getElementById('root')
); 使用 Link
const ulStyle = {
'list-style-type': 'none',
margin: 0,
padding: 0,
};
const liStyle = {
display: 'inline-block',
width: '60px',
};
const Navigation = () => (
<header>
<nav>
<ul style={ulStyle}>
<li style={liStyle}><Link to='/'>Home</Link></li>
<li style={liStyle}><Link to='/about'>About</Link></li>
</ul>
</nav>
</header>
) 使用 Route 和 Switch
const Content = () => (
<main>
<Switch>
<Route exact path='/' component={Home}/>
<Route path='/about' component={About}/>
</Switch>
</main>
)
动态路由
<Switch>
<Route exact path='/' component={Home}/>
{
isUserLogin() &&
<Route exact path='/product' component={Product}/>,
}
<Route path='/about' component={About}/>
</Switch>
|
纯原生 HTML 使用 ant design react 组件
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>ant design in html</title>
<link rel="stylesheet" href="./umd/antd.css">
<script src="./umd/react.production.min.js"></script>
<script src="./umd/react-dom.production.min.js"></script>
<script src="./umd/browser5.8.24.js"></script>
<script src="./umd/moment-with-locales.js"></script>
<script src="./umd/antd-with-locales.js"></script>
<script></script>
</head>
<body>
<div id="date"></div>
<script>
moment.locale('zh-cn');
const dateDom = document.querySelector('#date')
const reactEle = React.createElement(antd.DatePicker, {
onChange: (e) => {
console.log(e)
console.log(e._d)
console.log(new Date(e._d).toLocaleTimeString())
}
})
ReactDOM.render(reactEle, dateDom)
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>ant design in html</title>
<link rel="stylesheet" href="./umd/antd.css">
<script src="./umd/react.production.min.js"></script>
<script src="./umd/react-dom.production.min.js"></script>
<script src="./umd/browser5.8.24.js"></script>
<script src="./umd/moment-with-locales.js"></script>
<script src="./umd/antd-with-locales.js"></script>
<script></script>
</head>
<body>
<div id="date"></div>
<script>
moment.locale('zh-cn');
const dataSource = [
{
key: '1',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '2',
name: '胡彦祖',
age: 42,
address: '西湖区湖底公园1号',
},
];
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
},
{
title: '住址',
dataIndex: 'address',
key: 'address',
},
];
const dateDom = document.querySelector('#date')
const reactEle = React.createElement(antd.Table, {
dataSource,
columns,
onChange: (e) => {
console.log(e)
console.log(e._d)
console.log(new Date(e._d).toLocaleTimeString())
}
})
ReactDOM.render(reactEle, dateDom)
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>ant design in html</title>
<link rel="stylesheet" href="./umd/antd.css">
<script src="./umd/react.production.min.js"></script>
<script src="./umd/react-dom.production.min.js"></script>
<script src="./umd/browser5.8.24.js"></script>
<script src="./umd/moment-with-locales.js"></script>
<script src="./umd/antd-with-locales.js"></script>
<script></script>
</head>
<body>
<div id="date"></div>
<script>
const dateDom = document.querySelector('#date')
class LikeButton extends React.Component {
constructor(props) {
super(props);
this.state = {liked: false};
}
render() {
if (this.state.liked) {
return 'You liked this.';
}
return React.createElement(
'button',
{
onClick: () => {
console.log(123)
this.setState({liked: true})
}
},
'Like'
);
}
}
ReactDOM.render(React.createElement(LikeButton), dateDom);
</script>
</body>
</html> |
Hook
Hook API基础 Hook
额外的 Hook
什么时候我会用 Hook
计数器示例
import React,{useState} from 'react'
export default function State1(){
const [count,setCount]=useState(0)
const update=()=>{
setCount(count+1)
}
return (
<div>
<p>{count}</p>
<button onClick={update}>click</button>
</div>
)
}
import React from 'react';
import logo from './logo.svg';
import './App.css';
import State1 from './component/useState'
const App: React.FC = () => {
return (
<div className="App">
<header className="App-header">
<State1 />
</header>
</div>
);
}
export default App; Hook 使用规则
State HookuseState 就是一个 Hook
声明多个 state 变量
function ExampleWithManyStates() {
// 声明多个 state 变量!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
} Effect Hook副作用(作用)
无需清除的 effect
import React, { 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>
);
} useEffect 做了什么
需要清除的 effect
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 运行第一个 effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 运行下一个 effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 运行下一个 effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect 跳过对 effect 的调用
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
对于有清除操作的 effect 同样适用:useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
useEffect 就是一个 Effect Hook
“清除”副作用
import React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
可以在组件中多次使用 useEffect组件在 React 更新 DOM 后会设置一个页面标题:import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用浏览器的 API 更新页面标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
} 自定义 Hook
FriendStatus 的组件
useFriendStatus 的自定义 Hookimport React, { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
} 在两个组件中使用它:function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
} function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
在多个 Hook 之间传递信息
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID); useContext
function Example() {
const locale = useContext(LocaleContext);
const theme = useContext(ThemeContext);
// ...
} useReducer
useReducer 的 Hookfunction useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
} 在组件中使用它,让 reducer 驱动它管理 state:function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);
function handleAddClick(text) {
dispatch({ type: 'add', text });
}
// ...
} function Todos() {
const [todos, dispatch] = useReducer(todosReducer);
// ... |
React 最佳实践 [React best practice]从设计稿开始第一步:将设计好的 UI 划分为组件层级
第二步:用 React 创建一个静态版本将渲染 UI 和添加交互这两个过程分开。
自上而下或者自下而上构建应用
第三步:确定 UI state 的最小(且完整)表示
通过问自己以下三个问题,你可以逐个检查相应数据是否属于 state:
第四步:确定 state 放置的位置
第五步:添加反向数据流
避免不需要的状态 [Unneeded state]
为什么使用状态?
为什么要保持状态尽可能小
单一事实来源
如何避免不必要的状态
检查不需要的状态 - 算法
<!DOCTYPE html>
<html>
<head>
<script src="//fb.me/react-0.13.3.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<div id="form"></div>
</body>
</html> class List extends React.Component {
render() {
let { name, items } = this.props;
let options = [];
options.push(<option value={name}>{name}</option>);
for(var index in items) {
let item = items[index];
options.push(<option value={item}>{item}</option>);
}
return (
<span>
<select onChange={this.props.handler} value={this.props.value ? this.props.value : "Model"}>
{options}
</select>
</span>
);
}
}
class TwoLists extends React.Component {
constructor(props) {
super(props);
this.state = {
brand: null, model: null
};
this.brandChanged = this.brandChanged.bind(this);
this.modelChanged = this.modelChanged.bind(this);
this.buttonClicked = this.buttonClicked.bind(this);
this.knownModel = this.knownModel.bind(this);
}
brandChanged(event) {
let brand = event.target.value;
if(this.knownBrand(brand)) {
let models = this.data()[brand];
this.setState({
brand, model: null,
models: models, buttonDisabled: true,
});
} else {
this.setState({
brand: null, model: null
});
}
}
modelChanged(event) {
let model = event.target.value;
if(this.knownModel(model)) {
this.setState({ model });
} else {
this.setState({ model: null });
}
}
buttonClicked(event) {
let { brand, model } = this.state;
console.log(this.state);
console.log(`${brand} ${model} riding...`);
}
data() {
return (
{
'Opel': ['Agila', 'Astra', 'Corsa', 'Vectra'],
'Škoda': ['Fabia', 'Octavia', 'Superb', 'Yeti'],
'Toyota': ['Auris', 'Avensis', 'Corolla', 'Prius']
}
);
}
buttonDisabled() {
return this.state.model === null || this.state.brand === null;
}
models() {
return this.state.brand ? this.data() [this.state.brand] : [];
}
brands() {
return Object.keys(this.data());
}
knownBrand(brand) {
return this.brands().indexOf(brand) !== -1
}
knownModel(model) {
return this.models().indexOf(model) !== -1
}
render() {
return (
<div id={this.props.id}>
<List name="Brand" items={this.brands()} handler={this.brandChanged} value={this.state.brand} />
<List name="Model" items={this.models()} handler={this.modelChanged} value={this.state.model} />
<button onClick={this.buttonClicked} disabled={this.buttonDisabled()}>Ride</button>
</div>
);
}
}
React.render(
<TwoLists id="two-lists"/>,
document.getElementById("form")); 状态是一种反模式 State is an antipattern
React 组件基本上是黑盒子:
好的 React 组件是直观的
getInitialState(){
return {color: 'green'}
}
onClicked (e){
this.setState({color: 'red'})
} 错误版本 Button2
<Button2/> 正确版本 Button1
<Button1
color={flux.store.wasTheButtonClicked() ? 'red' : 'green'}
onClick={flux.actions.buttonClicked}
/> 好的 React 组件是幂等的
好的 React 组件是纯粹的
<Tweets2 hashtag="reactjs"/>
//The store
getTweets (){
if(this.tweets) return this.tweets;
TweetLibrary.getTweetsFor("reactjs").then(function(tweets){
this.tweets = tweets;
this.emit("change");
}.bind(this));
}
//The component
<Tweets1 tweets={flux.store.getTweets()}/> 如果 React 组件必须有副作用,它们必须是 Flux 动作[或其他回调]
使用状态导致的问题
结论:
|
React 计算属性 [React computed]使用 “计算属性” 的方法Class Component 类组件
class ClassComponent extends Component {
// 对于 props 的一些处理
get computed() {
return this.props.value
}
// 支持计算属性相互依赖
// 在 vue 中,由于不支持 data 相互依赖,但支持 computed 相互依赖,这也是 vue 中非常好用的一点!
get computed2() {
return this.computed + '2'
}
render() {
return (
<div>{this.computed}, {this.computed2}</div>
)
}
}
function App() {
const [state, setState] = useState('old')
useEffect(() => {
// 模拟一些异步处理,变化传入的 props
setTimeout(() => {
setState('new')
}, 4000)
}, [])
return (
<ClassComponent value={state}></ClassComponent>
)
}
Function Component 函数组件
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// 对 props 的处理函数
const handler = (v) => v
function FC({ value }) {
// 通过依赖列表变化 useMemo 重新计算结果达到计算属性实时更新
const computed = useMemo(() => handler(value), [value])
return (
<div>{computed}</div>
)
}
function App() {
const [state, setState] = useState('old')
useEffect(() => {
// 模拟一些异步处理,变化传入的 props
setTimeout(() => {
setState('new')
}, 4000)
}, [])
return (
<FC value={state}></FC>
)
} Function Component Async
// 模拟异步处理 props 的逻辑
const asyncHandler = (v) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`已处理 ${v}`)
}, 2000)
})
}
/**
* 处理 async 业务的 hooks 封装
* @param {Function} func 异步逻辑函数
* @param {Array} dep 依赖列表
* @param {Object} initialValue 初始值
*/
function useAsyncComputed(func, dep, initialValue) {
const [val, setVal] = useState(initialValue)
// 借用 useEffect 执行异步逻辑
useEffect(() => {
let cancel = false
const handler = async () => {
const res = await func()
if(!cancel) {
setVal(res)
}
}
handler()
// 卸载时标记 cancel 已退出,在进程中的异步逻辑将不会再改变 val 值
return () => {
cancel = true
}
}, dep)
return val
}
function AsyncFC({ value }) {
const computed = useAsyncComputed(() => asyncHandler(value), [value], value)
return (
<div>{computed}</div>
)
}
function App() {
const [state, setState] = useState('old')
useEffect(() => {
// 模拟一些异步处理,变化传入的 props
setTimeout(() => {
setState('new')
}, 4000)
}, [])
return (
<AsyncFC value={state}></AsyncFC>
)
}
useAsyncMemonpm install use-async-memo --save
yarn add use-async-memo import {useAsyncMemo} from 'use-async-memo'
const replyMessage = useAsyncMemo(async () => ( // 这里的回调函数变成了async函数
await api.fetchReply(message) // 这里也变成了await
), [message])
const [input, setInput] = useState()
const users = useAsyncMemo(async () => {
if (input === '') return []
return await apiService.searchUsers(input)
}, [input], [])
const [input, setInput] = useState()
const [debouncedInput] = useDebounce(input, 300)
const users = useAsyncMemo(async () => {
if (debouncedInput === '') return []
return await apiService.searchUsers(debouncedInput)
}, [debouncedInput], []) |
React 的真正优势: 组合、单向数据流、免于 DSL、显式变异和静态心智模型
|
设计纯函数组件 design purely functional componentsReact + Flux 和正确的思维方式
工作流程
<UserBox showLoginMenu=true|false/>
<UserBox isLoggedIn={true}/>
无状态组件的处理
<Breakfast coffee={coffeeSettings} eggs={eggsSettings}/>
<Coffee {...coffeeSettings}/>
|
state 与 ref 的监测测试测试 hooks 中的 模拟请求
// 模拟请求函数
export const mockApiCall = (params: unknown): Promise<string> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Received params: ${JSON.stringify(params)}`);
}, 2000);
});
}
import { useState, useEffect } from 'react'
export interface QueryResult<T, P> {
data: T | null
error: Error | null
isLoading: boolean
exec: (p: P) => void
}
// 通用请求 hooks
const useQuery = <T, P>(queryFn: (params: P) => Promise<T>, params: P): QueryResult<T, P> => {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
const exec = async (newParams: P) => {
setIsLoading(true)
// try {
// const response = await queryFn(newParams || params)
// console.log(`response--${JSON.stringify(response)}`)
// setData(response)
// } catch (error) {
// setError(error as Error)
// } finally {
// setIsLoading(false)
// }
queryFn(newParams || params).then((response)=>{
console.log(`queryFn response-${JSON.stringify(response)}`)
setData(response)
}).catch((error)=>{
setError(error as Error)
}).finally(()=>{
setIsLoading(false)
})
}
useEffect(() => {
console.log('queryFn 123')
exec(params)
}, [])
useEffect(() => {
console.log(' queryFn 111111111data:', data)
}, [data])
return { data, error, isLoading, exec }
}
export default useQuery
//import useQuery from "../utils/useQuery";
import { mockApiCall } from "../utils/mock";
import { useEffect, useRef, useState } from "react";
const useTestHookQuery = (params: unknown, lazy = false) => {
const [data, setData] = useState<unknown>();
const dataRef = useRef<unknown>();
const [error, setError] = useState<Error>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const exec = async (newParams: unknown) => {
setIsLoading(true);
mockApiCall(newParams || params)
.then((response) => {
console.log(`响应 response:`);
console.table(response);
setData(response);
dataRef.current = response;
})
.catch((error) => {
setError(error as Error);
})
.finally(() => {
setIsLoading(false);
});
};
useEffect(() => {
if (lazy) return;
exec(params);
}, []);
useEffect(() => {
// useState的值 可以监测变更
console.log(`监测 useTestHookQuery data:-data${data}}`);
console.log(`监测 useTestHookQuery data:-dataRef:${dataRef.current}}`);
}, [data]);
useEffect(() => {
// useRef的值 无法监测变更
console.log(`监测 useTestHookQuery dataRef:-data${data}}`);
console.log(`监测 useTestHookQuery dataRef:-dataRef:${dataRef.current}}`);
}, [dataRef]);
return {
data,
error,
isLoading,
exec,
};
};
export default useTestHookQuery;
import React, { useEffect } from 'react';
import useQuery from '../utils/useQuery';
import {mockApiCall} from '../utils/mock'
const TestSingQuery = () => {
const { data, error, isLoading, exec } = useQuery<string, { userId: number }>(mockApiCall, { userId: 123 });
useEffect(()=>{
console.log(`监测 TestSingQuery data 更新:${data}`)
},[data])
return (
<div>
{isLoading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data && <p>Data: {data}</p>}
<button onClick={() => exec({ userId: 456 })}>Refetch with new params</button>
</div>
);
}
export default TestSingQuery;
import { useEffect, useState } from "react";
import "./App.css";
import TestSingQuery from "./components/TestQueryEffect";
import useTestHookQuery from "./hooks/useTestHookQuery";
function App() {
const [count, setCount] = useState(0);
const { data, exec } = useTestHookQuery({ a: 2222 }, true);
useEffect(() => {
exec({ a: 333 });
}, []);
return (
<>
{/* <TestSingQuery /> */}
{data}
</>
);
}
export default App; |
为什么react推荐使用新对象来更新state,而非直接修改源对象在React中推荐使用新对象来更新state,而不是直接修改源对象,主要是为了确保React能够正确地检测到state的变化,从而有效地管理组件的更新。这里有几个关键原因:
示例代码以下是一个简单的例子,说明如何在React中更新state时创建新对象: import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// 错误的做法:直接修改源对象(不会触发重新渲染)
const incrementWrong = () => {
count++;
};
// 正确的做法:创建一个新对象来更新state
const incrementRight = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementWrong}>Wrong Increment</button>
<button onClick={incrementRight}>Right Increment</button>
</div>
);
}
export default Counter; 在上述代码中, 总结通过保持state不可变性并使用新对象来更新state,React能够更准确地检测和管理组件的更新,确保应用性能和一致性。 是否建议使用map对象来保存,且直接修改map对象来更新state在React中使用Map对象来保存state时,需要小心处理,因为直接修改Map对象可能会导致React无法正确检测到state的变化。为了确保React能够正确地管理组件的更新,仍然建议在更新Map对象时创建一个新对象或进行浅拷贝。以下是一些详细的原因和建议: 为什么不建议直接修改Map对象
示例代码以下是如何在React中正确使用Map对象来更新state的示例: import React, { useState } from 'react';
function MapComponent() {
const [mapState, setMapState] = useState(new Map());
// 错误的做法:直接修改Map对象(不会触发重新渲染)
const updateMapWrong = (key, value) => {
mapState.set(key, value);
setMapState(mapState); // 这不会触发重新渲染
};
// 正确的做法:创建一个新的Map对象
const updateMapRight = (key, value) => {
const newMap = new Map(mapState);
newMap.set(key, value);
setMapState(newMap); // 这会触发重新渲染
};
return (
<div>
<p>Map size: {mapState.size}</p>
<button onClick={() => updateMapWrong('key1', 'value1')}>Wrong Update</button>
<button onClick={() => updateMapRight('key1', 'value1')}>Right Update</button>
</div>
);
}
export default MapComponent; 在上述示例中, 总结在React中,使用Map对象保存state时,为了确保React能够正确地检测到state的变化并管理组件的更新,建议在更新Map对象时创建一个新的Map对象,而不是直接修改原对象。这样可以保持不可变性,确保应用的状态管理更为可靠和可预测。 既然推荐直接用新对象更新,那么面对复杂对象,需要更新的只是其中一个属性怎么办?特别是一个嵌套很深的对象或数组在React中更新嵌套很深的对象或数组时,依旧建议通过不可变性原则来更新状态,即创建一个新的对象,而不是直接修改原对象。这种做法在面对复杂的嵌套结构时可能会比较繁琐,但有几种策略可以帮助简化这个过程,包括使用扩展运算符、结合工具库(如 使用扩展运算符更新嵌套对象对于浅层次的嵌套对象,可以使用扩展运算符来更新特定的属性: import React, { useState } from 'react';
function App() {
const [state, setState] = useState({
user: {
name: 'John',
address: {
city: 'New York',
zip: '10001'
}
}
});
const updateCity = () => {
setState(prevState => ({
...prevState,
user: {
...prevState.user,
address: {
...prevState.user.address,
city: 'Los Angeles'
}
}
}));
};
return (
<div>
<p>City: {state.user.address.city}</p>
<button onClick={updateCity}>Update City</button>
</div>
);
}
export default App; 使用工具库对于嵌套更深的对象,可以使用 Lodashimport React, { useState } from 'react';
import _ from 'lodash';
function App() {
const [state, setState] = useState({
user: {
name: 'John',
address: {
city: 'New York',
zip: '10001'
}
}
});
const updateCity = () => {
const newState = _.cloneDeep(state);
_.set(newState, 'user.address.city', 'Los Angeles');
setState(newState);
};
return (
<div>
<p>City: {state.user.address.city}</p>
<button onClick={updateCity}>Update City</button>
</div>
);
}
export default App; Immutability-helperimport React, { useState } from 'react';
import update from 'immutability-helper';
function App() {
const [state, setState] = useState({
user: {
name: 'John',
address: {
city: 'New York',
zip: '10001'
}
}
});
const updateCity = () => {
const newState = update(state, {
user: {
address: {
city: { $set: 'Los Angeles' }
}
}
});
setState(newState);
};
return (
<div>
<p>City: {state.user.address.city}</p>
<button onClick={updateCity}>Update City</button>
</div>
);
}
export default App; 使用ImmerImmer是一个更现代化的库,它使用“生产者函数”来处理状态的不可变更新,使得代码更加简洁和易读。 import React, { useState } from 'react';
import produce from 'immer';
function App() {
const [state, setState] = useState({
user: {
name: 'John',
address: {
city: 'New York',
zip: '10001'
}
}
});
const updateCity = () => {
setState(produce(draft => {
draft.user.address.city = 'Los Angeles';
}));
};
return (
<div>
<p>City: {state.user.address.city}</p>
<button onClick={updateCity}>Update City</button>
</div>
);
}
export default App; 总结对于嵌套很深的对象或数组,推荐使用工具库或现代状态管理工具来简化不可变状态更新。 嵌套很深的数组状态如何修改是最佳实践?在React中处理嵌套很深的数组状态时,确保以不可变的方式修改状态是非常重要的,因为这样可以避免引发潜在的渲染和性能问题。以下是几种最佳实践来安全和有效地修改嵌套数组: 1. 使用不可变数据更新技术对于简单的结构,可以使用扩展运算符或数组的方法(如 示例:更新嵌套数组中的某一项const updateItemInNestedArray = (data, itemIndex, arrayIndex, newValue) => {
return data.map((item, index) => {
if (index === itemIndex) {
const newArray = [...item];
newArray[arrayIndex] = newValue;
return newArray;
}
return item;
});
}; 2. 使用
|
在 React 中使用 JSX 时,一些属性名称(特别是包含点号的属性名称)会导致语法错误。要解决这个问题,可以将这些属性设置为对象属性,而不是直接在 JSX 中使用它们。const ctl = $('#myWriterControl');
ctl.attr('dctype', 'WriterControlForWASM');
ctl.attr('RuleVisible', 'true');
ctl.attr('RuleBackColor', 'rgb(213, 220, 223)');
ctl.attr('imagedataembedinhtml', 'true');
ctl.attr('RegisterCode', '05D7DFC4AA1E317860F50521D0FC0DF2FA9D179A9393535FE32CAE901F58C5C89F6D07B71387AE26203CC42786E9FE2E4040F0A102BD9F8F5F48477C648F2DD3B092F82F98A23A2E1AECC40B9226BFA5897E9216D95435D320E2898F1EAFE71740DAC50CED00DC4E8FACC196015BCA1B05');
ctl.attr('DocumentOptions.BehaviorOptions.CompressLayoutForFieldBorder', 'true'); |
React 权限控制假设 const userInfo.permissions=['crm:contract-config:query','crm:contract-config:create'] 通过hooks ref 获取组件来控制组件的显示或隐藏,不适合循环创建的组件
import { useRef, useEffect } from 'react'
import { useSelector } from 'react-redux'
// 自定义 Hook 来处理权限
export const usePermissionDirective = () => {
const { userInfo } = useSelector((store: any) => store.preBook)
const refs = {} // 用对象来存储多个 ref
// 内部生成唯一的 ref key
const getRef = (permission) => {
const uniqueKey = Symbol(permission) // 使用 Symbol 生成唯一 key
if (!refs[uniqueKey]) {
refs[uniqueKey] = useRef(null)
}
useEffect(() => {
const el = refs[uniqueKey]?.current
if (el) {
if (!userInfo?.permissions || !userInfo?.permissions?.includes(permission)) {
el.style.display = 'none' // 没有权限则隐藏
} else {
el.style.display = '' // 有权限时显示
}
}
}, [permission, userInfo])
return refs[uniqueKey]
}
return getRef
}
import {usePermissionDirective} from '@/shared/hooks/usePermissionDirective'
const getPermissionRef = usePermissionDirective();
<Button ref={getPermissionRef("crm:contract-config:query")} > 通过包装高阶组件HOC来控制组件的显示或隐藏适合各种情况,可在循环创建的组件中使用
import React from 'react'
import { useSelector } from 'react-redux'
// 定义权限类型
interface UserInfo {
permissions: string[]
}
// 从 Redux store 中选择用户信息
const useUserInfo = () => useSelector((store: any) => store.preBook.userInfo)
// 高阶组件的类型
interface WithPermissionProps {
permissions: string
}
export const withPermission = <P extends object>(WrappedComponent: React.ComponentType<P>) => {
const WithPermissionComponent: React.FC<P & WithPermissionProps> = (props) => {
const userInfo = useUserInfo()
const { permissions, ...restProps } = props as WithPermissionProps
const hasPermission = userInfo?.permissions?.includes(permissions)
// 如果没有权限,则返回 null(不渲染任何内容)
if (!hasPermission) {
return null
}
// 如果有权限,渲染包装组件
return <WrappedComponent {...(restProps as P)} />
}
// 设置 displayName 以便于调试
WithPermissionComponent.displayName = `WithPermission(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`
return WithPermissionComponent
}
const PermissionedButton = withPermission(Button);
const App: React.FC = () => (
<div>
<PermissionedButton permissions='crm:contract-config:query'>
查看合同配置
</PermissionedButton>
<PermissionedButton permissions='erp:purchase-in:create'>
创建采购单
</PermissionedButton>
</div>
);
export default App; |
使用
|
The text was updated successfully, but these errors were encountered: