Clone React JSX Element with nested state - javascript

I faced with some problem. I have heavy JSX Element with multipe states. In another part of app I need to pass this Element to Modal window with keeping all states. What is the best solution for solving this problem? Of course I can make Parent with all states and pass it to Child. But maybe it's possible to freeze all states and pass JSX Element as independent component?
Structure will look like:
ParentElement
|_
AnotherElement
|_
SomeHeavyElement
ParentElement:
const ParentElement= () => {
return (
<React.Fragment>
<AnotherElement />
<SomeHeavyElement />
</React.Fragment>
);
};
export default ParentElement;
AnotherElement:
const AnotherElement= () => {
return (
<React.Fragment>
<dialog>
<SomeHeavyElement/>
</dialog>
</React.Fragment>
);
};
export default AnotherElement;
SomeHeavyElement
const SomeHeavyElement= () => {
const [state1, setState1] = useState(true);
...
const [state99, setState99] = useState(false);
return (
<React.Fragment>
{/*some logic*/}
</React.Fragment>
);
};
export default SomeHeavyElement;

You have to lift state up, meaning you should define your state on top of both component (in <ParentElement>). You can't really freeze your component internal state.
Here is a minimal example:
const ParentElement= () => {
const [state1, setState1] = useState(true);
// ...
const [state99, setState99] = useState(false);
return (
<React.Fragment>
<AnotherElement state={{state1, state99}} />
<SomeHeavyElement state={{state1, state99}} />
</React.Fragment>
);
};
export default ParentElement;
const SomeHeavyElement= ({state}) => {
return (
<React.Fragment>
{/*some logic*/}
</React.Fragment>
);
};
export default SomeHeavyElement;
const AnotherElement= ({state}) => {
return (
<React.Fragment>
<dialog>
<SomeHeavyElement state={state} />
</dialog>
</React.Fragment>
);
};
export default AnotherElement;
Also, when you have a lot of useState defined, you could useReducer to centralize your component state. Also, if you want to avoid props drilling, you could define handle your state using React API context.

Related

onClick react error Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop

**I'm getting react Error while trying to change parent component styles onMouseEnter event. How could I change the styles via child component button?
The error is - Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
That seems odd for onMouseLeave={() => setHovered(false)} is an arrow function.
https://codesandbox.io/s/trusting-moon-djocul?
**
// App js
import React, { useState, useEffect } from "react";
import Categories from "./Categories";
import ShopPage from "./components/Products";
export default function App() {
const [data, setData] = useState(Categories);
useEffect(() => {
setData(data);
}, []);
return (
<div className="wrapper">
<ShopPage products={data} filterResult={filterResult} />
</div>
);
}
// Shopping page
const ShopPage = ({ products }) => {
return (
<>
<div>
<Products products={products} />
</div>
</>
);
};
// Parent component, the main part goes here
const Products = ({products}) => {
const [hovered, setHovered] = useState(false);
const [style, setStyle] = useState(false);
if (hovered) {
setStyle({
// inline styles
});
} else {
setStyle({
// inline styles
});
}
return (
<>
<Product setHovered={setHovered} style={style} products={products}/>
</>
);
};
export default Products;
// Child component
const Product = ({ setHovered, style, products }) => {
return (
<div className={styles.items}>
{products.map((value) => {
return (
<>
<div style={style}>
<button
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
Add to Cart
</button>
</div>
</>
);
})}
</div>
);
};
export default Product;
The issue is you are setting setHovered state in component, the simple solution could be to use it in useEffect and add required dependency.
If we talk about your code so you can easily do this by using the state in child component instead of passing through props.
I have updated your code below:
https://codesandbox.io/s/nameless-cookies-e6pwxn?file=/src/components/Product.js:141-151

Pass a variable from Layout to children in next js

I have the following code `
import { useState, useEffect } from 'react'
import LayoutContent from './layout_content';
type Props = {
children: JSX.Element | JSX.Element[]
}
const Layout = ({ children }: Props) => {
const [selected, setSelected] = useState(countries[0]);
const country= selected.id
return (
<>
<Sidebar onClick={toggle} sidebar={open} />
<LayoutContent sidebar={open} countriesWithsrc ={countriesWithsrc} selected={selected} lected={setSelected} >
{children}
</LayoutContent>
</>
)
}
export default Layout;
`
How do I pass the variable country from the Layout component to the children without state management ?.I.e I want to drill it.
If you don't want any state management you can use React.Children. It provides utilities to work with the children prop. React.Children.map will run a method for every immediate child of the component. You can use cloneElement along with that to create a clone of your element by passing in extra properties. Infact you can even modify the children of an element you are cloning, but that is not the ask here.
Do note that context would be the better way to do it.
const Layout = ({ children }: Props) => {
....
....
const modifiedChildren = React.Children.map(children, child => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { testProp : 'test' });
}
return child;
});
....
....
return (
<>
<Sidebar onClick={toggle} sidebar={open} />
<LayoutContent sidebar={open} countriesWithsrc ={countriesWithsrc} selected={selected} lected={setSelected} >
{modifiedChildren}
</LayoutContent>
</>
)
}

Extend default className component

Hey I use bootstrap with React and I try figure out, how I can extend my component by passing className props deeper. In my atom component I have two files. First one with component declaration.
Breadcrumb.js
export const Breadcrumb = (props) => {
const { className } = props;
const classes = getClasses(className);
return (
<Link to={props.path} className={classes} {...props}>
{props.children}
</Link>
);
};
and another one with getClasses() which returns all default BS classes.
Breadcrumb.style.js
export const getClasses = (extra = "") => {
const defaultClasses = getDefaultClasses();
const addingClasses = extra;
const classes = `${defaultClasses} ${addingClasses}`;
return classes;
};
const getDefaultClasses = () => `ps-3 fs-3 fw-bold text-decoration-none`;
What I want to achieve is, when I'll invoke my Breadcrumb component, and I'll decied to extend it on extra classes I can do that by pass className props...like
TopBar.js
export const TopBar = () => {
const breadcrumbs = useBreadcrumbs(routes, { disableDefaults: true });
const classes = getClasses();
return (
<div className={classes}>
{breadcrumbs.map(({ match, breadcrumb }) => (
<Breadcrumb
path={match.pathname}
children={breadcrumb}
className="cs_breadcrumb"
key={uuidv4()}
/>
))}
</div>
);
};
But when I do that, my declare Breadcrumb className is override by invoke Breadcrumb className... Although in Breadcrumb.js console.log(classes) returns concated classes.
Anyone knows how to achieve that or has any tips ?? I'll be glad
Change
export const Breadcrumb = (props) => {
const { className } = props;
const classes = getClasses(className);
return (
<Link to={props.path} className={classes} {...props}>
{props.children}
</Link>
);
};
to
export const Breadcrumb = ({ className, ...rest }) => {
const classes = getClasses(className);
return (
<Link to={props.path} className={classes} {...rest}>
{props.children}
</Link>
);
};
So, you need to extract the className prop in the place where props was, and also add ...rest for the rest props.
I guess you want to extend component classes with other classes passed via props.
If I understand correctly, you can try like this:
export const Breadcrumb = (props) => {
const { className } = props;
const classes = getClasses(className);
return (
<Link to={props.path} className={[classes, className].join(" ")]}
{...props}>
{props.children}
</Link>
);
};

Pass dynamic title to HOC common function in reactjs

I have one common component for Success and rendering this component through navigating routes.
SuccessComponent.jsx
var pageTitle;
const SuccessComponent = (props) => {
pageTitle = props.location.state.title;
return(
<> jsx code here </>
)
}
//This title needs to be dynamic, not getting props here hence took var pageTitle but getting undefined.
let SuccessComp = withTitle({component: SuccessComponent, title: pageTitle})
export default SuccessComp;
WithTitle component is setting title through react-helmet library and updating on each screen.
I need to change title on different calls of SuccessComponent. How can I achieve this?
I'm using SuccessComponent as below.
MyComponent.jsx
export default MyComponent = () => {
const onSubmit = () => {
props.history.push({pathname:'/success',state:{title: 'my comp'})
}
return(
<> jsx code here </>
)
}
MyComponent1.jsx
export default MyComponent1 = () => {
const onSubmit = () => {
props.history.push({pathname:'/success',state:{title: 'my comp 1'})
}
return(
<> jsx code here </>
)
}
withTitle.jsx
export default function withTitle({component: Component, title}){
return function title(props){
(
<>
<Helmet>
<title>{title}</title>
</Helmet>
<Component {...props} />
</>
)
}
}
You are sending the state through the react-router but you try to access local props. You need to access the title in this.props.location.state.title
If you have a look at this answer it will help you get the right conslusion. How to pass params with history.push/Link/Redirect in react-router v4?
withTitle.jsx
export default function withTitle({component: Component, title}){
const [titleState, setTitleState] = useState();
return function title(props){
(
<>
<Helmet>
<title>{title}</title>
</Helmet>
<Component {...props} setTitle={setTitleState}/>
</>
)
}
}
Added one method call and called it from Success Component as below.
SuccessComponent
const SuccessComponent = (props) => {
props.setTitle(props.location.state.pageTitle);
return(
<> jsx code here </>
)
}
//This title needs to be dynamic, not getting props here hence took var pageTitle but getting undefined.
let SuccessComp = withTitle({component: SuccessComponent })
export default SuccessComp;

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

Categories

Resources