一、相关概念

1、容器组件和展示组件相分离

展示组件:就是仅仅render html的component,不与store直接发生关系,而是要通过容器组件与store进行交互,以便改变state。

示例:

const Tabs = (props) => (
  <div className='ui top attached tabular menu'>
    {
      props.tabs.map((tab, index) => (
        <div
          key={index}
          className={tab.active ? 'active item' : 'item'}
          onClick={() => props.onClick(tab.id)}
        >
          {tab.title}
        </div>
      ))
    }
  </div>
);

  仅仅用于展示,展示所需的数据tabs和相应的action都是通过props接受容器组件传递的内容

容器组件示例:

class ThreadTabs extends React.Component {
  render() {
    return (
      <Tabs
        tabs={this.props.tabs}
        onClick={(id) => (
          store.dispatch({
            type: 'OPEN_THREAD',
            id: id,
          })
        )}
      />
    );
  }
}

  容器组件负责向展示组件传递所需的数据(tabs)和展示组件的相应动作(onClick),他不返回任何的html标签,仅仅是用来做数据的传递。容器组件要用class来声明,不能用stateless组件。

原来的未拆分前的Threadtabs组件是将展示html元素、数据交互和动作处理集中在一起,如下:

class ThreadTabs extends React.Component {
  handleClick = (id) => {
    store.dispatch({
      type: 'OPEN_THREAD',
      id: id,
    });
  };

  render() {
    const tabs = this.props.tabs.map((tab, index) => (
      <div
        key={index}
        className={tab.active ? 'active item' : 'item'}
        onClick={() => this.handleClick(tab.id)}
      >
        {tab.title}
      </div>
    ));
    return (
      <div className='ui top attached tabular menu'>
        {tabs}
      </div>
    );
  }
}

  拆分之后,可以使得tabs得到最大程度的复用,任何需要展示tabs的地方,无论其背后的数据和逻辑是什么样的,都可以使用tabs组件。

2、容器组件负责与store进行交互,包括接受数据(getState)和传递action(dispatch)

上面的ThreadTabs直接dispatch(action)给到store,但是接受数据的话还是通过this.props.tabs间接的通过App从store中接受数据。

ThreadTabs完全可以自己直接从store接受数据,没必要经过一道store

因此,我们将ThreadTabs修改如下:

第一步:向store中添加listener,当store中的state发生变化时,调用强制更新,保持ThreadTabs中的state数据与store中保持一致。

class ThreadTabs extends React.Component {
  componentDidMount() {
    store.subscribe(() => this.forceUpdate());
  }

  

第二步:直接从store中获取数据。

render() {
    const state = store.getState();

    const tabs = state.threads.map(t => (
      {
        title: t.title,
        active: t.id === state.activeThreadId,
        id: t.id,
      }
    ));

    return (
      <Tabs
        tabs={tabs}
        onClick={(id) => (
          store.dispatch({
            type: 'OPEN_THREAD',
            id: id,
          })
        )}
      />
    );
  }
}

  这样我们便不需要通过App中转数据了。

除了可以服用展示组件外,我们还有一个好处,那就是我们将所有与redux和store相关的操作都放在了容器组件中,有一天如果我们不用redux了,更换另外一个state management,我们可以把容器组件全部删掉,所有的展示组件是可以全部留下复用的,降低了转换成本。

现在我们已经完整的分拆了ThradTabs组件为容器组件和展示组件,下面我们将进一步的拆分Thread组件。

Thread组件包括两个部分,messageList和TextInputFiled组件两个部分。现在的Thread组件如下:THread既包含html展示标签,又负责动作相应,还与向store进行传递动作。

class MessageInput extends React.Component {
  state = {
    value: '',
  };

  onChange = (e) => {
    this.setState({
      value: e.target.value,
    })
  };

  handleSubmit = () => {
    store.dispatch({
      type: 'ADD_MESSAGE',
      text: this.state.value,
      threadId: this.props.threadId,
    });
    this.setState({
      value: '',
    });
  };

  render() {
    return (
      <div className='ui input'>
        <input
          onChange={this.onChange}
          value={this.state.value}
          type='text'
        />
        <button
          onClick={this.handleSubmit}
          className='ui primary button'
          type='submit'
        >
          Submit
        </button>
      </div>
    );
  }
}

class Thread extends React.Component {
  handleClick = (id) => {
    store.dispatch({
      type: 'DELETE_MESSAGE',
      id: id,
    });
  };

  render() {
    const messages = this.props.thread.messages.map((message, index) => (
      <div
        className='comment'
        key={index}
        onClick={() => this.handleClick(message.id)}
      >
        <div className='text'>
          {message.text}
          <span className='metadata'>@{message.timestamp}</span>
        </div>
      </div>
    ));
    return (
      <div className='ui center aligned basic segment'>
        <div className='ui comments'>
          {messages}
        </div>
        <MessageInput threadId={this.props.thread.id} />
      </div>
    );
  }
}

  我们可以将展示组件分离出来分别是展示一组messagesList的组件,仅仅返回一组messages,还有一组仅仅返回一个input框和submit按钮的组件,命名为TextInputField;两个组件都不再传递动作给store.thread仅仅展示着两个组件,Threaddisply负责从store中接受数据,并传递给Thread组件,并负责处理想store传递action

class TextFieldSubmit extends React.Component {
  state = {
    value: '',
  };

  onChange = (e) => {
    this.setState({
      value: e.target.value,
    })
  };

  handleSubmit = () => {
    this.props.onSubmit(this.state.value);
    this.setState({
      value: '',
    });
  };

  render() {
    return (
      <div className='ui input'>
        <input
          onChange={this.onChange}
          value={this.state.value}
          type='text'
        />
        <button
          onClick={this.handleSubmit}
          className='ui primary button'
          type='submit'
        >
          Submit
        </button>
      </div>
    )
  }
}

const MessageList = (props) => (
  <div className='ui comments'>
    {
      props.messages.map((m, index) => (
        <div
          className='comment'
          key={index}
          onClick={() => props.onClick(m.id)}
        >
          <div className='text'>
            {m.text}
            <span className='metadata'>@{m.timestamp}</span>
          </div>
        </div>
      ))
    }
  </div>
);

const Thread = (props) => (
  <div className='ui center aligned basic segment'>
    <MessageList
      messages={props.thread.messages}
      onClick={props.onMessageClick}
    />
    <TextFieldSubmit
      onSubmit={props.onMessageSubmit}
    />
  </div>
);

class ThreadDisplay extends React.Component {
  componentDidMount() {
    store.subscribe(() => this.forceUpdate());
  }

  render() {
    const state = store.getState();
    const activeThreadId = state.activeThreadId;
    const activeThread = state.threads.find(
      t => t.id === activeThreadId
    );

    return (
      <Thread
        thread={activeThread}
        onMessageClick={(id) => (
          store.dispatch({
            type: 'DELETE_MESSAGE',
            id: id,
          })
        )}
        onMessageSubmit={(text) => (
          store.dispatch({
            type: 'ADD_MESSAGE',
            text: text,
            threadId: activeThreadId,
          })
        )}
      />
    );
  }
}

 3、通用的容器组件

容器组件的特征:

(1)在componentDIdMount中绑定传递监听事件到store中,以便获取与store同步的state数据;

(2)从stroe中获取数据后通过props传递给展示组件

(3)响应展示组件的动作,向store传递action

就是一个数据中转站,stateTOprops和eventTOdispacth(action)

我们可以使用react-reudx来简化容器组件的撰写工作。

第一步:为所有的容器组件提供一个store

现在容器组件之所以能够使用store是因为我们在文件中定义了一个const store=createStroe(reducer)这样一个变量,但是我们不能将所有的容器组件写在一个文件下,如果项目大的话。我们应该通过组件传递给下级组件,而不是在文件中声明一个变量,store只有一个,我们可以在组件的最顶层使用上下文将App包裹起来,然后通过Provider组件,将store传递给App,这样,所有的App下的组件无论是否在一个文件中都可以获取store.

对App增加一层包裹,如下所示:

import { Provider } from 'react-redux';
const store = createStore(reducer);
const WrappedApp = () => (
  <Provider store={store}>
    <App />
  </Provider>
);
export default WrappedApp;

 

第二步:现在我们可以通过react-redux中为我们提供的connect()方法来构造容器组件了

首先:重写ThreadTabs组件

我们需要传递两个参数给connect(),第一个是stateTOprops,第二个是dispatch(action)TOprops

第一个:mapStateToTabProps,返回一个对象,{tabs:tabs}可以简写为{tabs}

const mapStateToTabsProps = (state) => {
  const tabs = state.threads.map(t => (
    {
      title: t.title,
      active: t.id === state.activeThreadId,
      id: t.id,
    }
  ));

  return {
    tabs,
  };
};

  

第二个:dispatch to props

接受dispatch返回一个一个对象,onClick对应了Tabs的onClick事件的处理函数。

const mapDispatchToTabsProps = (dispatch) => (
  {
    onClick: (id) => (
      dispatch({
        type: 'OPEN_THREAD',
        id: id,
      })
    ),
  }
);

  

第三:生成新的ThreadTabs,将上面两个参数传递进去,

const ThreadTabs = connect(
  mapStateToTabsProps,
  mapDispatchToTabsProps
)(Tabs);

  connect()返回一个函数,可以将要与store进行连接的展示组件(tabs)传递进去,确保第一个参数返回的tabs与Tabs中的props.tabs一致,第二个参数返回的onClick与Tabs中的props.onClick一致。这样的话就会返回已个react容器组件。

现在用connect()生成一下ThreadDisplay组件

const mapStateToThreadProps = (state) => (
  {
    thread: state.threads.find(
      t => t.id === state.activeThreadId
    ),
  }
);

const mapDispatchToThreadProps = (dispatch) => (
  {
    onMessageClick: (id) => (
      dispatch({
        type: 'DELETE_MESSAGE',
        id: id,
      })
    ),
    dispatch: dispatch,
  }
);

const mergeThreadProps = (stateProps, dispatchProps) => (
  {
    ...stateProps,
    ...dispatchProps,
    onMessageSubmit: (text) => (
      dispatchProps.dispatch({
        type: 'ADD_MESSAGE',
        text: text,
        threadId: stateProps.thread.id,
      })
    ),
  }
);

const ThreadDisplay = connect(
  mapStateToThreadProps,
  mapDispatchToThreadProps,
  mergeThreadProps
)(Thread);

  由于mapDispatchToThreadProps仅仅接受dispatch作为参数,如果想引用state的话,name就需要引用connect()的第三个参数 mergeThreadProps,mergeThreadProp接受mapStateToThreadProps的结果作为第一个参数,接受mapDispatchToThreadProps的结果作为第二个参数,然后返回的结果会作为connect的参数。