How can I test arguments passed to a render prop? - reactjs

I have the following component, Layout:
const Layout = ({ children, data, ...otherProps }) => (
<ErrorBoundary>
<App render={({ isSidebarOpen, scrollTop, toggleSidebar }) => (
<React.Fragment>
<Helmet
title={get(data, 'site.siteMetadata.title')}
meta={[
{ name: 'description', content: get(data, 'site.siteMetadata.description') },
{ name: 'pinterest', content: 'nopin' },
{ name: 'og:title', content: 'Daniel Spajic' },
{ name: 'og:description', content: get(data, 'site.siteMetadata.description') },
{ name: 'og:type', content: 'website' },
{ name: 'og:url', content: get(data, 'site.siteMetadata.siteUrl') },
{ name: 'og:image', content: ogImage },
{ name: 'og:locale', content: 'en_AU' },
]}
>
<link rel="shortcut icon" type="image/png" href={favicon} />
<link href="https://afeld.github.io/emoji-css/emoji.css" rel="stylesheet" />
</Helmet>
<div id={PAGE_CONTENT_CONTAINER_ID}>
<Sidebar isOpen={isSidebarOpen} toggle={toggleSidebar} />
<div id={PAGE_CONTENT_ID}>
{children({ scrollTop, toggleSidebar, ...otherProps })}
</div>
</div>
</React.Fragment>
)}
/>
</ErrorBoundary>
);
As shown it renders an App with a render prop. The isSidebarOpen and scrollTop arguments for the prop are both from App's state. toggleSidebar is one of App's methods.
I want to test a few things:
The rendered Sidebar sets its toggle prop to toggleSidebar, and isOpen prop to isSidebarOpen
The children function is called with an object containing scrollTop, toggleSidebar, and otherProps as its argument
These involve retrieving App's state and methods for comparison. I've tried accessing its state with Enzyme, but it's not possible because state() can only be accessed on the root node:
ShallowWrapper::state() can only be called on the root
Therefore how can I access App's state and methods so I can test these things?

ShallowWrapper::state() can only be called on the root may not be a problem because tested values should be preferably hard-coded in unit tests. It's better to make a test unintentionally fail where it should pass than to make it unintentionally pass where it should fail, the former is much easier to debug and fix.
Though it may be beneficial to get component state, at least for assertions.
const layoutWrapper = mount(<Layout/>);
const appWrapper = layoutWrapper.find(App).dive();
expect(appWrapper.state('isSidebarOpen')).toBe(false);
expect(appWrapper.first(Sidebar).props('isOpen').toBe(false);
appWrapper.setState({ isSidebarOpen: true });
expect(appWrapper.state('isSidebarOpen')).toBe(true);
expect(appWrapper.first(Sidebar).props('isOpen').toBe(true);
There's a lot of moving parts in this component, this is also suggested by that it should be tested with mount and not shallow. it may be beneficial to provide fine-grained isolated tests, i.e. test render prop separately:
const layoutWrapper = mount(<Layout/>);
const appWrapper = layoutWrapper.first(App);
const Child = appWrapper.prop('render');
const childWrapper = shallow(<Child isSidebarOpen={false} ... />);
expect(childWrapper.find(Sidebar).props('isOpen').toBe(false);
...
And how App state interacts with render prop should be tested in dedicated App component test.

Related

How to apply styles to TabPane in new Antd Tabs component

This is my old implementation of the Tabs component in Ant Design.
const tabList = [
{
key: "tab1",
label: "Tab1",
children: <Tab1 />,
},
{
key: "tab2",
label: "Tab2",
children: <Tab2 />,
},
];
<Tabs onChange={onTabChange} activeKey={selectedTab}>
{tabList.map((tab) => {
const { key, label, children } = tab;
return (
<Tabs.TabPane
key={key}
tab={label}
style={{ margin: "1.5rem auto 1.5rem" }}
>
{children}
</Tabs.TabPane>
);
})}
</Tabs>;
In the new version ( > 4.23.0 ) the boilerplate got reduced.
I can simply pass my tabList to my Tabs as a prop items.
The new code looks something like this.
<Tabs items={tabList} />
But I had an issue with styling.
I am adding top and bottom margins to all of my TabPane components.
To get that margin in the new implementation. I had to do something like this.
{
key: "tab1",
label: "Tab1",
children: <Tab1 style={{margin: "1.5rem 0 1.5rem"}} />,
},
Here I am facing two issues.
I need to add this for all my tabs in the tabList
I need to have a div in every component spreading the props that are passed above.
function Tab1(props) {
return <div {...props}>JSX for original Tab1</div>;
}
Is there a better way to do this?
Higher order component can solve this issue
Use a higher Order component like <TabPaneWrapper> or <TabChildrenWrapper>. This component does nothing but to wrap your children (<TabPane>) with a div and give the styles you require.
Component:
export function TabPaneWrapper({
children,
...props
}){
return (
<div style={{ margin: "1.5rem auto 1.5rem" }} {...props}>
{children}
</div>
);
}
Usage:
const tabList = [
{
key: "tab1",
label: "Tab1",
children: <TabPaneWrapper> <Tab1 /> </TabPaneWrapper>,
},
{
key: "tab2",
label: "Tab2",
children: <TabPaneWrapper> <Tab2 /> </TabPaneWrapper>,
},
];
If you have more tabs or use this tabs component in multiple places. You will find TabPaneWrapper to be repetitive. In such case,
You can create a custom Tabs component like <CustomTabs/> which takes the tabList mentioned in the question.
Instead of wrapping every tab in the tabList with <TabPaneWrapper/>. You can loop this list inside the <CustomTabs/> component to generate the tabList mentioned above and then pass it to the Ant Desing <Tabs/> component.

Is it possible to pass components as props, or Images as props [duplicate]

Lets say I have:
import Statement from './Statement';
import SchoolDetails from './SchoolDetails';
import AuthorizedStaff from './AuthorizedStaff';
const MultiTab = () => (
<Tabs initialIndex={1} justify="start" className="tablisty">
<Tab title="First Title" className="home">
<Statement />
</Tab>
<Tab title="Second Title" className="check">
<SchoolDetails />
</Tab>
<Tab title="Third Title" className="staff">
<AuthorizedStaff />
</Tab>
</Tabs>
);
Inside the Tabs component, this.props has the properties
+Children[3]
className="tablist"
justify="start"
Children[0] (this.props.children) will look like
$$typeof:
Symbol(react.element)
_owner:ReactCompositeComponentWrapper
_self:null
_shadowChildren:Object
_source:null
_store:Object
key:null
props:Object
ref:null
type: Tab(props, context)
__proto__
Object
Children[0].props looks like
+Children (one element)
className="home"
title="first title"
Finally Children object looks like (this is what i want to pass):
$$typeof:Symbol(react.element)
_owner:ReactCompositeComponentWrapper
_self:null
_shadowChildren:undefined
_source:null
_store:
key:null
props:Object
__proto__:Object
**type: function Statement()**
ref:null
The question is this, if I rewrite MultiTab like this
<Tabs initialIndex={1} justify="start" className="tablisty">
<Tab title="First Title" className="home" pass={Statement} />
<Tab title="Second Title" className="check" pass={SchoolDetails} />
<Tab title="Third Title" className="staff" pass={AuthorizedStaff} />
</Tabs>;
Inside the Tabs component
this.props.children looks the same as above.
children[0].props looks like
classname:"home"
**pass: function Statement()**
title: "First title"
I want the pass property to look like. Above just prints out the Statement function.
$$typeof:Symbol(react.element)
_owner:ReactCompositeComponentWrapper
_self:null
_shadowChildren:undefined
_source:null
_store:
key:null
props:Object
__proto__:Object
**type: function Statement()**
ref:null
This is a weird question, but long story I'm using a library and this is what it comes down to.
Using this.props.children is the idiomatic way to pass instantiated components to a react component
const Label = props => <span>{props.children}</span>
const Tab = props => <div>{props.children}</div>
const Page = () => <Tab><Label>Foo</Label></Tab>
When you pass a component as a parameter directly, you pass it uninstantiated and instantiate it by retrieving it from the props. This is an idiomatic way of passing down component classes which will then be instantiated by the components down the tree (e.g. if a component uses custom styles on a tag, but it wants to let the consumer choose whether that tag is a div or span):
const Label = props => <span>{props.children}</span>
const Button = props => {
const Inner = props.inner; // Note: variable name _must_ start with a capital letter
return <button><Inner>Foo</Inner></button>
}
const Page = () => <Button inner={Label}/>
If what you want to do is to pass a children-like parameter as a prop, you can do that:
const Label = props => <span>{props.content}</span>
const Tab = props => <div>{props.content}</div>
const Page = () => <Tab content={<Label content='Foo' />} />
After all, properties in React are just regular JavaScript object properties and can hold any value - be it a string, function or a complex object.
As noted in the accepted answer - you can use the special { props.children } property. However - you can just pass a component as a prop as the title requests. I think this is cleaner sometimes as you might want to pass several components and have them render in different places. Here's the react docs with an example of how to do it:
https://reactjs.org/docs/composition-vs-inheritance.html
Make sure you are actually passing a component and not an object (this tripped me up initially).
The code is simply this:
const Parent = () => {
return (
<Child componentToPassDown={<SomeComp />} />
)
}
const Child = ({ componentToPassDown }) => {
return (
<>
{componentToPassDown}
</>
)
}
By using render prop you can pass a function as a component and also share props from parent itself:
<Parent
childComponent={(data) => <Child data={data} />}
/>
const Parent = (props) => {
const [state, setState] = useState("Parent to child")
return <div>{props.childComponent(state)}</div>
}
I had to render components conditionally, so the following helped me:
const Parent = () => {
return (
<Child componentToPassDown={<SomeComp />} />
)
}
const Child = ({ componentToPassDown }) => {
return (
<>
{conditionToCheck ? componentToPassDown : <div>Some other code</div>}
</>
)
}
In my case, I stacked some components (type_of_FunctionComponent) into an object like :
[
{...,one : ComponentOne},
{...,two : ComponentTwo}
]
then I passed it into an Animated Slider, and in the the slide Component, I did:
const PassedComponent:FunctionComponent<any> = Passed;
then use it:
<PassedComponent {...custom_props} />
How about using "React.createElement(component, props)" from React package
const showModalScrollable = (component, props) => {
Navigation.showModal({
component: {
name: COMPONENT_NAMES.modalScrollable,
passProps: {
renderContent: (componentId) => {
const allProps = { componentId, ...props };
return React.createElement(component, allProps);
},
},
},
});
};

Render a React component by mapping an array inside of an array of objects

I have trouble rendering a react component by mapping an array nested inside an array of objects.
I have this React component, which is simple images
function ProjectLangIcon(props) {
return (
<img className="project-icon" src={props.src} alt={props.alt}/>
)
}
This component is a part of this bigger one :
function Project(props) {
const projectLang = projectList.map(projects => {
return projects.lang.map(langs => {
return <ProjectLangIcon
key={langs.key}
src={langs.src}
alt={langs.alt}
/>
})
})
return (
<div className="project">
{props.title}
<div className="project-content">
{projectLang}
</div>
</div>
)
}
I'm mapping the data from this file :
const projectList = [
{
key: 1,
name: "Site de mariage from scratch",
link: "https://mariage-hugo-et-noemie.fr/view/index.php",
lang: [css, html, javascript, php, mySql]
},
{
key: 2,
name: "Site de paris sportifs",
link: "https://akkezxla.ae.lu/Akkezxla-Betting-Site/bet-list.php",
lang: [css, html, javascript, php, mySql]
},
{
key: 3,
name: "Site de location d'appartement",
link: "http://paris-island.com/",
lang: [wordpress, css, javascript]
}
]
with the lang array being constitued of objects like these :
const css = {
name: "CSS",
src: cssIcon,
alt: "Icône CSS"
}
const html = {
name: "HTML",
src: htmlIcon,
alt: "Icône HTML"
}
Then, I map the projects component inside the Realisation component like this :
function Realisation() {
const projects = projectList.map(item => {
return <Project
key={item.key}
title={item.name}
link={item.link}
/>
})
return (
<div className="realisations pro-block">
<BlockTitle
title="Réalisations"
close={closeReal}
/>
<div className="realisation-content">
{projects}
</div>
<BlockNav
left="Compétences"
switchl={switchComp}
center="Parcours Porfessionnel"
switchc={switchParcoursPro}
right="Parcours Académique"
switchr={switchParcoursAcad}
/>
</div>
)
}
the result I get is this : enter image description here
But I want each projects to have its corresponding language icons.
Does anyone have an idea of hiw I should proceed ?
Your issue is due to Project component. You are rendering this component for each Project of projectList but inside of this component you are not taking in the account the actual Project that is passed to this component.
Project (component)
const projectLang = projectList.map(projects => {
return projects.lang.map(langs => {
return <ProjectLangIcon
key={langs.key}
src={langs.src}
alt={langs.alt}
/>
})
})
Lines above are building the same projectLang for any Project you passed to this component. And is building it from All the projects you have, it returns the array of arrays. So [[all_icons_of_project_1], [all_icons_of_project_2], ...].
Here is what you should do:
First: You need to modify Product component to either pass a product item itself, either to pass additional property, langs in your case. And to modify .map function a little.
Notice: i destructured the props object passed as a parameter to Product component. Also, i wrapped the map function into useMemo for optimization purposes.
function Project({ title, link, langs }) {
const projectLang = useMemo(() => {
return langs.map((lang) => (
<ProjectLangIcon key={lang.key} src={lang.src} alt={lang.alt} />
));
}, [langs]);
return (
<div className="project">
<a href={link} className="project-txt" target="_blank">
{title}
</a>
<div className="project-content">{projectLang}</div>
</div>
);
}
Second - just pass the langs attribute (prop) to your Product component. (Again, i wrapped it into useMemo, for optimizations)
function Realisation() {
const projects = useMemo(() => {
return projectList.map((item) => (
<Project
key={item.key}
title={item.name}
link={item.link}
langs={item.lang}
/>
));
}, []);
return (
<div className="realisations pro-block">
{/* ... */}
<div className="realisation-content">{projects}</div>
{/* ... */}
</div>
);
}
Note - no icons in codesandbox, only placeholders and alt text.

How to pass a non prop value to a component in unit test with react testing library

I need to pass a value to the component with react testing library, the value isn't a component prop but a value that is a local state or a state from state management library.
If the value is a props we could just pass it to the component <Current book="some object"/> but in case the book value is from a local state or from a state management library, then how to pass it from unit test.
Component Code:
I use recoil (state management library), but it can be also a redux or a local state.
function Current() {
const currentBook = useRecoilValue<BookProps | null>(currentAtom);
return (
<div className={styles.current__container}>
<h3>Current Reading Book</h3>
{currentBook && (
<Link href="reading">
<div className={styles.current__reading}>
<div className={styles.current__image}>
<Image
src={currentBook.img}
alt="currentbook image"
width={63}
height={93}
objectFit="cover"
/>
</div>
<div>
<p>{currentBook?.name}</p>
<p>{currentBook?.author}</p>
</div>
</div>
</Link>
)}
</div>
);
}
Unit Test Code:
const MockCurrent = () => {
return (
<RecoilRoot>
<Current />
</RecoilRoot>
);
};
describe("MenuSelect", () => {
const book = {
_id: "628f164f901c4b564bad6dca",
name: "400 Days",
slug: "400-days",
category: "crime",
author: "Chetan Bhagat",
authorId: "0",
img: "https://images-na.ssl-images-amazon.com/images/I/81tSFxicufL.jpg",
user: "user#test.com",
createdAt: "2022-05-26T05:55:27.055Z",
updatedAt: "2022-05-26T05:55:27.055Z",
__v: 0,
};
it("should have author menu if the user is logged in", () => {
render(<MockCurrent />);
const textElement = screen.getByText(/current reading book/i);
expect(textElement).toBeVisible();
});
});
From the above code, book is the value that needs to be passed to the component to check if it renders properly.
How to pass the book value to the Current Component?

React multi carousel renders items wrongly

I started using Next js and I don't know whether it is problem regarding to it or React itself.
So the problem is that the "react-multi-carousel" does not work in my app. So, basically it works if I hardcode the values in there, but when I use my custom components, where the is map function, it does not render it properly. It takes 3 components as they are in one . You can verify it on the image I posted below. I tried to render Compilations component outside SliderCarousel component and it worked as it should, but when I pass Compilations as a child to SliderCarousel, it does not catch it and give it its own classes from react-multi-carousel library
Here is my code below and I ommited some imports and exports to focus attention on main parts
My Compilation component looks like this:
const compilation = ({ className, text, img }) => {
return (
<div className={`${className} ${classes.Compilation}`}>
<img src={img} alt={text} />
<div>
<h3>{text}</h3>
</div>
</div>
);
};
My Compilations component looks like this:
const compilations = ({ items, onClick }) => {
const compilationsView = items.map(item => {
return <Compilation key={item.id} onClick={() => onClick(item.id)} text={item.text} img={item.img} />;
});
return <React.Fragment>{compilationsView}</React.Fragment>;
};
SliderCarousel component looks like this
<Carousel
swipeable={true}
draggable={true}
showDots={true}
responsive={responsive}
ssr={true} // means to render carousel on server-side.
infinite={true}
autoPlay={true}
autoPlaySpeed={1000}
keyBoardControl={true}
customTransition="all .5"
transitionDuration={500}
containerClass="carousel-container"
removeArrowOnDeviceType={[ "tablet", "mobile" ]}
// deviceType={this.props.deviceType}
dotListClass="custom-dot-list-style"
itemClass="carousel-item-padding-40-px"
>
{items}
</Carousel>{" "}
Here is my pages/index.js file
<SliderCarousel items={<Compilations items={getBookCarouselItems()} />} />
And the function is:
{
id: 0,
img: "/static/images/main/books/slider-carousel/1.png",
text: "ТОП-10 романов"
},
{
id: 1,
img: "/static/images/main/books/slider-carousel/2.png",
text: "На досуге"
},
{
id: 2,
img: "/static/images/main/books/slider-carousel/3.png",
text: "Бестселлеры"
}
];
I hope that you can help me resolve this problem, cause I have no idea how to resolve this issue
actually this carousel makes a <li> for each element to manoeuvre the carousel effects as you can see in the inspect screenshot
in your code
const compilations = ({ items, onClick }) => {
const compilationsView = items.map(item => {
return <Compilation key={item.id} onClick={() => onClick(item.id)} text={item.text} img={item.img} />;
});
return <React.Fragment>{compilationsView}</React.Fragment>;
};
you are wrapping your map list in fragment and hence carousel got only one item as a component and hence one <li/>
so in order to work you'll have to pass the map list (i.e. array) of <Compilation />
const allCompilations = (items) => items.map(item => {
return <Compilation key={item.id} onClick={() => onClick(item.id)} text={item.text} img={item.img} />;
});
to you carousel as children
<SliderCarousel items={allCompilations(getBookCarouselItems())} />

Resources