React.cloneElement clone already cloned element to add new props - reactjs

I have have TestWrapper component that clones element and is supposed to make the background blue. Test is also doing the same, but instead sets color. I was expecting the rendered element to have blue background and red text, but it is only red. Here is the example:
const Test: React.FC<{ element: React.ReactElement }> = ({ element }) => {
return React.cloneElement(element, {
const isSelected = useIsSelected();
style: { ...element.props.style, color: isSelected ? 'red' : 'black' }
});
};
const TestWrapper: React.FC<{ element: React.ReactElement }> = ({
element
}) => {
// BackgroundColor is blue
const { backgroundColor } = useTheme();
return React.cloneElement(element, {
style: { ...element.props.style, background: backgroundColor }
});
};
export function App() {
return <TestWrapper element={<Test element={<h1>Heading</h1>} />} />;
}
How can I achieve this? I could do this differently, but I have to be able to access hook methods from Test and TestWrapper.
Simple codesandbox example: https://codesandbox.io/s/serene-bassi-ve1ym?file=/src/App.tsx

In TestWrapper you are cloning the Test component and applying your style props to it, which are not being passed down to the element that it's cloning itself. Just returning a cloned element doesn't create a referential equality where doing something to the component will affect the element it is cloning. You would need to give a style prop to Test and pass it down to the element being cloned:
const Test: React.FC<{
style?: React.CSSProperties;
element: React.ReactElement;
}> = ({ element, style }) => {
return React.cloneElement(element, {
style: { ...element.props.style, ...style, color: "red" }
});
};
I made a fork here. Hopefully this helps!

Related

Observe (get sized) control (listen to events) over a nested component in the react and typescript application via the forwardRef function

I have a functional component called MyDivBlock
const MyDivBlock: FC<BoxProps> = ({ }) => {
{getting data...}
return (
<>
<div className='divBlock'>
{data.map((todo: { id: string; title: string }) =>
<div key={todo.id}>{todo.id} {todo.title} </div>)}
</div>
</>
);
};
I use it in such a way that MyDivBlock is nested as a child of
const App: NextPage = () => {
return (
<div>
<Box >
<MyDivBlock key="key0" areaText="DIV1" another="another"/>
</Box>
</div>
)
}
Note that MyDivBlock is nested in Box and MyDivBlock has no ref attribute. This is important because I need to write Box code with no additional requirements for my nested children. And anyone who will use my Box should not think about constraints and ref attributes.
Then I need to get the dimensions of MyDivBlock in the code of Box component, and later attach some event listeners to it, such as scrolling. These dimensions and listeners will be used in the Box component. I wanted to use Ref to control it. That is, the Box will later observe changes in the dimensions and events of MyDivBlock by creating a ref-reference to them
I know that this kind of parent-child relationship architecture is implemented through forwardRef
And here is the Box code:
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
export interface BoxProps extends React.ComponentProps<any> {
children?: Element[];
className: string;
}
export const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const methods = {
show() {
if (childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, function (item) {
console.log(item)})
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
return <div ref={childRef}> {children} </div>
});
ChildWithForwardRef.displayName = 'ChildWithForwardRef';
return (
<div
className={'BoxArea'}>
<button name="ChildComp" onClick={() => childRef.current.show()}>get Width</button>
<ChildWithForwardRef ref={childRef} />
</div>
);
}
export default Box;
The result of pressing the button:
childRef.current is present...
[...]
$$typeof: Symbol(react.element) key: "key0" props: {areaText: 'DIV1', another: 'another'}
[...] Object
offsetWidth = undefined
As you can see from the output, the component is visible through the created ref. I can even make several nested ones and get the same for all of them.
But the problem is that I don't have access to the offsetWidth and other properties.
The other challenge is how can I add the addEventListener?
Because it works in pure Javascript with their objects like Element, Document, Window or any other object that supports events, and I have ReactChildren objects.
Plus I'm using NextJS and TypeScript.
Didn't dive too deep into the problem, but this may be because you are passing the same childRef to both div inside ChildWithForwardRef and to ChildWithForwardRef itself. The latter overwrites the former, so you have the method .show from useImperativeHandle available but not offsetWidth. A quick fix is to rewrite ChildWithForwardRef to use its own ref:
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const ref = useRef<HTMLDivElement>()
const methods = {
show() {
if (ref.current) {
console.log("ref.current is present...");
React.Children.forEach(children, (item) => console.log(item))
console.log("offsetWidth = " + ref.current.offsetWidth);
} else {
console.log("ref.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
// Here ref instead of childRef
return <div ref={ref}> {children} </div>
});
But really I don't quite get why you would need ChildWithForwardRef at all. The code is basically equivalent to this simpler version:
const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const showWidth = () => {
if(childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, item => console.log(item))
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
}
return (
<div className={'BoxArea'}>
<button name="ChildComp" onClick={showWidth}>get Width</button>
<div ref={childRef}>{children}</div>
</div>
);
}
You can't solve this completely with React. I solved it by wrapping the child component, making it take the form of the parent.

How can I use a variable value for class name when using makeStyles?

In my React app, I have a component which takes in some data. Depending on the value of that data, I want to show the component with a different coloured background.
The styles are defined as:
const useStyles = makeStyles((theme) => ({
class1: {
backgroundColor: "red",
},
class2: {
backgroundColor: "pink",
},
}));
The component is:
const MyBox = ({ data }) => {
let classes = useStyles();
let innerClassName;
if (data.value) {
innerClassName = "class1";
} else {
innerClassName = "class2";
}
return (
<div className={innerClassName}>
Content goes here
</div>
);
};
export default MyBox;
However, this gives the component a class of "class1" or "class2", which doesn't get picked up by makeStyles. I also tried <div className={classes.innerClassName}> but then it looks for a class called 'innerClassName' which obviously it can't find.
I think I need to use some kind of variable string within <div className={????}> but I've tried various template literal strings and none of them have worked. What should I be doing?

How to get the result 'toHaveStyle' in testing-library-react?

Testing library react does not catch 'toHaveStyle'.
When I clicked on the 'Content', its children which have a blue color were changed to the red color.
However, in my test, they always have the blue color.
What should I do to solve this problem?
[...]
<Content data-testid={"list-element-content"} id={data.id} toggle={state[data.id - 1]}>
<div>{data.titleUnBold}</div>
<BoldTitle>{data.titleBold}</BoldTitle>
</Content>
[...]
const Content = styled.div`
color: ${ (props) => props.toggle ? "red" : "blue" };
`;
Below the test code:
test("color changed", () => {
const mockState = [false];
const mockSwitchGuide = jest.fn();
const { getAllByTestId, rerender } = render(
<GuideListPresenter
data={mockListData}
state={mockState}
onClick={mockSwitchGuide}
/>
);
act(() => {
fireEvent.change(getAllByTestId("list-element-content")[0],{
target: {toggle: true},
});
});
rerender(
<GuideListPresenter data={mockListData} state={mockState} onClick={mockSwitchGuide} />
);
expect(getAllByTestId("list-element-content")[0].toggle).toEqual(true); // success
expect(getAllByTestId("list-element-content")[0]).toHaveStyle("color: red"); // failed
})
To test the style of your component, you can get it directly from the html document, and see precisely what style is used for a specific element.
In your example, you would do something like below:
it('should change color to red on toggle click', () => {
const { container, getAllByTestId } = render(
<GuideListPresenter
data={mockListData}
state={mockState}
onClick={mockSwitchGuide}
/>
);
// Replace <YOUR_DIV_ID> by your component's id
let contentDiv = document.getElementById('<YOUR_DIV_ID>');
let style = window.getComputedStyle(contentDiv[0]);
expect(style.color).toBe('blue'); // Sometimes, only rgb style type is read here. See the rgb that corresponds to your color if need be.
act(() => {
fireEvent.change(getAllByTestId("list-element-content")[0],{
target: {toggle: true},
});
});
// Get the updated contentDiv
contentDiv = document.getElementsByClassName('<YOUR_DIV_CLASS_NAME>');
style = window.getComputedStyle(contentDiv[0]);
expect(style.color).toBe('red');
expect(getAllByTestId("list-element-content")[0].toggle).toEqual(true);
}
Here, to get the style of your element, I am using the element's id. However, it could also work with the element's className, and using the method document.getElementByClassName('YOUR_DIV_CLASS_NAME') instead. Note that the given name here should be unique, either with the id technique, or the className.

Unable to use "this" in makeStyle

I would like to create a dynamic background image depending on my props. So for that I wanted to make a react style and give it the picture stored in my state but I can't use this.state.pictures in it and I don't know why.
class DisplayArtist extends Component {
state = {
name : this.props.Info.artists.items[0].name,
followers: this.props.Info.artists.items[0].followers.total,
genres: this.props.Info.artists.items[0].genres,
picture: this.props.Info.artists.items[0].images[0]
}
useStyles = makeStyles({
root: {
backgroundImage={this.state.pictures}
}
});
makeStyles is better used in a functional component, rather than a class component.
using makeStyes inside a function component causes the style to be recreated on every render. I don't recommend doing it that way.
The recommended approach is to use inline styles for dynamic background images
e.g. style={{ backgroundImage: artist.images[0] }}
Converting to Functional Component
const DisplayArtist = (props) => {
const [ artist, setArtist ] = useState(null);
useEffect(() => {
//do your own checks on the props here.
const { name, total, genres, images } = props.Info.artists.items[0]
setArtist({name, total, genres, images});
},[props])
return ( <div style={{ width: '200px', height:'200px', backgroundImage: artist.images[0] }} /> )
}
export default DisplayArtist
You can use Functional Component and use useStyles and pass the state you want to it
const useStyles = makeStyles(() => ({
root: {
backgroundImage:({ picture }) => ( picture )
}
}))
function DisplayArtist({Info}) {
const [picture, setPicture] = useState()
const classes = useStyles({picture})
}
Use Higher-order component API Higher-Order-Component-Api
const styles = theme => ({
root: {
backgroundImage={this.state.pictures}
}
});
class DisplayArtist extends Component {
state = {
name : this.props.Info.artists.items[0].name,
followers: this.props.Info.artists.items[0].followers.total,
genres: this.props.Info.artists.items[0].genres,
picture: this.props.Info.artists.items[0].images[0]
}
}
export default withStyles(styles)(DisplayArtist);

Flow - missing type annotation

Pretty new to flow and trying to fix my code to include flow. This is my code at the moment and I've added flow type check and now getting errors so I need to annotate my code properly:
// #flow
import React, { Component } from 'react';
import { Manager, Reference, Popper } from 'react-popper';
import cx from 'classnames';
import css from './Tooltip.css';
import animationsCss from './TooltipAnimations.css';
type Props = {
theme: string,
eventsEnabled: boolean,
}
class Tooltip extends Component<Props> {
static defaultProps = {
theme: 'light',
eventsEnabled: true
};
firstOrderPlacement(placement) {
if (!placement) return null;
return placement.split('-')[0];
}
arrowDirectionClass(firstOrderPlacement) {
switch (firstOrderPlacement) {
case 'right':
return css.arrowLeft;
case 'left':
return css.arrowRight;
case 'top':
return css.arrowDown;
case 'bottom':
return css.arrowUp;
default:
return css.arrowUp;
}
}
render() {
const { placement, className, children, fadeIn, theme, eventsEnabled } = this.props;
return (
<Popper placement={placement} eventsEnabled={eventsEnabled}>
{({ ref, style, placement }) => {
const firstOrderPlacement = this.firstOrderPlacement(placement);
const arrowDirectionClass = this.arrowDirectionClass(firstOrderPlacement);
const subContainerStyle = {
display: 'flex',
flexDirection:
firstOrderPlacement === 'top' || firstOrderPlacement === 'bottom' ? 'column' : 'row',
};
const childrenContainerClassName = cx(
css.childrenContainer,
css.wrapper,
theme === "dark" ? css.dark : css.light
);
const content = <div className={childrenContainerClassName}>{children}</div>;
const subContainerClassName = fadeIn ? cx(animationsCss.fadeIn, className) : className;
return (
<div
ref={ref}
className={cx(css.container, css.mobileTooltip)}
style={style}
data-placement={placement}
>
<div className={subContainerClassName} style={subContainerStyle}>
{(firstOrderPlacement === 'left' || firstOrderPlacement === 'top') && content}
<div>
<div className={cx(css.arrow, arrowDirectionClass)} />
</div>
{(firstOrderPlacement === 'right' || firstOrderPlacement === 'bottom') && content}
</div>
</div>
);
}}
</Popper>
);
}
}
export { Manager as TooltipManager, Reference as TooltipReference, Tooltip };
I believe I need to add these to my props. Are these correct?
placement: string,
className?: string,
children?: any,
fadeIn: any,
I'm missing type annotation for these two which I'm not sure how to proceed:
firstOrderPlacement(placement) {..}
arrowDirectionClass(firstOrderPlacement) {..}
Any help?
Annotate Props like:
type Props = {
...
placement: string,
className?: string,
children?: any,
fadeIn: any,
...
}
Placement parameter is string firstOrderPlacement(placement) {..} and return value of function is null or string, so you can use maybe type for annotation:
firstOrderPlacement(placement: string): ?string => {...}
Or with union type because maybe type covers undefined.
type FirstOrder = string | null;
Result of firstOrderPlacement function is parameter of arrowDirectionClass. So type of parameter:
arrowDirectionClass(firstOrderPlacement: ?string): string => {...}
Or:
arrowDirectionClass(firstOrderPlacement: FirstOrder): string => {...}
Are these correct?
Try them and find out!
Keep in mind, though, that using any is a shortcut that basically just turns off typing. For children where you are expecting react elements you should usually just use React.node. This will work if you basically want arbitrary nested DOM-like content like react elements, arrays of elements, strings of text, etc. fadeIn looks like it's probably a string, but it's kind of hard to tell without seeing where it comes from.
I'm missing type annotation for these two which I'm not sure how to proceed:
Flow wants you to type the arguments to the functions. Something like:
firstOrderPlacement(placement: string) {
or whatever.

Resources