fresnel
fresnel copied to clipboard
Expose a `useMedia` hook
I've found myself loving Fresnel for responsiveness, but I still miss the flexibility of window.matchMedia
to access breakpoints using a React Hooks pattern. However, I think there's a way to map this functionality to Fresnel's approach!
Let's say @artsy/fresnel
exposes a useMedia
hook from the result of createMedia
, like so:
import { createMedia } from '@artsy/fresnel';
const { Media, MediaContextProvider, useMedia } = createMedia({ ... });
Then, from any functional component that's nested within <Media>
, I can use the hook like so:
function App() {
<>
<Media at="sm">
<SomeView />
</Media>
<Media greaterThan="sm">
<SomeView />
</Media>
</>
}
function SomeView() {
const atSm = useMedia({ at: 'sm' });
const greaterThanSm = useMedia({ greaterThan: 'sm' });
if (atSm) return <>Something for small screens</>
if (greaterThan) return <>Something for bigger screens</>
}
If useMedia
is invoked without the <Media>
context provider, then the return value from useMedia
should always be false
and maybe a warning is raised.
One thing that this would impact is SSR rendering, as outlined in the "Why not conditionally render" section of the docs. That said, I think this hook could have some value.
@alloy @zephraph, y'all have any thoughts?
One thing that this would impact is SSR rendering
If useMedia
is relying only on the props of a <Media>
component higher in the tree (as opposed to an actual window.matchMedia
check), would this still pose a problem?
@smithki it would be a problem, yeah. The challenge here is during SSR we don't know which breakpoints we're rendering for so we render all the breakpoints and use media queries to control which is displayed or not. Essentially, the best we could do here on the server is make everything true to render all of those components or make everything false to render none of them.
That's why we have the wrapping <Media>
components, because conditional rendering of any sort fundamentally breaks down when server side rendering.
The challenge here is during SSR we don't know which breakpoints we're rendering for so we render all the breakpoints and use media queries to control which is displayed or not.
I think for the use-case I described it shouldn't be a problem that useMedia
will always return true
in the server, because those components are being rendered anyways as they are wrapped with <Media>
somewhere higher. What I'm proposing is not necessarily a "real" media query hook, but rather an interface for functional components deeper in the tree to know which props exist on the nearest <Media>
parent.
This also means that useMedia
would only work on components that are wrapped with <Media>
.
I think this could be an ergonomic way of solving the problem hi-lighted in the README, where instead of a HoC pattern, we use hooks!
So like, the question is how to programmatically represent the underlying architecture of this node structure 🤔:
<Media at="sm">
...
</Media>
It seems like all the bits are there to do this, but would be good if the original author (@alloy) could chime in on it.
I've used the logic of the <Media>
component to populate Redux with the current breakpoint. It's relatively simple albeit somewhat contrived, as it only populates the breakpoint in Redux after the browser has loaded. It's not perfect, but it works for my use cases. Try rendering this near the root of your application's tree.
import React, {useLayoutEffect} from "react"
import {useDispatch} from "react-redux"
import {Media} from "lib/utils/media/Media"
import {SET_CURRENT_BREAKPOINT} from "redux/reducers/ui/ui-reducer"
const mediaConfig = ["xxs", "xs", "sm", "md", "lg"]
/**
* This is a dummy component that renders an empty fragment. Its only purpose is to set
* the current breakpoint, and is rendered conditionally based on the parent <Media> context.
*/
function BreakpointOption({breakpoint, dispatch}) {
useLayoutEffect(() => {
dispatch({type: SET_CURRENT_BREAKPOINT, value: breakpoint})
}, [breakpoint, dispatch])
return null
}
/**
* A lightning quick solution to set the current breakpoint on browser hydration.
* <Media> renders on the server and when it's time for the browser to hydrate,
* it computes the layout very quickly.
*/
function BreakpointSwitchComponent() {
const dispatch = useDispatch()
return (
<>
{mediaConfig.map((at, bp) => {
return (
<Media key={at} at={at}>
<BreakpointOption breakpoint={bp} dispatch={dispatch} />
</Media>
)
})}
<Media greaterThanOrEqual="xl">
<BreakpointOption breakpoint={5} dispatch={dispatch} />
</Media>
</>
)
}
export default BreakpointSwitchComponent
It seems like all the bits are there to do this, but would be good if the original author (@alloy) could chime in on it.
I agree that functionally it seems like the bits are there, perhaps an initial naive PR would better show if there are issues being overlooked, but the thing that worries me is making it too easy to shoot yourself in the foot by making it easier to do if … else …
type of rendering.
Your example does the right thing, but it doesn’t seem far-fetched to imagine that somebody naively would write the same example like so:
function SomeView() {
const atSm = useMedia({ at: 'sm' });
const greaterThanSm = useMedia({ greaterThan: 'sm' });
if (atSm) {
return <>Something for small screens</>
} else if (greaterThan) {
return <>Something for bigger screens</>
}
}
Or:
function SomeView() {
const atSm = useMedia({ at: 'sm' });
if (atSm) {
return <>Something for small screens</>
} else {
return <>Something for bigger screens</>
}
}
As of yet I’m on the fence, but open to being shown ways to combat my worries and opinions of @damassi & @zephraph.
I’m trying to think this all through and wondering if the if … else …
pattern matters if the hook is required to be used inside a Media
component, rather than MediaContextProvider
. Perhaps this is where our shared understanding is lacking?
what if atSm
were used within <MediaContextProvider>
, but you run multiple passes of rendering (ReactDOMServer.renderToString
), alternating atSm
so it is both true (first pass) and then false (second pass), and then join the rendered layouts together afterwards (so all layouts are still shipped to the client)? I imagine you could then do:
function App() {
<MediaContextProvider>
<SomeView />
</MediaContextProvider>
}
function SomeView() {
const atSm = useMedia({ at: 'sm' });
if (atSm) {
return <>Something for small screens</>
} else {
return <>Something for bigger screens</>
}
}
Which would output all the breakpoints:
<>Something for small screens</>
<>Something for bigger screens</>
So the interesting case from the Readme would become:
function SomeView() {
const atSm = useMedia({ at: 'sm' });
return <Sans size={atSm ? 2 : 3}>
}
Returning:
<Sans size={2}>
<Sans size={3}>
It's late, so I haven't thought hard about how the multiple passes of rendering approach would work when combining several useMedia hooks and advanced branching logic. But in principle it should work: setting a breakpoint and going through all the motions. Then setting the next breakpoint, and so going through all the motions again. I feared it could become a combinatorial explosion of breakpoints, necessitating an exponential times of rendering (2^X
). But I don't think we need all combinations, because we just need the output of a single breakpoint at a time, so all the other ones could be set to false.
This way, you could avoid <Media>
components altogether, and the extra DOM nodes that they inject. Which were some times problematic. By using @xiata's code to apply attributes to all the elements instead.
The final output could be:
<div data-breakpoint="sm">Something for small screens</div>
<div data-breakpoint="lg">Something for bigger screens</div>
Which Fresnel could target using a CSS attribute selector like this:
[data-breakpoint="sm"] {
display:none !important;
}
This would solve this issue: #152 - Optional container-less rendering by using React.Fragments And this issue as well: #183 - Compatibility with React Native Web + NextJS
I imagine it could also be a win for Fresnel for simple SSR'ed pages that just need a little bit of classic responsivity (media queries for styles) without any change to the markup/structure.
Some potential inspiration: useMediaQuery
This is a CSS media query hook for React. It listens for matches to a CSS media query. It allows the rendering of components based on whether the query matches or not.
Been a minute, going through issues and clearing out things that are stale. Happy to check out a PR if anyone has it.