【问题标题】:Efficient way to share methods that bind to this between React Components在 React 组件之间共享绑定到 this 的方法的有效方法
【发布时间】:2019-06-16 15:38:15
【问题描述】:

我有两个组件共享一些相同的方法,很可能不会再改变,如果是这样,那么两个组件的它们都会改变。这就是为什么我想在这里减少冗余

但是这些方法需要绑定到this,因为它们访问propsstate,例如喜欢这个:

updateProductFavorites = (product_key, action) => {
    Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
        if (err)
            makeAlert(err.reason, 'danger', 3000)
        else 
            this.getProductsByKeys()    
    })
}

这两个组件非常庞大,所以我想将它们分开,即conditional rendering 不是共享方法的选项。这两个组件需要由不同的routes 调用。我也不想从父组件传递方法,因为在这种情况下不需要父组件。

理想情况下,我希望将方法保存在单独的文件中。但是在导入它们后如何正确地将它们绑定到组件?还是有完全不同的方法?

这个问题已被作为comment 提出,但没有得到令人满意的回答。


编辑:我了解了什么是 HOC(高阶组件)。一旦我学会了如何在我的具体案例中实现它们,我就会发布一个答案。随时帮助我。我在下面发布了我的两个组件。

import { Meteor } from 'meteor/meteor';
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { Session } from 'meteor/session';
import makeAlert from '../makeAlert';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';


class ProductFavorites extends Component {
    constructor() {
        super()

        this.state = {
            products: [],
            productDetails: true,
            singleProductDetails: 0,
        }


    }
    
    updateProductFavorites = (product_key, action) => {
        Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
            if (err)
                makeAlert(err.reason, 'danger', 3000)
            else 
                this.getProductsByKeys()    
        })
    }

    toggleProductFavorite = (product_key) => {
        const { productFavorites } = this.props.user
        if (productFavorites.includes(product_key))
            this.updateProductFavorites(product_key, 'remove')
        else
            this.updateProductFavorites(product_key, 'add')
    }

    toggleSingleProductDetails = (order_number) => {
        const { singleProductDetails: current_product } = this.state
        order_number = current_product == order_number ? 0 : order_number
        this.setState({singleProductDetails: order_number})
    }

    toggleProductDetails = () => {
        this.setState((prevState) => ({productDetails: !prevState.productDetails}))
    }

    getProductsByKeys = () => {
        Meteor.call('Products.getByProductKey', (err, response) => {
            if (err)
                makeAlert(err.reason, 'danger', 3000)
            else 
                this.setState({products: response})
        })
    }

    mapProductFavorites = () => {
        const { products, productDetails, singleProductDetails } = this.state
        const { productFavorites } = this.props.user
        if (products.length == 0)
            return <div className="alert alert-primary col-12">You haven't favorited any products at the moment.</div>
        return (
            products.map((product, i) => {
                if (product.price_100_g_ml) {
                    var [euro, cent] = product.price_100_g_ml.toFixed(2).toString().split('.')
            }

            const { product_name, units, trading_unit, certificate, origin, order_number, supplierId } = product
            const isFavorite = productFavorites.includes(`${supplierId}_${order_number}`) ? 'is-favorite' : 'no-favorite'

            return (
                <div className="col-lg-4" key={i}>
                    <div key={i} className="product-card">
                        <div className="card-header" onClick={() => this.toggleSingleProductDetails(order_number)}>
                            {product_name}
                        </div>
                        {productDetails || singleProductDetails == order_number ?
                        <>
                            <div className="card-body">
                                {euro ?
                                    <>
                                        <div className="product-actions">
                                            <button className={`btn btn-light btn-lg product-${isFavorite}`}
                                                onClick={() => this.toggleProductFavorite(`${supplierId}_${order_number}`)}>
                                                <FontAwesomeIcon icon="heart"/>
                                            </button>
                                        </div>
                                        <div className="price-100-g-ml">
                                            <small>pro 100{units == 'kg' ? 'g' : 'ml'}</small><sup></sup>
                                            <big>{euro}</big>.<sup>{cent.substring(0,2)}</sup>
                                        </div>
                                    </> : null}
                            </div>
                            <div className="card-footer">
                                <div className="row">
                                    <div className="col-4">{trading_unit}</div>
                                    <div className="col-4 text-center">{certificate}</div>
                                    <div className="col-4 text-right">{origin}</div>
                                </div>                            
                            </div>
                        </> : null }
                    </div>
                </div>)
            })
        )
    }

    

    componentWillMount() {
        this.getProductsByKeys()
    }

    render() {
        const { isLoading } = this.props
        if (isLoading)
            return null
        const { productFavorites } = this.props.user
        console.log(productFavorites)
        return(
            <div className="container app-content product-favorites">
                <div className="row mt-3">
                    {this.mapProductFavorites()}
                </div>
            </div>
        )
    }
}

export default withTracker(() => {
    return {
        user: Meteor.user(),
        isLoading: !Meteor.user()
    }
})(ProductFavorites)


import { Meteor } from 'meteor/meteor';
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { Session } from 'meteor/session';
import makeAlert from '../makeAlert';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';


class ProductCatalog extends Component {
    constructor() {
        super()

        this.state = {
            categoriesBySupplier: [],
            productsFromCategory: [],
            supplierSection: {
                'supplier_0': true
            },
            productDetails: false,
            singleProductDetails: 0,
        }
    }

    updateProductFavorites = (product_key, action) => {
        Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
            if (err)
                makeAlert(err.reason, 'danger', 3000)
        })
    }
   

    getProductsFromCategoryOfSupplier = (supplierId, category1) => {
        // console.log(supplierId, category1)
        Meteor.call('Products.getFromCategory.ofSupplier', supplierId, category1, (err, response) => {
            if (err)
                makeAlert(err.reason, "danger", 3000)
            else
                this.setState({productsFromCategory: response})
        })
    }

    getProductCategories = () => {
        Meteor.call('Products.getCategories', (err, response) => {
            if (err)
                makeAlert(err.reason, "danger", 3000)
            else {
                this.setState({categoriesBySupplier: response})
                this.getProductsFromCategoryOfSupplier(0, response[0].category1[0])
            }
        })
    }

    productCategories = ({_id, category1}) => {
        return (
            category1.map((category, i) =>
                <button className="btn btn-primary btn-sm mr-1 mb-1" onClick={() => this.getProductsFromCategoryOfSupplier(_id, category)} key={i}>
                    {category}
                </button>)
        )
    }

    productsFromCategory = () => {
        const { productsFromCategory, productDetails, singleProductDetails } = this.state
        let { productFavorites } = this.props.user
        productFavorites = productFavorites == undefined ? [] : productFavorites
        // console.log(productsFromCategory, productFavorites)
        return (
            productsFromCategory.map((product, i) => {
                if (product.price_100_g_ml) {
                    var [euro, cent] = product.price_100_g_ml.toFixed(2).toString().split('.')
            }

            const { product_name, units, trading_unit, certificate, origin, order_number, supplierId } = product
            const isFavorite = productFavorites.includes(`${supplierId}_${order_number}`) ? 'is-favorite' : 'no-favorite'

            return (
                <div className="col-lg-4" key={i}>
                    <div key={i} className="product-card">
                        <div className="card-header" onClick={() => this.toggleSingleProductDetails(order_number)}>
                            {product_name}
                        </div>
                        {productDetails || singleProductDetails == order_number ?
                        <>
                            <div className="card-body">
                                {euro ?
                                    <>
                                        <div className="product-actions">
                                            <button className={`btn btn-light btn-lg product-${isFavorite}`}
                                                onClick={() => this.toggleProductFavorite(`${supplierId}_${order_number}`)}>
                                                <FontAwesomeIcon icon="heart"/>
                                            </button>
                                        </div>
                                        <div className="price-100-g-ml">
                                            <small>pro 100{units == 'kg' ? 'g' : 'ml'}</small><sup></sup>
                                            <big>{euro}</big>.<sup>{cent.substring(0,2)}</sup>
                                        </div>
                                    </> : null}
                            </div>
                            <div className="card-footer">
                                <div className="row">
                                    <div className="col-4">{trading_unit}</div>
                                    <div className="col-4 text-center">{certificate}</div>
                                    <div className="col-4 text-right">{origin}</div>
                                </div>                            
                            </div>
                        </> : null }
                    </div>
                </div>)
            })
        )
    }

    toggleSupplierSection = (event) => {
        const supplier = event.currentTarget.id
        this.setState((prevState) => ({supplierSection: {[supplier]: !prevState.supplierSection[supplier]}}))
    }

    toggleProductDetails = () => {
        this.setState((prevState) => ({productDetails: !prevState.productDetails}))
    }

    toggleSingleProductDetails = (order_number) => {
        const { singleProductDetails: current_product } = this.state
        order_number = current_product == order_number ? 0 : order_number
        this.setState({singleProductDetails: order_number})
    }

    toggleProductFavorite = (product_key) => {
        const { productFavorites } = this.props.user
        if (productFavorites.includes(product_key))
            this.updateProductFavorites(product_key, 'remove')
        else
            this.updateProductFavorites(product_key, 'add')
    }

    supplierSection = (supplier) =>
        <>
            {this.productCategories(supplier)}
            {<div className="row mt-3">{this.productsFromCategory()}</div>}
        </>
        
        
    mapSupplierSections = () => {
        const { categoriesBySupplier, supplierSection } = this.state
        
        if (categoriesBySupplier.length < 1)
            return null
        return categoriesBySupplier.map(supplier => {
            var icon = 'caret-up'
            var supplierId = supplierSection["supplier_" + supplier._id]
            if (supplierId != undefined) {
                var expand = supplierSection["supplier_" + supplier._id]
                icon = expand ? 'caret-up' : 'caret-down'
            }
            return (
                <div key={supplier._id} className="col-12">
                    <div className="input-group input-group-lg mb-3">
                        <div className="input-group-prepend">
                            <span className="input-group-text supplier-name">{supplier.supplierName}</span>
                        </div>
                        <div className="input-group-append">
                            <button className="btn btn-secondary" id={"supplier_" + supplier._id} onClick={this.toggleSupplierSection}>
                                <FontAwesomeIcon icon={icon} className="toggle-supplier-section"/>
                            </button>
                            <button className="btn btn-primary" id={"supplier_" + supplier._id} onClick={this.toggleProductDetails}>
                                <FontAwesomeIcon icon='th-list' className="toggle-supplier-section"/>
                            </button>
                        </div>
                    </div>
                    {expand
                        ? this.supplierSection(supplier)
                        : null
                    }
                </div>
            )
        })
    }

    componentWillMount() {
        this.getProductCategories()
    }

    render() {
        const { isLoading } = this.props
        if (isLoading)
            return null
        return (
            <div className="container app-content product-catalog">
                {this.mapSupplierSections()}
            </div>
        )
    }
}

export default withTracker(() => {
    return {
        user: Meteor.user(),
        isLoading: !Meteor.user()
    }
})(ProductCatalog)

【问题讨论】:

  • 您是否考虑过创建一个服务 JS 类,将可重用的方法放入该类中,并在使用这些方法的组件中实例化该类?不确定这是否适用于您的用例,但只是一个想法。
  • 听起来是使用 HOC 的好场景
  • @seanulus 是的,但是我怎样才能将它们绑定到组件的 this,特别是如果它是一个箭头函数?
  • @fagnzhzh 谢谢。我了解到父组件!== HOC。完全误解了高阶组件这个术语。

标签: reactjs methods import modularity


【解决方案1】:

React 16.8 开始,我们有一个新的解决方案,称为hooks,准确地说是自定义挂钩。自定义钩子允许我们将有状态的组件逻辑提取到一个函数中,然后在其他组件中重用它。然而,它们仅限于功能组件。不幸的是,你不能在类组件中调用钩子函数。

(所以这对我来说不是一个解决方案,因为我已经在这里编写了类组件,我暂时不会重写它们。我将不得不坚持使用 HOC)

您可以在official docs 中阅读有关挂钩的更多信息。听起来很有希望,似乎是未来的 React 方式。

【讨论】:

    猜你喜欢
    • 2020-09-23
    • 1970-01-01
    • 1970-01-01
    • 2016-05-04
    • 1970-01-01
    • 1970-01-01
    • 2018-03-25
    • 1970-01-01
    相关资源
    最近更新 更多