Open DerekHung opened 7 years ago
這邊使用 textfeild (react處理起來相當麻煩的component..)為範例 訂定一個未來重構後的標準component 的實作方式
在新的作法中我們會把react 元件拆分成 Data Layer container 跟 Presentational component Data Layer Container 負責跟Redux store 串接,集中管理資料流與傳遞props Presentational component只負責render markup 以及UI邏輯處理,不能串接redux store 各司其職讓UI跟Data抽離,也讓元件更容易共用跟測試
textfeild.js
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import CSSModules from 'react-css-modules';
import calculateNodeHeight from './calculateNodeHeight';
import style from './style.css';
class TextFeild extends Component {
constructor(props) {
super(props);
this.state = {
style: {}
}
this.inputNode = null;
}
// react lifeCycle method 寫在render 上方,方便總覽整隻程式的生命週期
componentDidMount() {
// 當component initial 完成之後 (client render)去抓取node的DOM結構並且計算真實的style
// 寫進state 裡面 (state裡面只存放跟data 沒有關連的UI邏輯 )
this.setState({
style: this._resizeElement(this.inputNode)
})
}
componentWillReceiveProps(nextProps) {
// 當父層的value 更新之後pass value 下來會在這邊接收到更新訊息
// 這時候重新計算input 的scrollHeight,取得應該有的高度寫進state裡
if ( nextProps.value !== this.props.value ) {
this.setState({
style: this._resizeElement(this.inputNode)
})
}
}
// 使用自定義的render function 回傳複雜的條件判斷markup
// 盡量避免在function 內產生side effect,保持function 的pure 方便測試
get renderInput() {
const { style } = this.state;
const { type, ...others } = this.props;
switch(type){
case 'textarea':
return (
<textarea
ref={ node => { this.inputNode = node;}}
style={style}
{ ...others }
/>
);
default:
return (
<input
ref={ node => { inputNode = node;}}
{ ...others }
/>
);
}
}
render() {
// 強制props 只能用object spread 方式取值,避免直接使用 this.props.data 的方式
const { minRows, maxRows,maxWords, value, errorMessage, className, type, ...others} = this.props;
// 將static markup 用變數存起來,方便整理跟重用
const renderMaxWords = maxWords ? (
<span styleName="maxWord">
<span styleName="front">{value.length}</span>/{maxWords}
</span>
): null;
const renderErrorMessage = errorMessage ? (
<div styleName="errorMessage">{ errorMessage }</div>
) : null;
// render return 的JSX語法盡量簡潔,並且用意義清楚的方式命名
return (
<div styleName={'input inputRoot'} className={className}>
{ this.renderInput }
{ renderMaxWords }
{ renderErrorMessage }
</div>
);
}
// 自定義function 寫在render 下方,區隔開lifecycle method
_resizeElement(inputNode) {
return calculateNodeHeight(
inputNode,
false,
this.props.minRows,
this.props.maxRows
)
}
}
// 嚴格定義propTypes,必要參數跟optional 參數必須定義清楚
TextFeild.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
errorMessage: PropTypes.string,
minRows: PropTypes.number,
maxRows: PropTypes.number,
maxWords: PropTypes.number,
onChange: PropTypes.func,
onBlur: PropTypes.func,
onFocus: PropTypes.func,
onKeyDown: PropTypes.func,
}
TextFeild.defaultProps = {
// type could be number, text, email, textarea... html5 input type
type: 'text',
minRows: 2,
disabled: false
}
export default CSSModules(TextFeild,style,{allowMultiple:true});
這邊是一個簡單的Form 表單實例,Data Layer Container層專注在管理資料的同步跟更新議題 更理想的作法是把裡面的markup 全部拆分出去成為Stateless component 但這沒有絕對正確的作法,更多的時候是在眾多優劣中取得平衡,但,一定要經過縝密的討論跟思考 這部份的選擇未來會是團隊緊密討論跟code review 產出共同的實作認知
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import CSSModules from 'react-css-modules';
import style from './style.css';
import TextField from 'client/components/textfeild';
import { getPersonalData, submitData } from 'action';
class Form extends Component {
constructor(props) {
super(props);
// container 層接收store的資料之後在生命週期內保存資料
this.state = {
formData: props.user
}
}
componentWillMount(){
// 取得store 裡的personal data 保存進state裡面
this.props.getPersonalData();
}
render(){
const { name, phone, email } = this.state.formData;
const renderInput = (value) => {
return(
<TextFeild
name={value}
value={value}
styleName="input"
type="text"
maxWords={10}
onChange={this._inputOnChange}
/>
)
}
return(
<div>
<h2>姓名</h2>
{ renderInput(name) }
<h2>電話</h2>
{ renderInput(phone) }
<h2>email</h2>
{ renderInput(email) }
<button onClick={this._handleSubmit} />
</div>
)
}
_inputOnChange(e) {
// 接收到子層的popup 事件之後更新container 層的資料 (資料的保存更新、連結store方式全部都在container層)
const { formData } = this.state;
formData[e.target.name] = e.target.value;
this.setState(formData);
}
_handleSubmit(e) {
// button click之後發送submit action
const { formData } = this.state;
this.props.submitData(formData);
}
}
function mapStateToProps(state, props) {
return {
user: state.user
};
}
export default compose(
connect(mapStateToProps, { getPersonalData, submitData }),
[CSSModules, '_', css]
)(Form);
React 的核心概念是data-driven component 而在目前WAP的component 在串接資料上並沒有被明確規範 導致資料的處理上相當混亂,也直接導致了在使用者做了某些互動就會容易產生資料不一致的狀況 以下節錄某一支component 並且針對裡面的錯誤切割出來講解
這個component 本身有連結redux store data ,可是自身的state 又自己擁有data去做handle,導致資料的更新跟維護上面變得相當複雜(需要隨時注意資料同步的問題)
這邊在componentRecieveProps 用了大量的判斷式去確保資料的同步,但是造成了閱讀上相當困難,也很難確保所有資料有被同步到
大量功能重複的函式以及無意義的 flag,沒有註解/ flag map 無從得知這個function 會有什麼結果
render 裡面放了大量的markup 跟 邏輯判斷式,但我們很難驗證這些flag 到底數值的意義跟正確性 同時我們也很難得知到底是store裡面的data 有錯還是 state 裡面的data 有錯 再來是大量的結構其實可以共用,但這邊沒有做好拆分