How to create Bootstrap-like shorthand props for React components? - javascript

What is the best way to achieve this behavior along with React + TypeScript?
import { Button, Card } from 'src/components';
const Page = () => (
<div>
<Card mx3 p3 flex justifyContentEnd>
/* Card content */
</Card>
<Button my2 mx3>
Login
</Button>
</div>
);
For instance, mx3 will add 16px margin horizontally, my2 will add 8px margin vertically, etc., similar to how the Bootstrap framework uses classes to apply utility styles easily.
I have looked through a few component libraries with this sort of behavior in order to find a suitable solution; however, I find most do not have strong typing support. Examples are RNUILib, NativeBase, Magnus UI, etc.

You can declare your props like that:
const styles = ['mx3', 'p3', 'flex'] as const
type Styles = Record<typeof styles[number], boolean>;
Then use them like this:
type CardProps = Styles & {
other: 'props here'
}
Now, this should work:
<Card mx3 p3 flex />
You can get applied props like this:
const values = Object.entries(props).map(([key, value]) => value ? key : null).filter(Boolean)

If you see the source code of react-bootstrap, they have mapped the boolean to some CSS class using a util function of classnames package. You can do the same:
...
...
<Component
{...buttonProps}
{...props}
ref={ref}
className={classNames(
className,
prefix,
active && 'active',
variant && `${prefix}-${variant}`,
size && `${prefix}-${size}`,
props.href && props.disabled && 'disabled',
)}
/>
...
...

Related

How to change state of a component when the component is scrolled in view?

Suppose I have a web structure as follows:
<Header />
<Component1 />
<Component2 />
<Component3 />
<Component4 />
Every component has a state as [component1Active, setComponent1Active] = useState(false)
Now, at the start Component1 is in view.
What I want is when component2 is scrolled in view, then I want to setComponent2Active = true and component1 as false.
I tried using useEffect, but it does not work as the states of all components are set as true during loading.
I am using functional components
Please help, any suggestions will be much appreciated.
Thanks in advance
There is a react library on NPM called 'react-in-viewport'
https://github.com/roderickhsiao/react-in-viewport
Here is an example.
import handleViewport from 'react-in-viewport';
const Block = ({ inViewport, forwardedRef } ) => {
const color = inViewport ? '#217ac0' : '#ff9800';
const text = inViewport ? 'In viewport' : 'Not in viewport';
return (
<div className="viewport-block" ref={forwardedRef}>
<h3>{ text }</h3>
<div style={{ width: '400px', height: '300px', background: color }} />
</div>
);
};
const ViewportBlock = handleViewport(Block, /** options: {}, config: {} **/);
const App= () => (
<div>
<div style={{ height: '100vh' }}>
<h2>Scroll down to make component in viewport</h2>
</div>
<ViewportBlock onEnterViewport={() => console.log('enter')} onLeaveViewport={() => console.log('leave')} />
</div>
)
Using const newComponent = handleViewport(<YourComponent/>) you can create any component into one that has the {inViewport, forwardedRef} props, on the containing element of your component set the ref attribute equal to forwardRef then you can use the inViewport prop to determine if it is in view. You can also use the onEnterViewport and onLeaveViewport attributes of the component created by handleViewport() to know the visibility of the component.
You can add the library to your project through either of the cli commands
npm install --save react-in-viewport
yarn add react-in-viewport
You could also do this yourself by accessing the document DOM with a listener on the scroll event and checking scrollHeight of the page and comparing it to the scrollHeights of each component. Using the library makes the implementation much less of a headache.

Adding inline CSS to JS class with React Fragment

I was following this article https://prawira.medium.com/react-conditional-import-conditional-css-import-110cc58e0da6 to conditionally lazy-load CSS stylings in my React app. The method described involves creating a dummy JS class, eg.
import React from 'react';
import './my-styles.css';
const Theme = () => (<React.Fragment></React.Fragment>);
export default Theme;
So that it can be used like this:
const styles = React.lazy(() => import("./theme"))
...
return (
<>
<Suspense fallback={<></>}>
{styles}
</Suspense>
<MyApp />
</>
);
I will need to be able to lazy load many CSS stylesheets, and I don't want to create the same JS wrapper over and over again for each one. Is there a way to combine the wrapper with inline CSS, or perhaps make one wrapper for all of the styles (such that the lazy-loading process still only loads the specific one I want?)
You should create a theme that will decide what styles have to be loaded as in the article you are refferring to:
const ThemeSelector = ({ children }) => {
const CHOSEN_THEME = localStorage.getItem('TYPE_OF_THEME') || TYPE_OF_THEME.DEFAULT;
return (
<>
<React.Suspense fallback={<></>}>
{(CHOSEN_THEME === TYPE_OF_THEME.LIGHT_MODE) && <LightTheme />}
{(CHOSEN_THEME === TYPE_OF_THEME.DARK_MODE) && <DarkTheme />}
</React.Suspense>
{children}
</>
)
}
In the expample you show there is no condition, by the way. It always loads './my-styles.css'

Toggle css in react component is not applied

I have the following component which map over an array and display a set of buttons which they render specific content:
export const Bookings = ({bookings}) => {
const [selectedBooking, setSelectedBooking] = useState(false);
const handleSelectedBooking = (id, destination) => {}
const handleToggleButton = () => {
setSelectedBooking(!selectedBooking)
}
return(
<div>
{
bookings.map(booking => (
<button
className={selectedBooking ? 'selectedBooking' : 'notSelectedBooking'}
onClick={() => {
handleSelectedBooking(booking.id, booking.destination)
handleToggleButton()
}}
>
{booking.destination}
</button>
))
}
</div>
)
}
Where I have these styles already defined but somehow the styles are not applied, did I miss anything?
You have a typo in your ternary operator. It should be selectedBooking instead of selectedBoking.
Did you import styles file in the current file, where you write your component?
Moreover, I recommend you using modules to set styles in your component. How this works: you name your css file like this MyStyles.module.css (.module is obligatory) and then import in React component file these styles like import styles from './MyStyles.module.css'. Then you set styles in jsx code like this: className={selectedBoking ? styles.selectedBooking : styles.notSelectedBooking}.
This approach makes classnames unique across all the project even though they have the same names in different css files.

Change props depending on breakpoint with SSR support

I'm using material-ui#v5, ie. the alpha branch.
Currently, I have a custom Timeline component which does this:
const CustomTimeline = () => {
const mdDown = useMediaQuery(theme => theme.breakpoints.down("md"));
return (
<Timeline position={mdDown ? "right" : "alternate"}>
{/* some children */}
</Timeline>
);
};
It works mostly as intended, but mobile users may experience layout shift because useMediaQuery is implemented using JS and is client-side only. I would like to seek a CSS implementation equivalence to the above code to work with SSR.
I have thought of the following:
const CustomTimeline = () => {
return (
<Fragment>
<Timeline sx={{ display: { xs: "block", md: "none" } }} position="right">
{/* some children */}
</Timeline>
<Timeline sx={{ display: { xs: "none", md: "block" } }} position="alternate">
{/* some children */}
</Timeline>
</Fragment>
);
};
This will work since the sx prop is converted into emotion styling and embedded in the HTML file, but this will increase the DOM size. Is there a better way to achieve that?
I have experienced the same problem before and I was using Next.js to handle SSR. But it does not matter.
Please first install this package and import it on your root, like App.js
import mediaQuery from 'css-mediaquery';
Then, create this function to pass ThemeProvider of material-ui
const ssrMatchMedia = useCallback(
(query) => {
const deviceType = parser(userAgent).device.type || 'desktop';
return {
matches: mediaQuery.match(query, {
width: deviceType === 'mobile' ? '0px' : '1024px'
})
};
},
[userAgent]
);
You should pass the userAgent!
Then pass ssrMatchMedia to MuiUseMediaQuery
<ThemeProvider
theme={{
...theme,
props: {
...theme.props,
MuiUseMediaQuery: {
ssrMatchMedia
}
}
}}>
This should work. I am not using material-UI v5. Using the old one. MuiUseMediaQuery name might be changed but this approach avoid shifting for me. Let me know if it works.
To avoid first render before useMediaQuery launches
From reactjs docs To fix this, either move that logic to useEffect (if it isn’t necessary for the first render), or delay showing that component until after the client renders (if the HTML looks broken until useLayoutEffect runs).
To exclude a component that needs layout effects from the server-rendered HTML, render it conditionally with showChild && and defer showing it with useEffect(() => { setShowChild(true); }, []). This way, the UI doesn’t appear broken before hydration.

How to style a nested functional component using styled components

I have a button with the following props - variant, loading, and disabled. Plus, I have a button group that accepts buttons as children and gaps them with 20px. Something like this:
Technically speaking, I have two components here. A <Button /> and a <ButtonGroup />. This would be achievable by writing:
const Button = styled.button`
// css implementation
:disabled {
opacity: 0.5;
}
`;
const ButtonGroup = styled.button`
// css implementation
${Button} + ${Button} {
margin-inline-start: 20px;
// PS - I'm aware I could use the `gap` property, but I'm not specifically talking about this example, but in general.
}
`;
// Usage
<ButtonGroup>
<Button ... />
<Button ... />
</ButtonGroup>
The last thing and the main issue here is to implement the loading state of the button. Or in general, adding extra logic to the styled component. So the "best" way I know of is to create a new functional component and then wrap it inside another styled. Something like this:
// Button.tsx
const StyledButton = styled.buton`...`;
const Button = (props) => {
return (
<StyledButton className={props.className}>
{props.loading && <LoadingSpinner />}
{props.children}
</StyledButton>
);
}
export default styled(Button)``; // It's needed for for nested styling.
...
// ButtonGroup.tsx
const ButtonGroup = styled.button`
// css implementation
${Button} + ${Button} {
margin-inline-start: 20px;
// PS - I'm aware I could use the `gap` property, but I'm not specifically talking about this example, but in general.
}
`;
It will work, of course, but I'm not sure if it's the best way. Currently, as you can see, I did it by calling styled component -> function component -> styled component for the simplest component. I'm not sure how it will scale with my other components, especially naming these components.
So my question is, is there a better, cleaner, simpler way of doing this?
I don't see a reason for three components, a pattern that works for me is using dot notation:
const StyledButton = styled.button``;
const Button = (props) => {
return (
<StyledButton className={props.className}>
{props.loading && <LoadingSpinner />}
{props.children}
</StyledButton>
);
};
Button.Styled = StyledButton;
export default Button;
In this way, you have a pattern where Component.Styled (if available) will always hold the runtime CSS-in-JS object which you can target.
Then in ButtonGroup implementation:
import { Button } from "#components";
// You can target the className
const ButtonGroup = styled.div`
${Button.Styled} { ... }
`;
// You can apply styles
styled(Button)
// You can use the component
<Button />
// Or extend style etc
<OtherButton as={Button.Styled} .../>

Categories

Resources