Why can I not use string interpolation for classNames in React? - reactjs

Here's my component:
export interface ButtonProps {
classes?: string[];
}
const Button: FC<ButtonProps> = (props) => {
const classes = props.classes || [];
return (
<button className={`button ${...classes}`}>
{children}
</button>
)
};
export default Button;
I'm getting an error - "property classes does not exist...".
It would be great if someone could explain to me why the above code doesn't work.
Many thanks.

One problem is that classes is an array of strings, not an object, so spreading it into a template literal doesn't make sense. Join by whitespace instead:
<button className={`button ${classes.join(' ')}`}>
Spread can only be used in:
argument lists: fn(...args)
array literals: [...items]
object literals: { ...props }
But it can't be used wherever a generic expression is expected.

you can also use clsx
const { foo, bar, baz } = classes
const style = clsx(foo, bar, baz)
<button className={`button ${style}`}>
you can also use as condition
const style = clsx({
[classes.root] : true, //always apply
[classes.open] : open //only when open === true
})

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 fix this Unit Test?

I'm fairly new to unit testing my .tsx files and I am currently having trouble testing this (sorry if the format is off)
//this is Banner.tsx
import React, {useCallback} from "react";
type Properties = {
close: () => void;
text: string;
const Banner: React.FC<Properties> = ({close, text}) => {
const onClick = useCallback(() => {
close();},
[close, text]);
return (
<div className = "BannerBox">
<div className = "banner">
<span className = "popup"> onClick={onClick}[x]
</span>
{text}
</div>
</div>
);
};
export default Banner;
//this is App.tsx
import Banner from "./Components/Banner";
function App(): JSX.Element {
const [isOpen, setIsOpen]=useState(false);
const toggleBanner = () => {
SetIsOpen(!isOpen);
};
return (
<div>
<input type = "button"
value = "popup"
onClick={toggleBanner}/>
<p>hi</p>
{isOpen && <Banner text = {"hello"} close={() => isOpen(false)}/>}
</div>
export default App;
this is what i have so far
//Banner.test.tsx
test("Check that all type Properties are being used", () => {
render(<Banner />);
})
it gives this error -> "type {} is missing the following properties from type Banner: close and text"
"type {} is missing the following properties from type Banner: close and text"
Read this error message carefully.
Banner is a functional component. That means it's a function that that takes it's props as an object. And it's typed to receive two props, close and text. These props are required.
But you are providing no props in your test. Since the props argument is always an object, and you have no props, then the props argument is an empty object.
So now that error tells you that your function expects an object, but the one you provided is missing the close and text props.
You need to satisfy the required props of your component. Whether you are in a test or not, the contract of those types must must be fulfilled.
That means you want something like this:
//Banner.test.tsx
test("Check that all type Properties are being used", () => {
render(<Banner text="Hello, World!" close={() => null} />);
})
In additional there several syntax errors in your components. And your code will be much easier to understand if you use proper indenting to inform you of the structure of your code.

Type properties so that they are mutually exclusive

How should I type my properties, so that either buttonLink or clickHandler, but not both, could be passed to my component at the same time?
export type ConfirmationProps = {
buttonLink?: string
clickHandler?: OnClick
}
export const Confirmation: FC<ConfirmationProps> = ({
buttonLink,
clickHandler,
}): JSX.Element => {
return (
{clickHandler && (
<Button onClick={clickHandler}>
Button
</Button>
)}
{buttonLink && (
<Link href={buttonLink}>
Link
</Link>
)}
)
}
Yes, it can be achieved by declaring possible variants and using never type:
export type ConfirmationProps = ({
buttonLink: string
clickHandler?: never
} | {
buttonLink?: never
clickHandler: OnClick
});
For your information, if there are other props which are common and acceptable for both variants then you can use the same solution, but firstly declare the common part and then use the & intersection literal:
export type ConfirmationProps = {
commonprop1: number
commonprop2: number
} & ({
buttonLink: string
clickHandler?: never
} | {
buttonLink?: never
clickHandler: OnClick
});

Trying to apply a class to a styled-component for an onclick event

In using standard JSX and CSS, I can add a className attribute with some logic to add a class name based on a boolean value, but when using styled-components, this doesn't appear to be as easy. This is what I have at the moment:
Menu.tsx
interface IMenuProps {
showMenu: boolean;
menuToggle: () => void;
}
const Menu: React.FC<IMenuProps> = ({ showMenu, menuToggle }) => {
return (
<MenuWrapper onClick={menuToggle} {showMenu ? "showMenu" : ""}>
...
At the moment, there's a red line under the showMenu within the ternary statement.
'...' expected.
Hopefully you can see what I'm trying to do here.
You forgot to add className.
const Menu: React.FC<IMenuProps> = ({ showMenu, menuToggle }) => {
return (
<MenuWrapper onClick={menuToggle} className={showMenu ? "showMenu" : ""}>
BTW:
If there isn't any logic inside your component you can write it as
const Example: FC = () => (
<div>
<h1>Example</h1>
</div>
);
Have you written types for your styled MenuWrapper?
If not, try something like this:
const MenuWrapper = styled.div<{showMenu?:boolean}>

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