Change props depending on breakpoint with SSR support - javascript

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.

Related

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

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',
)}
/>
...
...

React Native check context value on click test case

I have following context
Home.tsx
export const ThemeContext = React.createContext(null)
const Home = () => {
const { width } = Dimensions.get("window")
const [theme, setTheme] = React.useState({
active: 0,
heightOfScrollView: 0,
profileWidth: width * 0.2,
scrolledByTouchingProfile: false
})
const horizontalScrollRef = React.useRef<ScrollView>()
const verticalScrollRef = React.useRef<ScrollView>()
return (
<>
<SafeAreaView style={styles.safeAreaContainer} />
<Header title="Contacts" />
<ThemeContext.Provider value={{ theme, setTheme }}>
In component A, I have a button which changes in the context
const onProfileTouched = (index: number) => {
setTheme({ ...theme, active: index });
};
This leads to an image being active
const ImageCircle = ({ active, uri }: Props) => {
return (
<View
style={
active
? { ...styles.parentView, ...styles.active }
: { ...styles.parentView }
}>
<Image source={uri} width={30} height={30} />
</View>
);
};
Now, I want to write a test case (I haven't written a test case before) that confirms that the state has actually changed or perhaps an active border is added to the image
I added a testId to my button which I used to fire an event
it('changes active on profile clicked', () => {
const { getByTestId } = render(<Home />);
fireEvent.press(getByTestId('HScroll3.button'));
});
Now, I am unsure, how to grab the value of context or change in style so as I can confirm that indeed the component for which the button is pressed is active
I am using import {render, fireEvent} from '#testing-library/react-native' but open to change.
With testing-library you want to check the visual output, not the internal state. This way your tests are much more valuable, because the end user doesn't care if you're using context, state or anything else, they care if the button has the "active" state. So if at some point you'll decide to change your mind and refactor theming completely, your test will still give you value and confidence.
I would suggest to install #testing-library/jest-native, you just have to add this setupFilesAfterEnv": ["#testing-library/jest-native/extend-expect"]
to your Jest config or import that extend-expect file in your test setup file.
Once you have it set up, you're ready to go.
I don't know what's in your styles.active style, but let's assume it's e.g. { borderColor: '#00ffff' }.
Add a testID prop to the View in ImageCircle component, e.g. testID="imageCircleView". Then to test if everything works as you'd expect, you just have to add this to your test:
expect(getByTestId('imageCircleView')).toHaveStyle({ borderColor: '#00ffff' });
And that's it.

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.

Prevent execution of functions within <Collapse> until expanded, when using ReactJS with Material-UI

I am using Material-UI within my ReactJS app to create a table that, when clicked, expands to show more detailed info (a new row just beneath the clicked row). As example, here is a minimal toy example:
https://codesandbox.io/s/material-collapse-table-forked-t6thz
The code relevant to the problem is:
<Collapse
in={open}
timeout="auto"
TransitionProps={{
mountOnEnter: true,
unmountOnExit: true,
}}
mountOnEnter
unmountOnExit
>
<div>
{/* actual function calls here; produces JSX output */}
{console.log("This should not execute before expanding!")}
Hello
</div>
</Collapse>;
Do note that the console.log() statement is just a simple replacement for my actualy functionality, which involves some API calls that are made when a row is clicked, and the corresponding info is displayed. So instead of console.log() I would actually call some other function.
I find that the console.log() statement executed on initial page render itself, even though in=false initially. How can I prevent this? Such that the function calls take place only when the Collapse is expanded. I initially thought this would be automatically handled by using mountOnEnter and unmountOnExit, but that does not seem to be the case. Any help would be appreciated, that could fix this problem in the sample example above.
I am working on an existing open source project, and therefore do not have the flexibility to restructure the existing codebase a lot. I would ideally have loved to implement this differently, but don't have that option. So posting here to know what options I might have given the above scenario. Thanks.
Problem
The children are rendered on initial load because they're defined within the Row component.
Solution
Move the Collapse children to its own React component. This won't render the children until the Collapse is opened. However, it'll re-render the child component when Collapse is closed. So depending on how you're making the API call and how other state interacts with this component, you may want to pass open to this component and use it as an useEffect dependency.
For example:
const Example = ({ open }) => {
React.useEffect(() => {
const fetchData = async () => {...};
if(open) fetchData();
}, [open]);
return (...);
}
Demo
Code
A separate React component:
const Example = ({ todoId }) => {
const [state, setState] = React.useState({
error: "",
data: {},
isLoading: true
});
const { data, error, isLoading } = state;
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}`
);
if (res.status !== 200) throw String("Unable to locate todo item");
const data = await res.json();
setState({ error: "", data, isLoading: false });
} catch (error) {
setState({ error: error.toString(), data: {}, isLoading: false });
}
};
fetchData();
/* eslint-disable react-hooks/exhaustive-deps */
}, []);
return (
<div
style={{
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{error ? (
<p style={{ color: "red" }}>{error}</p>
) : isLoading ? (
<p>Loading...</p>
) : (
<>
<div>
<strong>Id</strong>: {data.id}
</div>
<div>
<strong>Title</strong>: {data.title}
</div>
<div>
<strong>Completed</strong>: {data.completed.toString()}
</div>
</>
)}
</div>
);
};
The Example component being used as children to Collapse (also see supported Collapse props):
<Collapse
in={open}
timeout="auto"
// mountOnEnter <=== not a supported prop
// unmountOnExit <=== not a supported prop
>
<Example todoId={todoId + 1} />
</Collapse>
Other Thoughts
If the API data is static and/or doesn't change too often, I'd recommend using data as a dependency to useEffect (similar to the open example above). This will limit the need to constantly query the API for the same data every time the same row is expanded/collapsed.
Firstly, huge thanks to Matt for his detailed explanation. I worked through his example, and expanded on it to work for me as required. The main takeaway for me was: "Move the Collapse children to its own React component."
The solution posted by Matt above, I felt, didn't completely solve the problem for me. E.g. if I add a console.log() statement to the render() of the new child component (<Example>), I still see it being executed before it is mounted.
Adding mountOnEnter and unmountOnExit solved this problem:
But as Matt mentioned, the number of times the children were getting rendered was still a problem. So I slightly changed some bits (also simplified the code a bit):
Essentially, I do this now:
My child component is:
function Example(props) {
return (
<div
style={{
fontSize: 100,
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{props.flag && console.log("This should not execute before expanding!")}
{props.value}
</div>
);
}
and I call it from the parent component as:
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" mountOnEnter unmountOnExit>
<Example value={row.name} flag={open} />
</Collapse>
</TableCell>
</TableRow>
Note that the parameter flag is essential to avoid the function execution during closing of the <Collapse>.

Material UI strange blue line when overflow attribute is added

I'm using Material-UI with my React application. I'm also using styled components and I'm viewing the app in a Chrome browser. The issue I'm having doesn't occur when using a Firefox browser.
When applying the overflow attribute in my styled component, I'm seeing this blue line towards the bottom of the modal. This only appears when I'm playing with the size of my browser window. As I gradually bring my browser window closer to normal size, the line goes away. I'm not sure why this is or what I can do to fix it.
Here is a snippet of my code:
export const ScrollableModal = styled(MUIModal)(() => ({
overflow: 'scroll',
}));
const Modal = ({ title, children, actionsLeft, actionsRight, ...rest }) => {
const wrappedTitle =
typeof title === 'string' ? <Typography>{title}</Typography> : title;
return (
<ScrollableModal {...rest}>
<Container>
I've left the rest out because it's not relevant to my question.
Here is a screenshot of what I'm describing:
I guess that's the outline property what they mentioned in the documentation for simple modal:
Notice that you can disable the outline (often blue or gold) with the outline: 0 CSS property.
First needs to be added to the current style:
const useStyles = makeStyles({
modal: {
textAlign: 'center',
width: '35vw',
backgroundColor: 'white',
opacity: 0.8,
outline: 0, // add / remove
}
});
Then it can be applied on the Container just like the following in the render:
const styles = useStyles();
return <>
<Modal open={true}>
<Container className={styles.modal}>
<p>Simple Modal</p>
</Container>
</Modal>
</>
Result by adding and removing outline property with value 0:
I guess with styled components just create a styled Container with opacity: 0 if you don't want to use makeStlyes for this purpose.
That resolved the issue for me.
I hope that helps!

Categories

Resources