JS/TS: object destructuring - javascript

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.

Related

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

how to declare the type in my functional component?

I wanted to declare the type in my component this way, I already saw that this is possible using classes, but how could I do it using functional components? Without having to export the interface? I have already researched a lot and found nothing about it.
const Elem1 = forwardRef((props : any, ref : { open()? : void }) => {
useImperativeHandle(ref, () => ({
open: () => console.log('opened')
}))
....
})
const Elem2 = () => {
const elem1Ref = useRef<Elem1>()
elem1Ref?.current?.open()
....
}
If i got right, you should use the type parameters provided by the TS Generics definition of forwardRef. Like this 👇
const Elem1 = forwardRef<YourRefType, YourPropsType>((props, ref) => {
...
}
I've been researching and from what they said, it is not possible to declare the type of a functional component, just by exporting the component's separate interface, thanks anyway!

React Native (or React) listen to the style changes for the dark mode in separate ts modules

Goal: Implement dark mode in react native application.
A little introduction to the system:
File structure:
Profile.ts
ProfileCss.ts
constants.ts
In my application in order to separate styles from components, I moved them into .ts files and exported as modules.
Example of StyleCss.ts:
import {StyleSheet} from 'react-native';
import {Window, FixedCSS, hsize, wsize} from '../../entities/constants';
export default StyleSheet.create({
loading: {
backgroundColor: FixedCSS.Colors.MainBackgroundColor,
}
...
});
All style-related constant values stored in constants.ts such as Colors, LineHeights, and etc. Also constants.ts contains a class called FixedCSS which has static properties:
export class FixedCSS {
static isDark = true;
static Colors = {
// Main Colors
RootColor: FixedCSS.isDark ? '#FF6666' : '#FF6666',
MainBackgroundColor: FixedCSS.isDark ? '#050505' : '#FFFFFF',
...
}
...
}
isDark property of FixedCSS is supposed to be changed with the help of Switch component in Profile.ts
<Switch
value={darkMode}
onValueChange={value => {
this.setState({darkMode: value});
}}
/>
The problem: How can I change isDark property in FixedCSS class so that it will affect the appearance of components. The most problematic aspect is that StyleCSS.ts files do not reflect to the change of isDark property in FixedCSS class, as they are exported only once when js bundle gets created.
Changing a static property of a class is not going to trigger re-renders the way that it's supposed to. This needs to make use of state somehow. I recommend a theme Context. You could use one context or have separate contexts for controlling isDarkMode and providing the CSS, which is what I am doing here.
In order to export a value only once but have that value stay current, the exported value can be a function rather than a constant. I am making the fixed CSS be a function of isDark and the individual component styles be a function of FixedCSS.
constants.ts
export const createFixedCSS = (isDark: boolean) => ({
isDark,
Colors: {
// Main Colors
RootColor: isDark ? "#FF6666" : "#FF6666",
MainBackgroundColor: isDark ? "#050505" : "#FFFFFF"
...
}
...
});
export type FixedCSS = ReturnType<typeof createFixedCSS>;
export const IS_DARK_DEFAULT = false;
cssContext.ts
// context to provide the FixedCSS object, which also includes isDark
const FixedCSSContext = createContext(createFixedCSS(IS_DARK_DEFAULT));
// context to provide a function for switching modes
// type Dispatch<SetStateAction<T>> allows for setting based on previous value
type SetModeContextValue = Dispatch<SetStateAction<boolean>>;
// needs an initial value that fits the type signature
const SetModeContext = createContext<SetModeContextValue>(() => {
throw new Error(
"cannot set `isDark` outside of a `DarkModeContext` Provider"
);
});
// combine both contexts into one Provider component
export const CSSProvider: FC<{}> = ({ children }) => {
// store the isDark state
const [isDark, setIsDark] = useState(IS_DARK_DEFAULT);
// update the FixedCSS when isDark changes
// I'm not sure if memoizing is actually necessary here?
const FixedCSS = useMemo(() => createFixedCSS(isDark), [isDark]);
return (
<SetModeContext.Provider value={setIsDark}>
<FixedCSSContext.Provider value={FixedCSS}>
{children}
</FixedCSSContext.Provider>
</SetModeContext.Provider>
);
};
// I prefer to export the hooks rather than the contexts themselves
export const useFixedCSS = () => useContext(FixedCSSContext);
export const useSetMode = () => useContext(SetModeContext);
// helper hook for `StyleCss.ts` files
// takes the makeStyles factory function and turns it into a style object by accessing the FixedCSS context
export const useStyleFactory = <T extends StyleSheet.NamedStyles<T>>(
factory: (css: FixedCSS) => T
): T => {
const FixedCSS = useFixedCSS();
return useMemo(() => factory(FixedCSS), [FixedCSS, factory]);
};
ProfileCss.ts
export const makeStyles = (FixedCSS: FixedCSS) => StyleSheet.create({
loading: {
backgroundColor: FixedCSS.Colors.MainBackgroundColor,
}
});
Profile.ts
const DarkModeToggle = () => {
// access isDark from global FixedCSS
const {isDark} = useFixedCSS();
// access update callback
const setIsDark = useSetMode();
return (
<Switch
value={isDark}
onValueChange={setIsDark}
/>
)
}
const Profile = () => {
// pass the imported `makeStyles` to the hook and get stateful styles object
const styles = useStyleFactory(makeStyles);
return (
<View>
<DarkModeToggle/>
<View style={styles.loading}/>
</View>
);
};
CodeSandbox Demo (all in one file)

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

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

Using branch from recompose

I am refactoring a stateless functional component to use branch and renderComponent from recompose.
The original component looks like this:
const Icon = props => {
const { type, name } = props
let res
if (type === 'font') {
return (<FontIcon name={name} />)
} else if (type === 'svg') {
res = (<SvgIcon glyph={name} />)
}
return res
}
The component with branch looks like this:
const isFont = props => {
const { type } = props
return type === 'font'
}
const FontIconHoC = renderComponent(FontIcon)
const SvgIconHoC = renderComponent(SvgIcon)
const Icon = branch(isFont, FontIconHoC, SvgIconHoC)
Icon.propTypes = {
type: string,
name: string
}
export default Icon
I try and render the component using:
<Icon name='crosshairs' type='font' />
The resulting error looks like this:
invariant.js:44Uncaught Error: Icon(...): A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object.
branch returns a HOC, which accepts a component and return a component, so branch(...) is a HOC and branch(...)(...) is a component.
In your case, because Icon is not a component but a HOC, so React can't render it. To fix it, you can move SvgIcon out from branch's arguments and apply it to the HOC returned by branch(...), ex:
const Icon = branch(
isFont,
FontIconHoC,
a => a
)(SvgIcon)
We apply an identity function (a => a) to the third argument of branch. You can think of the identity function is also a HOC, which basically just return the component it gets and does nothing more.
Because this pattern is used very often, so the third argument of branch is default to the identity function. As a result, we can omit it and make our code simpler:
const Icon = branch(
isFont,
FontIconHoC
)(SvgIcon)
I've created a jsfiddle for these code. You can try it here.
You can also just use an if statement instead of branch. Consider that you just had some difficulties doing what an if statement does.
Maybe time to reconsider that library ?

Categories

Resources