I have two components that share some identical methods, that will most probably not change anymore, and if so, then they'll change for both components. That's why I'd like to reduce redundancy here.
But these methods need to bind to
this
, because they accessprops
andstate
, e.g. like this one:
updateProductFavorites = (product_key, action) => {
Meteor.call('Accounts.updateProductFavorites', product_key, action, (err, response) => {
if (err)
makeAlert(err.reason, 'danger', 3000)
else
this.getProductsByKeys()
})
}
The two components are quite huge, so I'd like to keep them seperate, that is conditional rendering
is not an option for sharing the methods. The two components need to be called upon by different routes
. I don't want to pass methods from a parent component either, as there is no need for a parent component in this case.
Ideally I'd like to keep the methods in a separate file. But how can I properly bind them to the component after importing them? Or is there a completely different approach?
This question has been asked as a comment, but not been answered satisfyingly.
Edit: I learned what a HOC (Higher Order Component) is. As soon as I've learned how to implement them in my concrete case, I'll post an answer. Feel free to help me. I've posted my two components below.
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)