React无状态组件——为可复用而生

#1

react的核心之一是组件,那么在实际开发中,如何利用react提供的jsx语法写好一个可复用的组件呢?

最常用的组件是“无状态组件”,所谓无状态,也可以叫做无生命周期,无state,组件是一个纯jsx类或者对象。

展示一个常用的移动端头部导航组件

export class Header extends Component {

render() {
    const {title, imgUrl, linkTo, bgColor} = this.props
    //提供4个接口参数给父容器做设置,可以不传参。
    return (
        <header className='header' style={bgColor}>
            {title}
            <Link to={linkTo} className="a_link" >
                <img src={imgUrl} className="a_img" />
            </Link>
        </header>
    )
}
//严格来说,这些暴露给外部的参数都需要做验证,常用的验证类型为array,bool,func,number,object,string
static propTypes = {
    title: React.PropTypes.string.isRequired
}

}

title表示标题,imgUrl表示图片路径,linkTo表示下一页的路由地址,bgColor表示头部的背景颜色,通过外部参数来定制这4个部分,就可以让一个头部组件实现复用。

这个组件内部没有任何的生命周期和state状态,那么如果需要管理state状态,该怎么办呢?
解决方案是用redux,暂不做介绍。

2 Likes
#2

可以用内部的state呀,不依赖外部状态就不影响可复性吧

#3

如果用了内部state,你会发现复用性大打折扣,我所说的是无状态组件,内部包含自己的this.state的组件不属于无状态,这样的组件只能用于特定的内部逻辑不变的情况。
通常像按钮,列表,导航这些组件,state交给redux来管理就非常合适了,那么内部包含state的组件适用那些情况呢?
还真没发现。。因为我已经把项目中的全部state都放到reducer,并且不出现任何管理不方便的问题。。
像定时器这类的组件,一样交给reducer管理。。

#4

试试我的架构吧,状态托管和跨组件通讯的pub/sub结构

#5

内部state还是有的。
举个例子,原生的select标签,下拉框的开闭,本质上就是内部state。

如果我们写个组件替代原生select,拆分出reducer和view component,那反而对使用方的应用架构做出了强要求,从而伤害了复用性。

扯得远一点,redux的来源是分形的elm,将全部state都放reducer也是elm中的做法,但所有分形架构都会遇到叶子结点问题,即组件树的叶子结点一定是不可分形且也具备一定逻辑的(比如原生select, input标签),很多全分形架构的开源作者都支持web component,就是因为它能解决叶子结点的问题

1 Like
#6

web component更强调封装和复用,react更强调让dom同步数据,如果是react的话,我觉得基本可以不需要本地状态了,如果你有优雅的方案,任何状态都拿出来更适合

#7

下拉框本身就是一个完整的组件,跟react自定义组件不是一回事,那么举一个例子,toast提示框。

关于toast,几乎在每个网页多个地方都可能出现,而且提示的文本也不一样,一般程序员会觉得toast的显示和隐藏应该是内部state来控制,因为它需要全局多处复用。

那么我就分享一下用reducer来管理toast的state的方式。

1、写一个toast无状态组件,组件内部只是个纯JSX,没有任何的state。

export class Toast extends Component {
    render () {
       const { text } = this.props
       return (
          <div><span>{text}</span></div>
       )
   }
}

2、定义一个action来控制toast的状态,toast分为显示和隐藏2种状态。

exprots const toastAction = (bool, toastText) => {
 //传入一个bool值,来控制toast的显示和隐藏,传入一个显示的文字内容
    return {
        type: "TOAST_ACTION",
        bool,
        toastText
    }
}

3、在reducer定义组件的初始状态

const initState = {
    toast: false //初始为false,组件隐藏状态,
    toastText: null
}

exports const global = (state = initState, action) => {
     switch(action.type) {
         case "TOAST_ACTION":
               return {
                    ...state,
                    toast: action.bool,
                    toastText: action.toastText
               }
     }
}

4、在父容器调用组件

render() {
    const { toast, toastText } = this.props.global
    return (
        <div>
            //这里导入你的其他组件
           //toast组件默认导入进来,然后根据toast的外部state状态来判断是否显示toast组件,并且传入一个文本,最好的办法是把他放到全局container来,只需要导入一次组件即可。
           {
                toast ? <Toast text={toastText} /> : null
            }
        </div>
    )
}

4、触发action显示toast,在你的button或者其他按钮事件来触发即可。无论你在哪个页面,都可以触发这个组件。

handleClick () {
   this.props.toastAction(true, "登录成功")
}

5、用redux框架的一个基本的toast提示框,我写的这个组件不完美,因为toast组件还需要传入时间,还有到时卸载组件发送一个this.props.toastAction(false)请求。

基本原理就是这样,如果你对redux不熟悉,就会觉得这样很麻烦,但是当你的项目变的庞大,并且多人一起开发的时候,redux的优势就出来了,每个开发人员都遵循这样的数据流,直接this.props.action来改变组件的状态,就能调用组件了。如果一个页面需要多个地方调用同一个组件,那么可能需要在reducer存入当前显示组件的id,也就是用数组来保存状态。在多个下拉弹出框的业务场景就会用到数组保存状态。

话说你能想到的大部分场景,我都已经用redux实现了,因为只要理解了redux怎么回事,那么无论你是什么组件,什么特效,都是用这个原理步骤去写代码,这是从前端工程的角度去管理一个项目。

有兴趣的话可以探讨其他组件模型的实现,搜索框、文本框、按钮、卡片、日历、分页列表、导航等等。。好吧,日历我还没做过,用原生的日历。。

#8

redux对行为的把控非常赞,流水线式的reducer和扁平的state也很好。但是redux把一个模块拆分成:action,reducer,component,每次改动都需要文件*3,而且新手需要把这3个概念脑补成一个对象还是有点难度的,我的方案是这样的:

1、定义一个无状态的Toast组件

@lift({display: "hide", text: ""})
export class Toast extends Component {
    render () {
       const { display, text } = this.props
       return (
          <div className={display}><span>{text}</span></div>
       )
   }
}

2、在一个组件中直接修改toast组件的状态

handleClick = () => Store.Toast.setState({display: "show", text: "登录成功"})

这里看起来有点耦合,解耦的问题可以再谈

基本原理就是每个lift之后的组件都是一对模块化的pub/sub组件,而且状态是拿出来并且放在全局rootStore里面的,可以随时修改,而且是响应式的。

#9

其实你用到的是改变className来显示和隐藏toast,也就是说你的组件必须先加载进来,如果所有组件都是这样的模式,那么在不需要这个组件的时候,也把div什么的先加载进来,就显得多余了一些。而且这样的方式跟jQuery show() hide()没区别。。不能看成是组件、、、

#10

改变Toast组件的display状态怎么能和jquery是一回事呢。
这就和你dispatch改变Toast的state是一回事

#11

className也是组件的一个属性,这个属性我还没有在这个社区提过,也是最近才发现的,我的产品经理提了一个需求,就是根据不同的客户,要自定义组件的背景图片,还有一些样式,然后我发现样式也应提取出来当初组件提供给外部的一个接口。这些样式和文本,还有其他内部属性,都应该是构成组件的一个整体,不能把属性当成控制组件显示的状态。这样做是可以实现,但是用起来对样式的定制有影响。

#12

你说的对,样式也是组件的状态一部分,我上面说的是我解决问题的方式(声明式的状态,还有之后声明式的数据源),是否用className可以再谈的

#13

在以前我也用过你所说的方式来实现组件的控制,但是带来的问题就是组件管理不统一,然后别人阅读我写的代码的时候,就要从不同的角度去思考组件的挂载和卸载。最后我把所有组件都统一给reducer管理,不会有第二种方式,这样写代码轻松多了。。刷刷刷的写几百个都行。。别人一眼也能看懂怎么回事。。

#14

除非你的redux是用的duck module,不然可读性也好不到哪去,用redux开发的同事开发一个新功能要开4个窗口。声明式的代码可读性都差的话,难道命令式的dispatch(action)就可读性强吗

#15

别吓我,4个窗口。。那说明你同事和我的架构不是一回事。。虽然很多人在用redux,但不是每个人都是一样的架构。。我用的redux架构是在不断的项目经验中总结出来的,跟网上学的不是一回事。。而且我不是用dispatch(action)来控制的。。是this.props.action()。。。

#16

我假设你是这样获取数据的

componentWillReceive(nextProps){
  if(this.props.pid !== nextProps.tid){
    this.props.getData(pid)
  }
}
componentDidMount(){
   this.props.getData(pid)
}

这只是你的组件就已经很命令式的告诉组件怎么获取数据而且有点啰嗦,这还不包括你之后的action和reducer和store。我的代码是这样的

@inject(getData, "data")
@lift({data: []})
class App extends React.Component<any, void>{}

const getData = (store, state) => {
  if(store.pid !== state) return fetch(pid)
}

只需要声明一个数据源,而且只需要1次

#17

各有千秋吧,我是怎样获取数据的,已经在上面写的toast组件的实例说明了,你一直说的是redux的缺点,就是多层次的数据流,改变一个state,要通过dispatch,action,reducer,store,component,而react本身加上一些基本的逻辑代码也能控制state,简洁性上看是你写的好一些,从复杂的项目管理上就不一定适用。我不知道你的项目有多复杂,至少我现在做的项目,同一份代码要做不同的移动端版本,并且实现跨版本的组件复用和状态管理,如果不用redux,每个state都交给自己去管,写起来会崩溃的。。

#18

说起模块化和组件是要把你的文件夹拷过去就能用的,高内聚的模块才是好模块,新环境如果是redux你copy一个组件过去要额外准备多少文件?

#19

。。你想多了。。组件是不需要copy的,而是一份组件,多个地方共用的,只需要调用组件名就行了,不用复制整个组件。copy可耻。。

#20

没有内部state,表示这个组件和系统是紧耦合的,很难在系统外复用吧