Pass a Component with props, inside a Higher-Order-Component (HOC) - javascript

I refer to this example of creating a React Higher Order Component, taken from this article.
I am struggling to fully comprehend & make use of this HOC.
interface PopupOnHoverPropType {
hoverDisplay: string;
}
const WithPopupOnHover = <P extends object>(BaseComponent: React.FC<P>): React.FC<P & PopupOnHoverPropType> => ({ hoverDisplay, ...props }: PopupOnHoverPropType) => {
const [displayMsg, setDisplayMsg] = useState<boolean>(false);
const toggleDisplayMsg = () => {
setDisplayMsg(displayMsg);
};
return (
<div onMouseEnter={() => setDisplayMsg(true)}>
<div className={`${hoverDisplay} popup`} onClick={toggleDisplayMsg}/>
<BaseComponent {...props as P} />
</div>
)
};
Here is what my understanding of the above code is:
We define a HOC by the name of WithPopupOnHover,
which takes a React component (BaseComponent).
The BaseComponent comes with its props (referred as P)
which returns a new React component,
which takes props P & the props within PopupOnHoverPropType.
How do I use this HOC ?
The following attempt gives a typescript error that too many arguments are passed in:
const enhanced = WithPopupOnHover(Modal);
const m = Enhanced(a,b,c, "up");
The Modal React component has the following props structure:
const Modal: React.FC<{
a: string;
b(): void;
c(locationData: customType): void;
}> = { ... }

Looks like you just forgot the object brackets. React components all take a single props object argument.
const Enhanced = WithPopupOnHover(Modal);
const m = Enhanced({ a, b, c });

Related

Passing arguments to the elements between tags

the variable marked in the image is coming from the Formik Component
what is the name of this approach of passing an argument to the elements inside the tags (that's why the title is not clear so much, due to the limitation of knowing the name of the approach), and would appreciate an example of it.
I use this pattern in my app, check it out:
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { getGeo } from '#/data/geoLocation/useGeoLocation';
import { GeoLocation } from 'types/GeoLocation';
interface ChildrenParams {
currentLanguage: string;
handleChangeLanguage: (lang: string, cb?: () => void) => void;
languages: string[];
}
export default function LocaleToggle({
children,
}: {
children: (params: ChildrenParams) => JSX.Element;
}): JSX.Element {
const { i18n } = useTranslation();
const languages: string[] = React.useMemo(
() => Object.keys(i18n.services.resourceStore.data),
[i18n],
);
// Set stored language on mount.
useEffect(() => {
const region = i18n.language;
if (!languages.includes(region)) {
getGeo().then((ip: GeoLocation) => i18n.changeLanguage(ip.country_code));
return;
}
i18n.changeLanguage(region.toLowerCase());
}, [i18n, languages]);
const handleChangeLanguage = useCallback(
(lang: string, cb?: () => void) => {
i18n.changeLanguage(lang.toLocaleLowerCase());
if (cb) cb();
},
[i18n],
);
return (
<>
{children({
currentLanguage: i18n.resolvedLanguage,
handleChangeLanguage,
languages,
})}
</>
);
}
Then, I can use it like this, in the formik way:
<LocaleToggle>
{({ languages }) => (
<Select onChange={handleChangeLanguageSelect} value={newLang}>
{languages.map((l) => (
<option key={l} value={l}>
{getLanguage(l)}
</option>
))}
</Select>
)}
</LocaleToggle>
I'm not sure about how this is called, but I think it's named 'render prop pattern'
The JSX documentation calls this functions as children:
Functions as Children
Normally, JavaScript expressions inserted in JSX will evaluate to a string, a React element, or a list of those things. However, props.children works just like any other prop in that it can pass any sort of data, not just the sorts that React knows how to render. For example, if you have a custom component, you could have it take a callback as props.children:
// Calls the children callback numTimes to produce a repeated component
function Repeat(props) {
let items = [];
for (let i = 0; i < props.numTimes; i++) {
items.push(props.children(i));
}
return <div>{items}</div>;
}
function ListOfTenThings() {
return (
<Repeat numTimes={10}>
{(index) => <div key={index}>This is item {index} in the list</div>}
</Repeat>
);
}
Children passed to a custom component can be anything, as long as that component transforms them into something React can understand before rendering. This usage is not common, but it works if you want to stretch what JSX is capable of.
// Calls the children callback numTimes to produce a repeated component
function Repeat(props) {
let items = [];
for (let i = 0; i < props.numTimes; i++) {
items.push(props.children(i));
}
return <div>{items}</div>;
}
function ListOfTenThings() {
return (
<Repeat numTimes={10}>
{(index) => <div key={index}>This is item {index} in the list</div>}
</Repeat>
);
}
ReactDOM.createRoot(document.querySelector("#root")).render(<ListOfTenThings />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>

How to define generic HOC with additional interface to add to props

I need to create a generic HOC with will accpect an interface which will be added to component props.
I have implemented following function, but it requires two arguments instead of one. I want second argument to be taken from the Component that it's passed into the function.
export const withMoreProps = <NewProps, Props>(WrappedComponent: React.FC<Props>): React.FC<Props & NewProps> => {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
const ComponentWithMoreProps = (props: Props & NewProps) => <WrappedComponent {...props} />;
ComponentWithMoreProps.displayName = `withMoreProps(${displayName})`;
return ComponentWithMoreProps;
};
Currently when I try to use this:
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
export const Button2 = withMoreProps<{ newProperty: string }>(Button);
I get this error message
Expected 2 type arguments, but got 1.
It should work like styled-components, where you can define only additional props.
export const StyledButton = styled(Button)<{ withPadding?: boolean }>`
padding: ${({ withPadding }) => (withPadding ? '8px' : 0)};
`;
EDIT:
This is simplified version of HOC I have created in application. The real HOC is much more complex and does other stuff, but for sake of simplification I made this example to focus only on the problem I run into.
In general, you want to use the infer keyword. You can read more about it here, but in short you can think of it as a helper to "extract" a type out of a generic type.
Let's define a type that extract the prop type out of a react component.
type InferComponentProps<T> = T extends React.FC<infer R> ? R : never;
example on what it does:
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
type ButtonProps = InferComponentProps<typeof Button>; // hover over ButtonProps, see {color: string}
Now that we have this "helper type", we can move on to implement what you want - but we do run into an issue. When calling a generic function in typescript, you can't specify some of the types, and some no.
You either specify all the concrete types matching for this function call, or specify none, and let Typescript figure out the types.
function genericFunction<T1, T2>(t1: T2) {
//...
}
const a = genericFunction('foo') //ok
const b = genericFunction<number, string>('foo') //ok
const c = genericFunction<string>('foo') //error
You can track the typescript issue here.
So to solve this we need to do a small change to your code and do a function that returns a function that returns the new component. If you notice, it's exactly how styled works also, as using tagged template literals is really a function call. So there are 2 function calls in the styled components code you posted above.
so the final code looks something like this:
export const withMoreProps =
<C extends React.FC<any>>(WrappedComponent: C) =>
<NewProps extends Object>(): React.FC<InferComponentProps<C> & NewProps> => {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
//need to re-type the component
let WrappedComponentNew = WrappedComponent as React.FC<InferComponentProps<C> & NewProps>;
const ComponentWithMoreProps = (props: InferComponentProps<C> & NewProps) => <WrappedComponentNew {...props} />;
ComponentWithMoreProps.displayName = `withMoreProps(${displayName})`;
return ComponentWithMoreProps;
};
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
export const Button2 = withMoreProps(Button)<{ newProperty: string }>(); //Notice the function call at the end
If you just want a generic way to add more props to a component, you don't need the overhead of a HOC for this. You can easily achieve this using the rest and spread operator to pass on the props. (I use color on the HOC here unlike OP's example where it's on the main button, it's just a nice example)
const ColoredButton = ({color, ...other}) => <Button {...other, style: {color}/>
It's maybe slightly more code than the HOC version that basically handles passing on ...other for you. However in return:
You don't get a weird display name (withMoreProps(Button) which could be any props). Instead it will just use the function name like any other React component (e.g. ColoredButton). You'd rather have the latter while debugging.
The resulting component is as flexible as any other React component. Just add logic to the function body if you find you need it. But with HOCs there is no function body. You could add more than 1 HOC but that gets unwieldy very quickly.
Similarly, your issue with declaring the types simply goes away. It works exactly the same like the main button type.
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
export const Button2 = ({ newProperty: string, ...other }) => <Button {...other, newProperty}/>
// Original for comparison.
export const Button3 = withMoreProps<{ newProperty: string }>(Button);

JS/TS: object destructuring

i've a variable named props.
The type extends from VariantTheme, VariantSize, VariantGradient, & React.DOMAttributes<HTMLOrSVGElement>
Then i want to make one more variable, let say htmlProps.
I want to fill the htmlProps from props.
I don't want it filled with a foreign props other than listed on React.DOMAttributes<HTMLOrSVGElement>.
Then spread the filtered prop into react component.
Here the not working code:
export interface Props
extends
VariantTheme,
VariantSize,
VariantGradient,
React.DOMAttributes<HTMLOrSVGElement>
{
tag? : keyof JSX.IntrinsicElements
classes? : string[]
}
export default function Element(props: Props) {
const elmStyles = styles.useStyles();
// themes:
const variTheme = useVariantTheme(props);
const variSize = useVariantSize(props);
const variGradient = useVariantGradient(props);
const Tag = (props.tag ?? 'div');
const htmlProps = props as React.HTMLAttributes<HTMLOrSVGElement>; // not working! Still containing strange props from VariantTheme, VariantSize, tag, classes, ...
return (
<Tag {...htmlProps}
className={[
elmStyles.main,
// themes:
variTheme.class,
variSize.class,
variGradient.class,
...(props.classes ?? []),
].filter((c) => !!c).join(' ')}
>
{(props as React.PropsWithChildren<Props>)?.children}
</Tag>
);
};
Do you have an idea to fix my code?
#spender already gave you the answer. Typescript only exists at compile time and what you are really doing is telling the compiler that props is now of type React.HTMLAttributes<HTMLOrSVGElement>, but you are still responsible on how the props are composed. In sum typescript does not alter how your javascript code works, it only helps you find mistakes at compile time.
One solution for your use case is to have htmlProps as a property of props and then spread these to Tag.
export interface Props
extends
VariantTheme,
VariantSize,
VariantGradient
{
tag? : keyof JSX.IntrinsicElements
classes? : string[],
htmlProps: React.DOMAttributes<HTMLOrSVGElement>, // dom attributes is now a property in props
}
export default function Element(props: Props) {
const elmStyles = styles.useStyles();
// themes:
const variTheme = useVariantTheme(props);
const variSize = useVariantSize(props);
const variGradient = useVariantGradient(props);
const Tag = (props.tag ?? 'div');
return (
<Tag {...props.htmlProps} // you can now pass the desired props to tag
className={[
elmStyles.main,
// themes:
variTheme.class,
variSize.class,
variGradient.class,
...(props.classes ?? []),
].filter((c) => !!c).join(' ')}
>
{(props as React.PropsWithChildren<Props>)?.children}
</Tag>
);
};
But #spender's suggestion is a better approach.
Don’t forget to update your hooks to accommodate this change.

How to divide business logic in one react component?

i use react.js to build my spa app.
I use functional style to make my components.
As the business logic gonna bigger, there are inevitably many functions.
So i tried to divide in to multiple components. Because it's hard to put many codes in one file even if it is just a 1 component.
However, this also has obvious limitations. In the case of complex components, there are a large number of event callback functions in addition to the functions directly called by the user.
Depending on the size of the component, it is sometimes difficult to write all the logic in one jsx file, so I want to divide the code into different files as needed. (Like c# partial class)
However, this is not easy. As an example, let's assume that the callback functions are made external functions of other files and imported into this component jsx file and used. But it seems that the component states, props information and the dispatch function also should be passed as parameters to the function. This seems hassle but except this, i have no idea a way to access this component's states, props, dispatch function from a function in another file.)
//For example of callback function
const onHoldButtonClicked = (state, props, dispatch, event) =>
{
~~~
}
//For example of normal function
const updateValidUser = (state, props, dispatch, userInfo, data) =>
{
let id = userInfo.id;
if(id == data.passID)
{
if(props.valid == 10)
dispatch({action: 'ChangeUser', user: id});
}
}
In React, how to divide logic(functions) when the logic gonna bigger in one component? (In general case)
Even if it is divided into several components, a big component inevitably has many functions.
I would recommend to extract logic into hooks and place these hooks into their own files.
hook.js
const useAwesomeHook = () => {
const [someState, setSomeState] = useState("default");
const myCoolFunction = useCallback(() => {
console.log('do smth cool', someState);
}, [someState]);
return myCoolFunction;
};
export default useAwesomeHook;
main.js
import useAwesomeHook from './hook';
const Main = ({ someProperty }) => {
const myCoolFunction = useAwesomeHook(someProperty);
return <button onClick={myCoolFunction}>Click me</button>;
};
Here is an example for logic and business and component separation.
The separation makes your code testable, atomic, maintainable, readable and SRP(single responsibility rule )
// Presentational component
const QuantitySelector = () => {
const { onClickPlus, onClickMinus, state } = useQuantitySelector();
const { message, value } = state;
return (
<div className="quantity-selector">
<button onClick={onClickMinus} className="button">
-
</button>
<div className="number">{value}</div>
<button onClick={onClickPlus} className="button">
+
</button>
<div className="message">{message}</div>
</div>
);
};
export default QuantitySelector;
and below code is the above component logic
import { useState } from "react";
// Business logic. Pure, testable, atomic functions
const increase = (prevValue, max) => {
return {
value: prevValue < max ? prevValue + 1 : prevValue,
message: prevValue < max ? "" : "Max!"
};
};
const decrease = (prevValue, min) => {
return {
value: prevValue > min ? prevValue - 1 : prevValue,
message: prevValue > min ? "" : "Min!"
};
};
// Implementation/framework logic. Encapsulating state and effects here
const useQuantitySelector = () => {
const [state, setState] = useState({
value: 0,
message: ""
});
const onClickPlus = () => {
setState(increase(state.value, 10));
};
const onClickMinus = () => {
setState(decrease(state.value, 0));
};
return { onClickPlus, onClickMinus, state };
};
export default useQuantitySelector;
I separate components into the logic and UI by function composition.The idea came from recompse which I used before hooks came into react.
you can add two helper functions.
first one is compose you can learn more about it here:
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
export default compose;
the second one is withHooks:
import React from 'react';
export default (hooks) =>
(WrappedComponent) =>
(props) => {
const hookProps = hooks(props);
return (
<WrappedComponent
{...props}
{...hookProps}
/>
);
}
with these two functions, you can put your logic in the hooks file and pass the as props to your UI with compose file you can see sandbox example here
what I usually do is create a folder for the big component, in the folder I create a functions file and put functions with state passed and other params necessary . as simple as that .
export const increment=(count,setCount)=>{...}
.
.
and in your component
import{increment,....} from './functions'
const Component=(props)=>{
const [count,setCount]=useState(1)
return <div>
<button onClick={e=>increment(count,setCount)}> count ={count}</button>
</div>
}

React functional component default props vs default parameters

In a React functional component, which is the better approach to set default props, using Component.defaultProps, or using the default parameters on the function definition, examples:
Default props:
const Component = ({ prop1, prop2 }) => (
<div></div>
)
Component.defaultProps = {
prop1: false,
prop2: 'My Prop',
}
Default parameters:
const Component = ({ prop1 = false, prop2 = 'My Prop' }) => (
<div></div>
)
defaultProps on functional components will eventually be deprecated (as per Dan Abramov, one of the core team), so for future-proofing it's worth using default parameters.
In general (ES6), the second way is better.
In specific (in React context), the first is better since it is a main phase in the component lifecycle, namely, the initialization phase.
Remember, ReactJS was invented before ES6.
First one can cause some hard-to-debug performance problems, especially if you are using redux.
If you are using objects or lists or functions, those will be new objects on every render.
This can be bad if you have complex components that check the component idenitity to see if rerendering should be done.
const Component = ({ prop1 = {my:'prop'}, prop2 = ['My Prop'], prop3 = ()=>{} }) => {(
<div>Hello</div>
)}
Now that works fine, but if you have more complex component and state, such as react-redux connected components with database connection and/or react useffect hooks, and component state, this can cause a lot of rerending.
It is generally better practice to have default prop objects created separately, eg.
const Component = ({prop1, prop2, prop3 }) => (
<div>Hello</div>
)
Component.defaultProps = {
prop1: {my:'prop'},
prop2: ['My Prop'],
prop3: ()=>{}
}
or
const defaultProps = {
prop1: {my:'prop'},
prop2: ['My Prop'],
prop3: ()=>{}
}
const Component = ({
prop1 = defaultProps.prop1,
prop2 = defaultProps.prop2
prop3 = defaultProps.prop3
}) => (
<div>Hello</div>
)
Shameless Plug here, I'm the author of with-default-props.
If you are a TypeScript user, with-default-props might help you, which uses higher order function to provide correct component definition with defaultProps given.
Eg.
import { withDefaultProps } from 'with-default-props'
type Props = {
text: string;
onClick: () => void;
};
function Component(props: Props) {
return <div onClick={props.onClick}>{props.text}</div>;
}
// `onClick` is optional now.
const Wrapped = withDefaultProps(Component, { onClick: () => {} })
function App1() {
// ✅
return <Wrapped text="hello"></Wrapped>
}
function App2() {
// ✅
return <Wrapped text="hello" onClick={() => {}}></Wrapped>
}
function App3() {
// ❌
// Error: `text` is missing!
return <Wrapped onClick={() => {}}></Wrapped>
}
I don't know if is the best way but it works :)
export interface ButtonProps {
children: ReactNode;
type?: 'button' | 'submit';
}
const Button: React.FC<ButtonProps> = ({ children, type = 'button' }) => {
return (
<button type={type}
>
{children}
</button>
);
};
Here is the official announcement regarding the deprecation of the defaultProps.
https://github.com/reactjs/rfcs/pull/107
Even maybe you ask, why not use sth like below code with props || value instead of defaultProps :
class SomeComponent extends React.Component {
render() {
let data = this.props.data || {foo: 'bar'}
return (
<div>rendered</div>
)
}
}
// SomeComponent.defaultProps = {
// data: {foo: 'bar'}
// };
ReactDOM.render(
<AddAddressComponent />,
document.getElementById('app')
)
But remember defaultProps make code more readable , specially if you have more props and controlling them with || operator could make your code looks ugly
you can use a destructured approach, example :
const { inputFormat = 'dd/mm/yyyy', label = 'Default Text', ...restProps } = props;

Categories

Resources