区分状态级别在组件设计中的重要性

818 阅读3分钟

随着全局状态管理库如vuex,mobx,redux的盛行,人们越来越习惯把状态一股脑丢在状态管理库里面,却忘了去划分状态的级别。这种滥用全局状态管理库的现象一直普遍存在。

什么叫滥用全局状态管理库?

就是没有认识到状态管理库的根本作用,或者说我们什么时候才需要状态管理库?

拿 react 来说,react 是有组内状态的,状态可以通过 props 传递。但是,但当 app 比较庞大的时候,兄弟组件,远亲组件这些的交流就变得困难起来, 它们必须依赖相同的父组件来完成信息的传递。这时,就是我们使用状态管理库的时候。

但是,很多人把所有状态都往 redux 里面丢,虽然这方便了开发,但缺点却很明显:

  1. 组件很难复用:因为状态保存在global,所以只有一份。你在多个地方使用了这个组件,但因为状态只有一份,这个组件的多个实例是相互影响的,这很糟糕。
  2. 耦合度高:根据高内聚低耦合的设计原则,我们在设计组件的时候应该保证这个组件有独立的功能,通过props来跟外界交流。但如果把组件的状态放在全局 model而不是放在组件内部,就违背了内聚这一规则。其次,也提供了让其他地方修改这份model的能力,使用这个组件的人不得不担心是否有地方影响到这份model,这很糟糕。

正确的做法

划分好状态的等级,尽量把状态放在组件内。当遇到需要共享组内状态的时候,提升状态到父级或者全局状态管理库。

你可以把view、model、loadData逻辑都塞进这个组件,这样它便有了独立功能。它把复杂度隐藏在内部,然后向外界暴露一些api来提供自己的能力。

例如一个列表组件:

// 方案一
// ListDemo.jsx
import React,{useEffect} from 'react';
import {getData} from 'services/api';

export default function ListDemo({requestId}){
  // model
  const [data,setData] = useState([]);
  const [visible,setVisible] = useState(false);

  useEffect(()=>{
    // services 层
    getData().then(data=>{
      setData(data)
    });
    /**
     * 当requestId变化时,列表会重新请求
     * 这里的requestId是组件向外界暴露的一个api
     **/
  },[requestId])

  useEffect(()=>{
    if(visible===true){
      // clearState
      setVisible(false);
    }
  },[requestId])

  return (
    // view
    <div>
      {
        data.map(item=><li>{item}</li>
      }
      {
        visible && (
          <div>
            this is a modal
          </div>
        )
      }
    </div>
    )
  )
}

// app.jsx
<ListDemo />

这种组件设计的特点是,组件可以重置自身状态的时机是由自身控制的。如果你觉得这样麻烦,你可以把重置自身状态的时机交给外部,通过key来 “销毁组件”=>“重新渲染组件”。上面的代码可以简化成:

// 方案二
// ListDemo.jsx
import React,{useEffect} from 'react';
import {getData} from 'services/api';

export default function ListDemo({requestId}){
  // model
  const [data,setData] = useState([]);
  const [visible,setVisible] = useState(false);

  useEffect(()=>{
    // services 层
    getData().then(data=>{
      setData(data)
    });
  },[])

  return (
    // view
    <div>
      {
        data.map(item=><li>{item}</li>
      }
      {
        visible && (
          <div>
            this is a modal
          </div>
        )
      }
    </div>
    )
  )
}

// app.jsx
/*
*当requestId变化时,ListDemo会重新渲染
*/
<ListDemo key={requestId} />

方案二的代码比较整洁,且出错率比方案一低,但是方案二存在重新渲染组件的一个环节,性能开支会比方案一多一点点(大部分情况你都可以忽略不计)。很多情况下,我都会采用方案二。