React 應用有四種不同的狀態:
setState
持有並維護的狀態。這是最為常見的模式。其理念是僅有 containers(smart components)負責創建狀態,也僅有它們可以取用全局 store 的狀態。它們能透過 setState
創建其自己的狀態,並用 props
傳給組件樹。
當全局狀態需要變化,該變化會在 container 處激發(如 Redux 的 dispatch 一個 action)。
這個模式有三種類型的狀態:全局狀態、組件狀態及相對狀態。
優點在於職責定義清晰。我們知道全局狀態的變動只能發生在 container,而組件接收的狀態只能來自於 container。
缺點來自相對狀態。當傳遞多於兩層時,就會變得混亂。
與前一個模式不同之處在於,dumb components 現在不那麼 dumb 了。它們能自己訪問到全局狀態,而毋需 Container 傳遞。
但這帶來的壞處是模糊了職責的定義,現在 container 和 dumb components 皆能自行訪問全局狀態;dumb components 何時應接收相對狀態、何時應使用全局狀態亦不明確。
最重要的是,全局狀態並不遵循 React 組件的生命週期。如果使用相對狀態模式,當 container 卸載時,其狀態會重置,意味著從 container 接收數據的組件不再擁有那些 props。但是如果該組件是從全局拉取其狀態的,而當其卸載時,該狀態仍存在於全局。當它重新掛載時,將會收到如舊的數據。這通常引致 bug。組件現在與相關的全局狀態緊密耦合了,降低其復用性。
一個方法是在組件卸載時重置該部分全局狀態。但這也說明了如果需要這樣做,或許更宜使用組件內的狀態。
React v16.3 後正式支援 Context API。每個 context 都有一個 provider
和 consumer
。在上圖中,Containers 是 context 的 providers,而 Components 是 consumers。Containers 能從全局狀態中承繼 state,亦能自己創造 state 並「provide」予 context。然後 Components 能從 context 中挑揀所需的 props,而毋須依賴 container 或全局狀態。
這種分工類似於 pattern 1——containers 是唯一能操縱並使用全局狀態的組件,components 只從 context 處獲取 props 並展現之。
這種模式解決了之前的問題:
一個 provided pattern 例子:
// table.container.js
import React, { Component } from 'react'
import { Table, Body, Header, TableContext } from '../table.component'
export default class TableContainer extends Component {
state = {
activeRow: null,
data: [{
id: 1,
year: '1961',
make: 'Jaguar',
model: 'E-Type'
}, {
id: 2,
year: '1969',
make: 'Farrari',
model: '365 GT 2 + 2'
}],
columns: [{
name: 'id',
editable: false
}, {
name: 'year',
editable: true,
}, {
name: 'make',
editable: true
}, {
name: 'model',
editable: true
}]
}
render() {
const { data, columns, activeRow } = this.state
return (
<TableContext.Provider
value={{
data,
columns,
activeRow,
selectRow: id => this.setState({ activeRow: id }),
updateRow: (...args) => this.updateRow(...args)
}}>
<Table>
<Header />
<Body />
</Table>
{activeRow && (
<button onClick={() => this.setState({ activeRow: null })}>Done</button>
)}
</TableContext.Provider>
)
}
updateRow(rowId, field, value) {
const { data } = this.state
const index = data.findIndex(({ id }) => id === rowId)
data[index][field] = value
this.setState({ data })
}
}
// table.component.js
import React, { createContext } from 'react'
import { Input } from '../shared/inputs'
export const TableContext = createContext()
const Row = ({ row }) => (
<TableContext.Consumer>
{({ columns }) =>
columns.map((field, i) => <td key={i}>{row[field.name]}</td>)
}
</TableContext.Consumer>
)
const EditRow = ({ row }) => (
<TableContext.Consumer>
{({ columns, updateRow }) =>
columns.map((field, i) => (
<td key={i}>
{field.editable ? (
<input
value={row[field.name]}
onChange={e => updateRow(row.id, field.name, e.target.value)} />
) : (
row[field.name]
)}
</td>
))
}
</TableContext.Consumer>
)
export const Body = () => (
<TableContext.Consumer>
{({ columns, data, activeRow, selectRow }) => (
<tbody>
{data.map((row, i) => (
<tr key={row.id} onClick={() => selectRow(row.id)}>
{activeRow === row.id ? <EditRow row={row} /> : <Row row={row} />}
</tr>
))}
</tbody>
)}
</TableContext.Consumer>
)
export const Header = () => (
<TableContext.Consumer>
{({ columns }) => (
<thead>
<tr>{columns.map((field, i) => <th key={i}>{field.name}</th>)}</tr>
</thead>
)}
</TableContext.Consumer>
)
export const Table = ({ children }) => <table>{children}</table>
完 :)