Reusable HOC that takes multiple components - javascript

I have this simple Higher-Order Component that works fine but it only accepts a single component:
let VerticalSlider = (Komponent) => {
return (props) => (
<div className='slider'>
<Komponent {...props}/>
</div>
);
};
How would one rewrite the HOC to accept multiple components (unknown number) and return them as siblings (one by one under eachother) together with their respective props?
I assume this is how you'd call a HOC with multiple components as it's just a function:
VerticalSlider(MyComponent, MyOtherComponent)
I know how to accept and destruct an unknown number of "normal" props, but I'm a but lost here when it comes to passed in components.

You can do this:
import React, { Component } from 'react';
import { render } from 'react-dom';
let VerticalSlider = (...komponents) => {
return (props) => (
<div className='slider'>
{ komponents.map((K, i) => <K {...props.ps[i]} /> ) }
</div>
);
};
const Apple = props =>
<div>{ props.name }</div>;
const Orange = props =>
<div>{ props.desc }</div>;
const MyComp = VerticalSlider(Apple, Orange);
const App = () =>
<div>
Hello
<MyComp
ps={[{ name: 'apple' }, { desc: 'orange' }]} />
</div>
render(<App />, document.getElementById('root'));
But it's a little ugly and your coworkers will hate you. Keep it simple and don't overthink it. Here is a demo

Related

Avoid prop drilling in mapped components

Im mapping over some data and returning a NestedComponentOne each time. A nested child component NestedComponentThree needs access to a restaurant. How can I avoid prop drilling here? Wrapping every NestedComponentOne in a provider seems wrong.
const DATA = ["Restaurant 1"," Resto 2", "resto 3"]
export default DATA
import data from './data'
export default function App() {
return (
<div className="App">
{data.map((restaurant) => {
return <NestedComponentOne/>
})}
</div>
);
}
const NestedComponentOne = () => <div><NestedComponentTwo/></div>
const NestedComponentTwo = () => <div><NestedComponentThree/></div>
// I need access to restaurant
const NestedComponentThree = () => <div>restaurant</div>
The React doc recommends component composition:
Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult.
If you only want to avoid passing some props through many levels, component composition is often a simpler solution than context.
So, you could for example do the following:
const data = ['Restaurant 1', ' Resto 2', 'resto 3'];
export default function App() {
return (
<div className="App">
{data.map((restaurant) => {
return (
<NestedComponentOne>
<NestedComponentTwo>
<NestedComponentThree restaurant={restaurant} />
</NestedComponentTwo>
</NestedComponentOne>
);
})}
</div>
);
}
const NestedComponentOne = ({ children }) => <div>{children}</div>;
const NestedComponentTwo = ({ children }) => <div>{children}</div>;
// Now you have access in this component to restaurant
const NestedComponentThree = ({ restaurant }) => <div>{restaurant}</div>;
Another way to do this is by passing the component as a prop:
const data = ['Restaurant 1', ' Resto 2', 'resto 3'];
export default function App() {
return (
<div className="App">
{data.map((restaurant) => {
return (
<NestedComponentOne
componentTwo={
<NestedComponentTwo
componentThree={
<NestedComponentThree restaurant={restaurant} />
}
/>
}
/>
);
})}
</div>
);
}
const NestedComponentOne = ({ componentTwo }) => <div>{componentTwo}</div>;
const NestedComponentTwo = ({ componentThree }) => <div>{componentThree}</div>;
// Now you have access in this component to restaurant
const NestedComponentThree = ({ restaurant }) => <div>{restaurant}</div>;

How to pass props to children with functional components?

I'm having a trouble with passing props to children in functional react. My {children} is a Details component like below:
<SidebarPage>
{children}
</SidebarPage>
const Details = () => {}
how can I pass props in way that, I put props to {children} and I receive this props in Details component? Is this even workable?
Thanks for any help!
You can pass component as prop, something like -
const SidebarPage = ({ChildComponent}) => {
const props = { name: "X" };
return <ChildComponent {...props} />
}
const Details = (props) => {}
<SidebarPage ChildComponent={Details} />
You could use cloneElement to add new props to a component children
const newProps = {aProp: "aProp"} ;
return (
<SidebarPage>
{React.Children.map(props.children, child => {
return React.cloneElement(child, {
newProps,
}, null)
}
</SidebarPage>
)

React hooks: Dynamically mapped component children and state independent from parent

I am gathering posts (called latestFeed) from my backend with an API call. These posts are all mapped to components and have comments. The comments need to be opened and closed independently of each other. I'm governing this mechanic by assigning a piece of state called showComment to each comment. showComment is generated at the parent level as dictated by the Rules of Hooks.
Here is the parent component.
import React, { useState, useEffect } from "react";
import { getLatestFeed } from "../services/axios";
import Child from "./Child";
const Parent= () => {
const [latestFeed, setLatestFeed] = useState("loading");
const [showComment, setShowComment] = useState(false);
useEffect(async () => {
const newLatestFeed = await getLatestFeed(page);
setLatestFeed(newLatestFeed);
}, []);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
return (
<div className="dashboardWrapper">
<Child posts={latestFeed} showComment={showComment} handleComment={handleComment} />
</div>
);
};
export default Parent;
latestFeed is constructed along with showComment. After latestFeed comes back with an array of posts in the useEffect hook, it is passed to the child show here:
import React, { useState } from "react";
const RenderText = ({ post, showComment, handleComment }) => {
return (
<div key={post._id} className="postWrapper">
<p>{post.title}</p>
<p>{post.body}</p>
<Comments id={post._id} showComment={showComment} handleComment={() => handleComment(post)} />
</div>
);
};
const Child = ({ posts, showComment, handleComment }) => {
return (
<div>
{posts.map((post) => {
<RenderPosts posts={posts} showComment={showComment} handleComment={handleComment} />;
})}
</div>
);
};
export default Child;
However, whenever I trigger handleComments, all comments open for all posts. I'd like them to be only the comment that was clicked.
Thanks!
You're attempting to use a single state where you claim you want multiple independent states. Define the state directly where you need it.
In order to do that, remove
const [showComment, setShowComment] = useState(false);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
from Parent, remove the showComment and handleComment props from Child and RenderText, then add
const [showComment, handleComment] = useReducer(state => !state, false);
to RenderText.

useEffect is not firing inside Hoc

If I had my component without HOC it did fire but now i wrapped my component inside withSpinner Hoc but it does not fire the fetching start.
const CollectionPage = (props) => {
const { isCollectionLoaded, isCollectionFetching } = props;
useEffect(() => {
props.fetchCollectionsStart();
}, []);
const { title, items } = props.collection;
return (
<div className="collection-page">
<SearchBar />
<h2 className="title">{title} </h2>
<div className="items">
{items.map((item) => (
<CollectionItem key={item.id} {...props} item={item} />
))}
</div>
</div>
);
};
const mapStateToProps = (state, ownProps) => ({
collection: selectCollection(ownProps.match.params.collectionId)(state),
isCollectionFetching: selectIsCollectionFetching(state),
isCollectionLoaded: selectIsCollectionsLoaded(state),
});
export default WithSpinner(
connect(mapStateToProps, { fetchCollectionsStart })(CollectionPage)
);
here is the console of the state.
and this is the withSpinner Hoc:
const WithSpinner = (WrappedComponent) => ({
isCollectionLoaded,
...otherProps
}) => {
return !isCollectionLoaded ? (
<SpinnerOverlay>
<SpinnerContainer />
</SpinnerOverlay>
) : (
<WrappedComponent {...otherProps} />
);
};
export default WithSpinner;
As you can see from the image, I just see the spinner is spinning becuase fetchCollectionStart is not firing so redux store is not updated.
isCollectionLoaded will be true (as I suspect) once dispatch fetchCollectionsStart finishes and redux state is updated.
But you have an issue, fetchCollectionsStart is only dispatched at CollectionPage mount phase which never occurs since isCollectionLoaded is false by default and WithSpinner blocks CollectionPage.
I suggest to move the dispatch useEffect to Spinner Hoc, which makes sense given your code structure. your hoc may look something like:
const WithSpinner = (WrappedComponent) => ({
isCollectionLoaded,
fetchCollectionsStart,
...otherProps
}) => {
useEffect(() => {
fetchCollectionsStart();
}, []);
return !isCollectionLoaded ? (
<SpinnerOverlay>
<SpinnerContainer />
</SpinnerOverlay>
) : (
<WrappedComponent {...otherProps} />
);
};
export default WithSpinner
It's because your property isCollectionLoaded isn't being updated, and your view to update the spinner to the WrappedComponent depends on the property isCollectionLoaded being changed.
You're already using a higher-order component with redux's connect, but what you're attempting to do is create a composite component, with the Spinner and collection searcher. Your instance of withSpinner in the second example will need to expose or call the connect function, so that redux can do its magic.
By exposing the named component in the first example, you're exposing a React component that has bound logic:
export default WithSpinner(
connect(mapStateToProps, { fetchCollectionsStart })(CollectionPage)
);
This can be used as:
<WithSpinner/>
The easier solution, rather than creating a composite component, is to add the spinner to the CollectionPage component:
if (!isContentLoaded) {
return (<Spinner/>);
}
return (
<div className="collection-page">
<SearchBar />
<h2 className="title">{title} </h2>
<div className="items">
{items.map((item) => (
<CollectionItem key={item.id} {...props} item={item} />
))}
</div>
</div>
);

Using dot notation with functional component

Official ReactJs documentation recommends to create components following the dot notation like the React-bootstrap library:
<Card>
<Card.Body>
<Card.Title>Card Title</Card.Title>
<Card.Text>
Some quick example text to build on the card title and make up the bulk of
the card's content.
</Card.Text>
</Card.Body>
</Card>
It is very easy to create this structure with the help of a class component:
const CardBody = ({ children }) => <div className='body'>{children}</div>;
class Card extends Component {
static Body = CardBody;
render() {
return (
<div className='card'>{this.props.children}</div>
);
}
}
But it's also recommended to use as much as possible functional component. Unfortunately I don't know how to achieve this using only functional component.
If I follow this way, I'm no more able to use Card as a component because he is now an object of components:
const Card = {
Component: CardComponent,
Body: CardBody
}
export default Card
I'd have to use it that way, and it's not really what I want:
<Card.Component>
<Card.Body>
...
Do you have any idea how to do that?
In function component you can do like so:
// Card.react.js
const Card = ({ children }) => <>{children}</>;
const Body = () => <>Body</>;
Card.Body = Body;
export default Card;
// Usage
import Card from "./Card.react.js";
const App = () => (
<Card>
<Card.Body />
</Card>
);
Or, you can exploit named exports:
// Card.react.js
export const Wrapper = ({ children }) => <>{children}</>;
export const Body = () => <>Body</>;
// Usage
import * as Card from "./Card.react.js";
const App = () => (
<Card.Wrapper>
<Card.Body />
</Card.Wrapper>
);
For functional components
const CardBody = ({ children }) => <div className='body'>{children}</div>;
const Card = (props) => (
<div className='card'>{props.children}</div>
);
Card.Body = CardBody
And then use it like
<Card>
<Card.Body>
....

Categories

Resources