1. 引言
React PowerPlug 是利用 render props 進行更好狀態管理的工具庫。
React 專案中,一般一個檔案就是一個類,狀態最細粒度就是檔案的粒度。然而檔案粒度並非狀態管理最合適的粒度,所以有了 Redux 之類的全域性狀態庫。
同樣,檔案粒度也並非狀態管理的最細粒度,更細的粒度或許更合適,因此有了 React PowerPlug。
比如你會在專案中看到這種眼花繚亂的 state
:
class App extends React.PureComponent {
state = {
name = 1
isLoading = false
isFetchUser = false
data = {}
disableInput = false
validate = false
monacoInputValue = ''
value = ''
}
render () { /**/ }
}
其實真正 App
級別的狀態並沒有那麼多,很多 諸如受控元件 onChange
臨時儲存的無意義 Value 找不到合適的地方儲存。
這時候可以用 Value
管理區域性狀態:
<Value initial="React">
{({ value, set, reset }) => (
<>
<Select
label="Choose one"
options={["React", "Preact", "Vue"]}
value={value}
onChange={set}
/>
<Button onClick={reset}>Reset to initial</Button>
</>
)}
</Value>
可以看到,這個問題本質上應該拆成新的 React 類解決,但這也許會導致專案結構更混亂,因此 RenderProps 還是必不可少的。
今天我們就來解讀一下 React PowerPlug 的原始碼。
2. 精讀
2.1. Value
這是一個值操作的工具,功能與 Hooks 中 useState
類似,不過多了一個 reset
功能(Hooks 其實也未嘗不能有,但 Hooks 確實沒有 Reset)。
用法
<Value initial="React">
{({ value, set, reset }) => (
<>
<Select
label="Choose one"
options={["React", "Preact", "Vue"]}
value={value}
onChange={set}
/>
<Button onClick={reset}>Reset to initial</Button>
</>
)}
</Value>
原始碼
- 原始碼地址
- 原料:無
State 只儲存一個屬性 value
,並賦初始值為 initial
:
export default {
state = {
value: this.props.initial
};
}
方法有 set
reset
。
set
回撥函式觸發後呼叫 setState
更新 value
。
reset
就是呼叫 set
並傳入 this.props.initial
即可。
2.2. Toggle
Toggle 是最直接利用 Value 即可實現的功能,因此放在 Value 之後說。Toggle 值是 boolean 型別,特別適合配合 Switch 等元件。
既然 Toggle 功能弱於 Value,為什麼不用 Value 替代 Toggle 呢?這是個好問題,如果你不擔心自己程式碼可讀性的話,的確可以永遠不用 Toggle。
用法
<Toggle initial={false}>
{({ on, toggle }) => <Checkbox onClick={toggle} checked={on} />}
</Toggle>
原始碼
- 原始碼地址
- 原料:Value
核心就是利用 Value 元件,value
重新命名為 on
,增加了 toggle
方法,繼承 set
reset
方法:
export default {
toggle: () => set(on => !on);
}
理所因當,將 value 值限定在 boolean 範圍內。
2.3. Counter
與 Toggle 類似,這也是繼承了 Value 就可以實現的功能,計數器。
用法
<Counter initial={0}>
{({ count, inc, dec }) => (
<CartItem
productName="Lorem ipsum"
unitPrice={19.9}
count={count}
onAdd={inc}
onRemove={dec}
/>
)}
</Counter>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 count
,增加了 inc
dec
incBy
decBy
方法,繼承 set
reset
方法。
與 Toggle 類似,Counter 將 value 限定在了數字,那麼比如 inc
就會這麼實現:
export default {
inc: () => set(value => value + 1);
}
這裡用到了 Value 元件 set
函式的多型用法。一般 set 的引數是一個值,但也可以是一個函式,回撥是當前的值,這裡返回一個 +1 的新值。
2.4. List
運算元組。
用法
<List initial={['#react', '#babel']}>
{({ list, pull, push }) => (
<div>
<FormInput onSubmit={push} />
{list.map({ tag }) => (
<Tag onRemove={() => pull(value => value === tag)}>
{tag}
</Tag>
)}
</div>
)}
</List>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 list
,增加了 first
last
push
pull
sort
方法,繼承 set
reset
方法。
export default {
list: value,
first: () => value[0],
last: () => value[Math.max(0, value.length - 1)],
set: list => set(list),
push: (...values) => set(list => [...list, ...values]),
pull: predicate => set(list => list.filter(complement(predicate))),
sort: compareFn => set(list => [...list].sort(compareFn)),
reset
};
為了利用 React Immutable 更新的特性,因此將 sort
函式由 Mutable 修正為 Immutable,push
pull
同理。
2.5. Set
儲存陣列物件,可以新增和刪除元素。類似 ES6 Set。和 List 相比少了許多功能函式,因此只承擔新增、刪除元素的簡單功能。
用法
需要注意的是,initial
是陣列,而不是 Set 物件。
<Set initial={["react", "babel"]}>
{({ values, remove, add }) => (
<TagManager>
<FormInput onSubmit={add} />
{values.map(tag => (
<Tag onRemove={() => remove(tag)}>{tag}</Tag>
))}
</TagManager>
)}
</Set>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 values
且初始值為 []
,增加了 add
remove
clear
has
方法,保留 reset
方法。
實現依然很簡單,add
remove
clear
都利用 Value 提供的 set
進行賦值,只要實現幾個運算元組方法即可:
const unique = arr => arr.filter((d, i) => arr.indexOf(d) === i);
const hasItem = (arr, item) => arr.indexOf(item) !== -1;
const removeItem = (arr, item) =>
hasItem(arr, item) ? arr.filter(d => d !== item) : arr;
const addUnique = (arr, item) => (hasItem(arr, item) ? arr : [...arr, item]);
has
方法則直接複用 hasItem
。核心還是利用 Value 的 set
函式一招通吃,將操作目標鎖定為陣列型別罷了。
2.6. map
Map 的實現與 Set 很像,類似 ES6 的 Map。
用法
與 Set 不同,Map 允許設定 Key 名。需要注意的是,initial
是物件,而不是 Map 物件。
<Map initial={{ sounds: true, music: true, graphics: "medium" }}>
{({ set, get }) => (
<Tings>
<ToggleCheck checked={get("sounds")} onChange={c => set("sounds", c)}>
Game Sounds
</ToggleCheck>
<ToggleCheck checked={get("music")} onChange={c => set("music", c)}>
Bg Music
</ToggleCheck>
<Select
label="Graphics"
options={["low", "medium", "high"]}
selected={get("graphics")}
onSelect={value => set("graphics", value)}
/>
</Tings>
)}
</Map>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 values
且初始值為 {}
,增加了 set
get
clear
has
delete
方法,保留 reset
方法。
由於使用物件儲存資料結構,操作起來比陣列方便太多,已經不需要再解釋了。
值得吐槽的是,作者使用了 !=
判斷 has:
export default {
has: key => values[key] != null;
}
這種程式碼並不值得提倡,首先是不應該使用二元運算子,其次比較推薦寫成 values[key] !== undefined
,畢竟 set('null', null)
也應該算有值。
2.7. state
State 純粹為了替代 React setState
概念,其本質就是換了名字的 Value 元件。
用法
值得注意的是,setState
支援函式和值作為引數,是 Value 元件本身支援的,State 元件額外適配了 setState
的另一個特性:合併物件。
<State initial={{ loading: false, data: null }}>
{({ state, setState }) => {
const onStart = data => setState({ loading: true });
const onFinish = data => setState({ data, loading: false });
return (
<DataReceiver data={state.data} onStart={onStart} onFinish={onFinish} />
);
}}
</State>
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 state
且初始值為 {}
,增加了 setState
方法,保留 reset
方法。
setState
實現了合併物件的功能,也就是傳入一個物件,並不會覆蓋原始值,而是與原始值做 Merge:
export default {
setState: (updater, cb) =>
set(
prev => ({
...prev,
...(typeof updater === "function" ? updater(prev) : updater)
}),
cb
);
}
2.8. Active
這是一個內建滑鼠互動監聽的容器,監聽了 onMouseUp
與 onMouseDown
,並依此判斷 active
狀態。
用法
<Active>
{({ active, bind }) => (
<div {...bind}>
You are {active ? "clicking" : "not clicking"} this div.
</div>
)}
</Active>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 active
且初始值為 false
,增加了 bind
方法。
bind
方法也巧妙利用了 Value 提供的 set
更新狀態:
export default {
bind: {
onMouseDown: () => set(true),
onMouseUp: () => set(false)
}
};
2.9. Focus
與 Active 類似,Focus 是當 focus 時才觸發狀態變化。
用法
<Focus>
{({ focused, bind }) => (
<div>
<input {...bind} placeholder="Focus me" />
<div>You are {focused ? "focusing" : "not focusing"} the input.</div>
</div>
)}
</Focus>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 focused
且初始值為 false
,增加了 bind
方法。
bind
方法與 Active 如出一轍,僅是監聽時機變成了 onFocus
和 onBlur
。
2.10. FocusManager
不知道出於什麼考慮,FocusManager 的官方文件是空的,而且 Help wanted。。
正如名字描述的,這是一個 Focus 控制器,你可以直接呼叫 blur
來取消焦點。
用法
筆者給了一個例子,在 5 秒後自動失去焦點:
<FocusFocusManager>
{({ focused, blur, bind }) => (
<div>
<input
{...bind}
placeholder="Focus me"
onClick={() => {
setTimeout(() => {
blur();
}, 5000);
}}
/>
<div>You are {focused ? "focusing" : "not focusing"} the input.</div>
</div>
)}
</FocusFocusManager>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 focused
且初始值為 false
,增加了 bind
blur
方法。
blur
方法直接呼叫 document.activeElement.blur()
來觸發其 bind
監聽的 onBlur
達到更新狀態的效果。
By the way, 還監聽了 onMouseDown
與 onMouseUp
:
export default {
bind: {
tabIndex: -1,
onBlur: () => {
if (canBlur) {
set(false);
}
},
onFocus: () => set(true),
onMouseDown: () => (canBlur = false),
onMouseUp: () => (canBlur = true)
}
};
可能意圖是防止在 mouseDown
時觸發 blur
,因為 focus
的時機一般是 mouseDown
。
2.11. Hover
與 Focus 類似,只是觸發時機為 Hover。
用法
<Hover>
{({ hovered, bind }) => (
<div {...bind}>
You are {hovered ? "hovering" : "not hovering"} this div.
</div>
)}
</Hover>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 hovered
且初始值為 false
,增加了 bind
方法。
bind
方法與 Active、Focus 如出一轍,僅是監聽時機變成了 onMouseEnter
和 onMouseLeave
。
2.12. Touch
與 Hover 類似,只是觸發時機為 Hover。
用法
<Touch>
{({ touched, bind }) => (
<div {...bind}>
You are {touched ? "touching" : "not touching"} this div.
</div>
)}
</Touch>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 touched
且初始值為 false
,增加了 bind
方法。
bind
方法與 Active、Focus、Hover 如出一轍,僅是監聽時機變成了 onTouchStart
和 onTouchEnd
。
2.13. Field
與 Value 元件唯一的區別,就是
用法
這個用法和 Value 沒區別:
<Field>
{({ value, set }) => (
<ControlledField value={value} onChange={e => set(e.target.value)} />
)}
</Field>
但是用 bind
更簡單:
<Field initial="hello world">
{({ bind }) => <ControlledField {...bind} />}
</Field>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
保留不變,初始值為 ''
,增加了 bind
方法,保留 set
reset
方法。
與 Value 的唯一區別是,支援了 bind
並封裝 onChange
監聽,與賦值受控屬性 value
。
export default {
bind: {
value,
onChange: event => {
if (isObject(event) && isObject(event.target)) {
set(event.target.value);
} else {
set(event);
}
}
}
};
2.14. Form
這是一個表單工具,有點類似 Antd 的 Form 元件。
用法
<Form initial={{ firstName: "", lastName: "" }}>
{({ field, values }) => (
<form
onSubmit={e => {
e.preventDefault();
console.log("Form Submission Data:", values);
}}
>
<input
type="text"
placeholder="Your First Name"
{...field("firstName").bind}
/>
<input
type="text"
placeholder="Your Last Name"
{...field("lastName").bind}
/>
<input type="submit" value="All Done!" />
</form>
)}
</Form>
原始碼
- 原始碼地址
- 原料:Value
依然利用 Value 元件,value
重新命名為 values
且初始值為 {}
,增加了 setValues
field
方法,保留 reset
方法。
表單最重要的就是 field
函式,為表單的每一個控制元件做繫結,同時設定一個表單唯一 key
:
export default {
field: id => {
const value = values[id];
const setValue = updater =>
typeof updater === "function"
? set(prev => ({ ...prev, [id]: updater(prev[id]) }))
: set({ ...values, [id]: updater });
return {
value,
set: setValue,
bind: {
value,
onChange: event => {
if (isObject(event) && isObject(event.target)) {
setValue(event.target.value);
} else {
setValue(event);
}
}
}
};
}
};
可以看到,為表單的每一項繫結的內容與 Field 元件一樣,只是 Form 元件的行為是批量的。
2.15. Interval
Interval 比較有意思,將定時器以 JSX 方式提供出來,並且提供了 stop
resume
方法。
用法
<Interval delay={1000}>
{({ start, stop }) => (
<>
<div>The time is now {new Date().toLocaleTimeString()}</div>
<button onClick={() => stop()}>Stop interval</button>
<button onClick={() => start()}>Start interval</button>
</>
)}
</Interval>
原始碼
- 原始碼地址
- 原料:無
提供了 start
stop
toggle
方法。
實現方式是,在元件內部維護一個 Interval 定時器,實現了元件更新、銷燬時的計時器更新、銷燬操作,可以認為這種定時器的生命週期繫結了 React 元件的生命週期,不用擔心銷燬和更新的問題。
具體邏輯就不列舉了,利用 setInterval
clearInterval
函式基本上就可以了。
2.16. Compose
Compose 也是個有趣的元件,可以將上面提到的任意多個元件組合使用。
用法
<Compose components={[Counter, Toggle]}>
{(counter, toggle) => (
<ProductCard
{...productInfo}
favorite={toggle.on}
onFavorite={toggle.toggle}
count={counter.count}
onAdd={counter.inc}
onRemove={counter.dec}
/>
)}
</Compose>
原始碼
- 原始碼地址
- 原料:無
通過遞迴渲染出巢狀結構,並將每一層結構輸出的值儲存到 propsList
中,最後一起傳遞給元件。這也是為什麼每個函式 value
一般都要重新命名的原因。
在 精讀《Epitath 原始碼 - renderProps 新用法》 文章中,筆者就介紹了利用 generator
解決高階元件巢狀的問題。
在 精讀《React Hooks》 文章中,介紹了 React Hooks 已經實現了這個特性。
所以當你瞭解了這三種 "compose" 方法後,就可以在合適的場景使用合適的 compose 方式簡化程式碼。
3. 總結
看完了原始碼分析,不知道你是更感興趣使用這個庫呢,還是已經躍躍欲試開始造輪子了呢?不論如何,這個庫的思想在日常的業務開發中都應該大量實踐。
另外 Hooks 版的 PowerPlug 已經 4 個月沒有更新了(非官方):react-powerhooks,也許下一個維護者/貢獻者 就是你。
如果你想參與討論,請點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。