一、相关概念
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的参数。