FP替代JavaScript / ReactJS

时间:2017-11-11 00:07:49

标签: javascript reactjs ecmascript-6 functional-programming

我目前正在开展一个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;
&#13;
&#13;

更新 即使我最初的问题是关于FP我认为渲染道具是我的问题的一个非常好的解决方案,并避免了HoC问题。

3 个答案:

答案 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>
); 

答案(你的例子):

将上述组合模式应用于您的示例,该解决方案重新构建您的代码,但假定它需要执行以下操作:

  • 允许输入的变体及其验证逻辑
  • 当用户提交有效输入时,请执行一些ajax

首先,为了简化操作,我将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)

非React FP示例

首先,在功能编程中,功能是一等公民。这意味着您可以像处理OOP中的数据一样处理函数(即作为参数传递,分配给变量等)。

您的示例在对象中使用行为来发送数据。为了编写纯粹的功能性解决方案,we'll want to separate these

  

功能编程从根本上讲是将数据与行为分开。

所以,让我们从isValid开始。

功能isValid

有几种方法可以在这里订购逻辑,但我们会这样做:

  1. 给出一份ids列表
  2. 如果不存在无效ID,则所有ID均有效
  3. 在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
    

    功能渲染

    我使用isValiddocument上欺骗了一点。在真正的函数式编程中,函数应该是。或者,换句话说,函数调用的结果必须只能从其输入中确定(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怎么样?

    不幸的是,我们无法执行ajax调用,改变DOM,设置事件侦听器或设置没有副作用的超时。我们可以为这些操作创建 monads 的复杂路径,但就我们的目的而言,只要说我们将继续使用非功能性方法就足够了。

    在React

    中应用它

    这是使用常见的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。请注意,我们可以通过几种不同的方式处理提交。一种方法是将usernamepassword作为道具传递给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