现在用Hooks管理React函数式组件中的状态已经很容易了。我以前曾写过“用自定义Hooks作为服务”和“自定义Hooks中的函数式编程”(译文)。在本文中,我会分享自己做的一个相当简单的重构,通过重构带来了一种更整洁、可重用且更简单的实现。
本文最初发布于Orizens博客,经原作者Oren Farhi授权,由InfoQ中文站翻译并分享。
我认为代码应该是自解释的,并且能轻松到处移动和重用。有时,一种比较简单的方法是从基础的方法入手,一旦看到重复出现的模式,就可以将其抽象化。
我认为正确应用的代码抽象可以让很多事情一目了然。但抽象太多可能适得其反——很难找出实现的脉络,或者我喜欢称之为"糟糕的诗"。
我为ReadM™创建了Speaker()组件,ReadM™是一款免费且易用的阅读Web应用,它可以激励孩子们通过实时反馈来练习、学习、阅读和讲出英语,并提供了很好的体验。
这一组件负责显示文本,且孩子读出句子或某个单词就可以用应用交互。为了改善用户体验,我决定在用户说话时添加单词高亮显示(很像卡拉OK)。
Speaker()组件会接收几个props来实现上述交互。
以下是所有props的简要介绍:
function Speaker({
text,
onSpeakComplete,
disable,
verified = [],
highlight = [],
speed,
}: SpeakerProps) {
// code
}
接下来(该函数的主体),将定义高亮显示被念到的单词的状态,以及用于设置该单词的函数handler。本节是很重要的一节,是本文要重点强调的内容。
const [highlightSpoken, setHighlightSpoken] = useState<{
word: string
index: number
}>()
const handleOnSpeak = useCallback(() => {
speak({
phrase: text,
speed,
onEndCallback: () => {
onSpeakComplete && onSpeakComplete(text)
setHighlightSpoken(null)
},
onSpeaking: setHighlightSpoken,
sanitize: false,
})
}, [text, onSpeakComplete, setHighlightSpoken, speed])
const handleOnSelectWord = (phrase: string) => {
speak({ phrase, speed, onEndCallback: noop })
}
现在代码从props中获取值以准备显示属性,这些属性将传递到返回渲染值内的表示组件中。
const words = verified.length ? verified : createVerifiedWords(text, highlight)
const rtlStyle = resolveLanguage(text).style
const justify = rtlStyle.length ? "end" : "between"
返回的渲染值是:
function Speaker(props) {
// all the above code commented
return (
<Column md="row" alignItems="center" justify={justify} className="speaker">
<Row
wrap={true}
className={`speaker-phrase bg-transparent m-0 ${rtlStyle}`}
>
{words.map((result, index) => (
<WordResult
key={`${text}-${index}`}
result={result}
disable={disable}
highlight={highlightSpoken && highlightSpoken.index === index}
onSelectWord={handleOnSelectWord}
/>
))}
</Row>
<ButtonIcon
data-testid="speaker"
icon="volume-up"
type="light"
size="4"
styles="mx-md-2"
disabled={disable}
onClick={handleOnSpeak}
/>
</Column>
)
}
虽然这个组件并不大,但它也能改得更有条理,更整洁一些。
Speaker的行为和功能代码部分可以重用,并整合到它自己的可操作单元中。请注意,“speak()”函数在两种上下文中使用了两次——也许这里可以DRY它一下,换一种方法。
我们可以创建一个新的可重用挂钩——useSpeaker()。我们需要让这个hook接收当前念到的单词(一个状态)和speak()功能。
然后我们才能抽象出整个行为的代码,并在Speaker的代码中使用这个好用的小代码段:
const { spokenWord, say } = useSpeaker({
text,
speed,
onEnd: onSpeakComplete,
})
useSpeaker()包括从Speaker组件中提取的代码。
import React from 'react';
import { speak } from '../utils/speaker.util';
type TextWord = {
word: string;
index: number;
};
export default function useSpeaker({ text, speed, onEnd }) {
const [spokenWord, setSpokenWord] = React.useState<TextWord>();
const say = React.useCallback(() => {
speak({
phrase: text,
speed,
onEndCallback: () => {
onEnd && onEnd(text);
setSpokenWord(null);
},
onSpeaking: setSpokenWord
sanitize: false,
});
}, [text, speed, onEnd]);
return { spokenWord, say };
}
现在有两个“speak()”函数调用。可以在WordResult组件内部重用新的useSpeaker() hook。 我们需要在WordResult中更改的是——传递speed属性,而不是传递onSelectWord()的函数handler。使用speed和result(包含“word”的对象)后,可以在WordResult内部重用useSpeaker的功能。
{
words.map((result, index) => (
<WordResult
key={`${text}-${index}`}
result={result}
disable={disable}
highlight={spokenWord && spokenWord.index === index}
speed={speed}
/>
))
}
使用上面的自定义hook——useSpeaker()后,我们成功将20行代码缩减为可重用的5行代码。最重要的是,这段代码现在有更多的语义含义,并且目标非常明确。
除了让代码实现"讲话"的功能外,useSpeaker()代码重构也体现了它的字面意思——只要使用正确的术语,代码就可能在你的脑海中发出声音。
我认为,编写好函数式代码后很快就应该考虑对其迭代。在阅读代码并尝试理解它时,你的脑海中可能会出现很多问题:
对于这些问题,我通常会添加一些目标,希望能带来更好的结果:
请查看我们的革命性应用ReadM™,这款程序能通过实时反馈树立儿童阅读和讲出英语的信心(更多语种正在开发中)。
我会基于ReadM™的开发经验,撰写更多有用的文章。
Oren Farhi是前端工程师和JS顾问。他的作品包括ReadM™、Echoes Player、ngx-infinite-scroll等。他撰写了《Angular和NgRx的响应式编程》一书。这里是他的开源项目列表。
https://orizens.com/blog/how-to-functional-programming-with-custom-react-hooks/
领取专属 10元无门槛券
私享最新 技术干货