我目前正在开展一个ReactJS项目,我需要创建"可重复使用"某些方法需要覆盖的组件"重写"。在OOP中我会使用多态。我已经做了一些阅读,似乎共识是使用HoC /组合,但我无法弄清楚如何实现这一目标。我想如果我可以使用合成得到一个ES6样本,那么之后可能更容易将这个想法改编为ReactJS。
以下是我想在ReactJS中实现的ES6 OOP示例(忽略处理它仅用于测试的事件)。有没有人对如何将ReactJS组件分解为HoC有一些指导,或者甚至只是演示如何根据示例在ES6中使用组合?
class TransferComponent {
constructor(){
let timeout = null;
this.render();
this.events();
}
events(){
let scope = this;
document.getElementById('button').addEventListener('click', function(){
scope.validate.apply(scope);
});
}
validate(){
if(this.isValid()){
this.ajax();
}
}
isValid(){
if(document.getElementById('username').value !== ''){
return true;
}
return false;
}
ajax(){
clearTimeout(this.timeout);
document.getElementById('message').textContent = 'Loading...';
this.timeout = setTimeout(function(){
document.getElementById('message').textContent = 'Success';
}, 500);
}
render(){
document.getElementById('content').innerHTML = '<input type="text" id="username" value="username"/>\n\
<button id="button" type="button">Validate</button>';
}
}
class OverrideTransferComponent extends TransferComponent{
isValid(){
if(document.getElementById('username').value !== '' && document.getElementById('password').value !== ''){
return true;
}
return false;
}
render(){
document.getElementById('content').innerHTML = '<input type="text" id="username" value="username"/>\n\
<input type="text" id="password" value="password"/>\n\
<button id="button" type="button">Validate</button>';
}
}
const overrideTransferComponent = new OverrideTransferComponent();
&#13;
<div id="content"></div>
<div id="message"></div>
&#13;
更新 即使我最初的问题是关于FP我认为渲染道具是我的问题的一个非常好的解决方案,并避免了HoC问题。
答案 0 :(得分:2)
关于您的示例代码的答案位于本文的中间/底部。
关于React组合的一个好方法是render-callback pattern,也就是函数as-child。它对HOC的主要优势在于它允许您在运行时动态组合组件(例如在渲染中),而不是在作者时间静态组合。
无论您使用渲染回调还是HOC,组件组合的目标都是将可重用行为委托给其他组件,然后将这些组件作为props传递给需要它们的组件。
以下Delegator
组件使用render-callback模式将实现逻辑委托给作为prop传递的ImplementationComponent
:
const App = () => <Delegator ImplementationComponent={ImplementationB} />;
class Delegator extends React.Component {
render() {
const { ImplementationComponent } = this.props;
return (
<div>
<ImplementationComponent>
{ ({ doLogic }) => {
/* ... do/render things based on doLogic ... */
} }
</ImplementationComponent>
</div>
);
}
}
各种实现组件如下所示:
class ImplementationA extends React.Component {
doSomeLogic() { /* ... variation A ... */ }
render() {
this.props.children({ doLogic: this.doSomeLogic })
}
}
class ImplementationB extends React.Component {
doSomeLogic() { /* ... variation B ... */ }
render() {
this.props.children({ doLogic: this.doSomeLogic })
}
}
稍后,您可以按照相同的组合模式在Delegator
组件中嵌套更多子组件:
class Delegator extends React.Component {
render() {
const { ImplementationComponent, AnotherImplementation, SomethingElse } = this.props;
return (
<div>
<ImplementationComponent>
{ ({ doLogic }) => { /* ... */} }
</ImplementationComponent>
<AnotherImplementation>
{ ({ doThings, moreThings }) => { /* ... */} }
</AnotherImplementation>
<SomethingElse>
{ ({ foo, bar }) => { /* ... */} }
</SomethingElse>
</div>
);
}
}
现在嵌套的子组件允许多个具体实现:
const App = () => (
<div>
<Delegator
ImplementationComponent={ImplementationB}
AnotherImplementation={AnotherImplementation1}
SomethingElse={SomethingVariationY}
/>
<Delegator
ImplementationComponent={ImplementationC}
AnotherImplementation={AnotherImplementation2}
SomethingElse={SomethingVariationZ}
/>
</div>
);
将上述组合模式应用于您的示例,该解决方案重新构建您的代码,但假定它需要执行以下操作:
首先,为了简化操作,我将DOM更改为:
<div id="content-inputs"></div>
<div id="content-button"></div>
现在,TransferComponent
只知道如何显示按钮,并在按下按钮并且数据有效时执行某些操作。它不知道要显示什么输入或如何验证数据。它将该逻辑委托给嵌套的VaryingComponent
。
export default class TransferComponent extends React.Component {
constructor() {
super();
this.displayDOMButton = this.displayDOMButton.bind(this);
this.onButtonPress = this.onButtonPress.bind(this);
}
ajax(){
console.log('doing some ajax')
}
onButtonPress({ isValid }) {
if (isValid()) {
this.ajax();
}
}
displayDOMButton({ isValid }) {
document.getElementById('content-button').innerHTML = (
'<button id="button" type="button">Validate</button>'
);
document.getElementById('button')
.addEventListener('click', () => this.onButtonPress({ isValid }));
}
render() {
const { VaryingComponent } = this.props;
const { displayDOMButton } = this;
return (
<div>
<VaryingComponent>
{({ isValid, displayDOMInputs }) => {
displayDOMInputs();
displayDOMButton({ isValid });
return null;
}}
</VaryingComponent>
</div>
)
}
};
现在我们创建VaryingComponent
的具体实现来充实各种输入显示和验证逻辑。
仅限用户名的实施:
export default class UsernameComponent extends React.Component {
isValid(){
return document.getElementById('username').value !== '';
}
displayDOMInputs() {
document.getElementById('content-inputs').innerHTML = (
'<input type="text" id="username" value="username"/>'
);
}
render() {
const { isValid, displayDOMInputs } = this;
return this.props.children({ isValid, displayDOMInputs });
}
}
用户名和密码实现:
export default class UsernamePasswordComponent extends React.Component {
isValid(){
return (
document.getElementById('username').value !== '' &&
document.getElementById('password').value !== ''
);
}
displayDOMInputs() {
document.getElementById('content-inputs').innerHTML = (
'<input type="text" id="username" value="username"/>\n\
<input type="text" id="password" value="password"/>\n'
);
}
render() {
const { isValid, displayDOMInputs } = this;
return this.props.children({ isValid, displayDOMInputs });
}
}
最后,撰写TansferComponent
的实例看起来像:
<TransferComponent VaryingComponent={UsernameComponent} />
<TransferComponent VaryingComponent={UsernamePasswordComponent} />
答案 1 :(得分:0)
阅读你的问题,不清楚你是指组成还是继承,但它们是不同的OOP概念。如果您不知道它们之间的区别,我建议您查看this article。
关于React的具体问题。我建议你尝试用户组合,因为它为你提供了很大的灵活性来构建你的UI并传递道具。
例如,如果您正在使用React,则可能在动态填充对话框时已经在使用合成。正如React docs所示:
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
Facebook上的人们一直在开发非常具有挑战性的用户界面,并使用React构建了数千个组件,并且没有找到一个很好的继承优于组合的用例。正如文档所说:
React有一个强大的组合模型,我们建议使用组合而不是继承来重用组件之间的代码。
如果您确实想要使用继承,他们的建议是您将要在组件上重用的功能提取到单独的JavaScript模块中。组件可以导入它并使用该函数,对象或类,而无需扩展它。
在您提供的示例中,utils.js
中的两个函数可以正常运行。参见:
isUsernameValid = (username) => username !== '';
isPasswordValid = (password) => password !== '';
您可以导入它们并在组件中使用就好了。
答案 2 :(得分:0)
首先,在功能编程中,功能是一等公民。这意味着您可以像处理OOP中的数据一样处理函数(即作为参数传递,分配给变量等)。
您的示例在对象中使用行为来发送数据。为了编写纯粹的功能性解决方案,we'll want to separate these。
功能编程从根本上讲是将数据与行为分开。
所以,让我们从isValid
开始。
有几种方法可以在这里订购逻辑,但我们会这样做:
在JS中,转换为:
const areAllElementsValid = (...ids) => !ids.some(isElementInvalid)
我们需要一些辅助函数来完成这项工作:
const isElementInvalid = (id) => getValueByElementId(id) === ''
const getValueByElementId = (id) => document.getElementById(id).value
我们可以在一行上写下所有内容,但是分解它会使它更具可读性。有了它,我们现在有了一个通用函数,我们可以用它来确定组件的isValid
!
areAllElementsValid('username') // TransferComponent.isValid
areAllElementsValid('username', 'password') // TransferOverrideComponent.isValid
我使用isValid
在document
上欺骗了一点。在真正的函数式编程中,函数应该是纯。或者,换句话说,函数调用的结果必须只能从其输入中确定(a.k.a.它是幂等的)并且它不能有side effects。
那么,我们如何在没有副作用的情况下渲染DOM?好吧,React使用一个虚拟DOM(一个花哨的数据结构,它存在于内存中并传递给函数并从函数返回以保持功能纯度),用于核心库。 React的副作用存在于react-dom
库中。
对于我们的情况,我们将使用超级简单的虚拟DOM(类型为string
)。
const USERNAME_INPUT = '<input type="text" id="username" value="username"/>'
const PASSWORD_INPUT = '<input type="text" id="password" value="password"/>'
const VALIDATE_BUTTON = '<button id="button" type="button">Validate</button>'
这些是我们的组件 - 使用React术语 - 我们可以将其组合到UI中:
USERNAME_INPUT + VALIDATE_BUTTON // TransferComponent.render
USERNAME_INPUT + PASSWORD_INPUT + VALIDATE_BUTTON // TransferOverrideComponent.render
这可能看起来过于简单化,根本不起作用。但+
运算符实际上是功能性的!想一想:
因此,render
现在正在运作!
不幸的是,我们无法执行ajax调用,改变DOM,设置事件侦听器或设置没有副作用的超时。我们可以为这些操作创建 monads 的复杂路径,但就我们的目的而言,只要说我们将继续使用非功能性方法就足够了。
这是使用常见的React模式重写您的示例。我正在使用controlled components表单输入。我们所讨论的大多数功能概念实际上都存在于React中,所以这是一个非常简单的实现,不会使用任何花哨的东西。
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
success: false
};
}
handleSubmit() {
if (this.props.isValid()) {
this.setState({
loading: true
});
setTimeout(
() => this.setState({
loading: false,
success: true
}),
500
);
}
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
{this.props.children}
<input type="submit" value="Submit" />
</form>
{ this.state.loading && 'Loading...' }
{ this.state.success && 'Success' }
</div>
);
}
}
使用state
可能看起来像副作用,不是吗?在某种程度上它是,但是挖掘React内部结构可能会揭示出比我们的单个组件更多的功能实现。
以下是您的示例的Form
。请注意,我们可以通过几种不同的方式处理提交。一种方法是将username
和password
作为道具传递给Form
(可能作为通用data
道具)。另一种选择是传递特定于该表单的handleSubmit
回调(就像我们为validate
所做的那样)。
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: ''
};
}
isValid() {
return this.state.username !== '' && this.state.password !== '';
}
handleUsernameChange(event) {
this.setState({ username: event.target.value });
}
handlePasswordChange(event) {
this.setState({ password: event.target.value });
}
render() {
return (
<Form
validate={this.isValid}
>
<input value={this.state.username} onChange={this.handleUsernameChange} />
<input value={this.state.password} onChange={this.handlePasswordChange} />
</Form>
);
}
}
你也可以写另一个Form
,但输入不同
class CommentForm extends React.Component {
constructor(props) {
super(props);
this.state = {
comment: ''
};
}
isValid() {
return this.state.comment !== '';
}
handleCommentChange(event) {
this.setState({ comment: event.target.value });
}
render() {
return (
<Form
validate={this.isValid}
>
<input value={this.state.comment} onChange={this.handleCommentChange} />
</Form>
);
}
}
例如,您的应用可以呈现Form
个实现:
class App extends React.Component {
render() {
return (
<div>
<LoginForm />
<CommentForm />
</div>
);
}
}
最后,我们使用ReactDOM
代替innerHTML
ReactDOM.render(
<App />,
document.getElementById('content')
);
通常使用JSX隐藏React的功能特性。我鼓励你阅读我们正在做的事情,实际上只是一堆功能组合在一起。 The official docs很好地涵盖了这一点。
为了进一步阅读,James K. Nelson在React上汇集了一些有助于您的功能理解的一流资源:https://reactarmory.com/guides/learn-react-by-itself/react-basics