UNPKG

18.1 kBMarkdownView Raw
1# @artsy/fresnel
2
3[![CircleCI][ci-icon]][ci] [![npm version][npm-icon]][npm]
4
5> The Fresnel equations describe the reflection of light when incident on an
6> interface between different optical media.
7
8– https://en.wikipedia.org/wiki/Fresnel_equations
9
10## Installation
11
12```sh
13 yarn add @artsy/fresnel
14```
15
16**Table of Contents**
17
18- [Overview](#overview)
19- [Basic Example](#basic-example)
20- [Server-side Rendering (SSR)](#server-side-rendering-ssr-usage)
21- [Usage with Gatsby or Next](#usage-with-gatsby-or-next)
22- [Example Apps](#example-apps)
23- [Why not conditionally render?](#why-not-conditionally-render)
24- [API](#api)
25- [Pros vs Cons](#pros-vs-cons)
26- [Development](#development)
27
28## Overview
29
30When writing responsive components it's common to use media queries to adjust
31the display when certain conditions are met. Historically this has taken place
32directly in CSS/HTML:
33
34```css
35@media screen and (max-width: 767px) {
36 .my-container {
37 width: 100%;
38 }
39}
40@media screen and (min-width: 768px) {
41 .my-container {
42 width: 50%;
43 }
44}
45```
46
47```html
48<div class="my-container" />
49```
50
51By hooking into a breakpoint definition, `@artsy/fresnel` takes this declarative
52approach and brings it into the React world.
53
54## Basic Example
55
56```tsx
57import React from "react"
58import ReactDOM from "react-dom"
59import { createMedia } from "@artsy/fresnel"
60
61const { MediaContextProvider, Media } = createMedia({
62 breakpoints: {
63 sm: 0,
64 md: 768,
65 lg: 1024,
66 xl: 1192,
67 },
68})
69
70const App = () => (
71 <MediaContextProvider>
72 <Media at="sm">
73 <MobileApp />
74 </Media>
75 <Media at="md">
76 <TabletApp />
77 </Media>
78 <Media greaterThanOrEqual="lg">
79 <DesktopApp />
80 </Media>
81 </MediaContextProvider>
82)
83
84ReactDOM.render(<App />, document.getElementById("react"))
85```
86
87## Server-side Rendering (SSR) Usage
88
89The first important thing to note is that when server-rendering with
90`@artsy/fresnel`, all breakpoints get rendered by the server. Each `Media`
91component is wrapped by plain CSS that will only show that breakpoint if it
92matches the user's current browser size. This means that the client can
93accurately start rendering the HTML/CSS while it receives the markup, which is
94long before the React application has booted. This improves perceived
95performance for end-users.
96
97Why not just render the one that the current device needs? We can't accurately
98identify which breakpoint your device needs on the server. We could use a
99library to sniff the browser user-agent, but those aren't always accurate, and
100they wouldn't give us all the information we need to know when we are
101server-rendering. Once client-side JS boots and React attaches, it simply washes
102over the DOM and removes markup that is unneeded, via a `matchMedia` call.
103
104### SSR Example
105
106First, configure `@artsy/fresnel` in a `Media` file that can be shared across
107the app:
108
109```tsx
110// Media.tsx
111
112import { createMedia } from "@artsy/fresnel"
113
114const ExampleAppMedia = createMedia({
115 breakpoints: {
116 sm: 0,
117 md: 768,
118 lg: 1024,
119 xl: 1192,
120 },
121})
122
123// Generate CSS to be injected into the head
124export const mediaStyle = ExampleAppMedia.createMediaStyle()
125export const { Media, MediaContextProvider } = ExampleAppMedia
126```
127
128Create a new `App` file which will be the launching point for our application:
129
130```tsx
131// App.tsx
132
133import React from "react"
134import { Media, MediaContextProvider } from "./Media"
135
136export const App = () => {
137 return (
138 <MediaContextProvider>
139 <Media at="sm">Hello mobile!</Media>
140 <Media greaterThan="sm">Hello desktop!</Media>
141 </MediaContextProvider>
142 )
143}
144```
145
146Mount `<App />` on the client:
147
148```tsx
149// client.tsx
150
151import React from "react"
152import ReactDOM from "react-dom"
153import { App } from "./App"
154
155ReactDOM.render(<App />, document.getElementById("react"))
156```
157
158Then on the server, setup SSR rendering and pass `mediaStyle` into a `<style>`
159tag in the header:
160
161```tsx
162// server.tsx
163
164import React from "react"
165import ReactDOMServer from "react-dom/server"
166import express from "express"
167
168import { App } from "./App"
169import { mediaStyle } from "./Media"
170
171const app = express()
172
173app.get("/", (_req, res) => {
174 const html = ReactDOMServer.renderToString(<App />)
175
176 res.send(`
177 <html>
178 <head>
179 <title>@artsy/fresnel - SSR Example</title>
180
181 <!–– Inject the generated styles into the page head -->
182 <style type="text/css">${mediaStyle}</style>
183 </head>
184 <body>
185 <div id="react">${html}</div>
186
187 <script src='/assets/app.js'></script>
188 </body>
189 </html>
190 `)
191})
192
193app.listen(3000, () => {
194 console.warn("\nApp started at http://localhost:3000 \n")
195})
196```
197
198And that's it! To test, disable JS and scale your browser window down to a
199mobile size and reload; it will correctly render the mobile layout without the
200need to use a user-agent or other server-side "hints".
201
202## Usage with Gatsby or Next
203
204`@artsy/fresnel` works great with Gatsby or Next.js's static hybrid approach to
205rendering. See the examples below for a simple implementation.
206
207## Example Apps
208
209There are four examples one can explore in the `/examples` folder:
210
211- [Basic](examples/basic)
212- [Server-side Rendering](examples/ssr-rendering)
213- [Gatsby](examples/gatsby)
214- [Next](examples/nextjs)
215- [Kitchen Sink](examples/kitchen-sink)
216
217While the `Basic` and `SSR` examples will get one pretty far, `@artsy/fresnel`
218can do a lot more . For an exhaustive deep-dive into its features, check out the
219[Kitchen Sink](examples/kitchen-sink) app.
220
221## Why not conditionally render?
222
223Other existing solutions take a conditionally rendered approach, such as
224[`react-responsive`][react-responsive] or [`react-media`][react-media], so where
225does this approach differ?
226
227Server side rendering!
228
229But first, what is conditional rendering?
230
231In the React ecosystem a common approach to writing declarative responsive
232components is to use the browser’s [`matchMedia` api][match-media-api]:
233
234```tsx
235<Responsive>
236 {({ sm }) => {
237 if (sm) {
238 return <MobileApp />
239 } else {
240 return <DesktopApp />
241 }
242 }}
243</Responsive>
244```
245
246On the client, when a given breakpoint is matched React conditionally renders a
247tree.
248
249However, this approach has some limitations for what we wanted to achieve with
250our server-side rendering setup:
251
252- It's impossible to reliably know the user's current breakpoint during the
253 server render phase since that requires a browser.
254
255- Setting breakpoint sizes based on user-agent sniffing is prone to errors due
256 the inability to precisely match device capabilities to size. One mobile
257 device might have greater pixel density than another, a mobile device may fit
258 multiple breakpoints when taking device orientation into consideration, and on
259 desktop clients there is no way to know at all. The best devs can do is guess
260 the current breakpoint and populate `<Responsive>` with assumed state.
261
262Artsy settled on what we think makes the best trade-offs. We approach this
263problem in the following way:
264
2651. Render markup for all breakpoints on the server and send it down the wire.
266
2671. The browser receives markup with proper media query styling and will
268 immediately start rendering the expected **visual** result for whatever
269 viewport width the browser is at.
270
2711. When all JS has loaded and React starts the rehydration phase, we query the
272 browser for what breakpoint it’s currently at and then limit the rendered
273 components to the matching media queries. This prevents life-cycle methods
274 from firing in hidden components and unused html being written to the DOM.
275
2761. Additionally, we register event listeners with the browser to notify the
277 `MediaContextProvider` when a different breakpoint is matched and then
278 re-render the tree using the new value for the `onlyMatch` prop.
279
280Let’s compare what a component tree using `matchMedia` would look like with our
281approach:
282
283<table>
284<tr><th>Before</th><th>After</th></tr>
285<tr><td>
286
287```tsx
288<Responsive>
289 {({ sm }) => {
290 if (sm) return <SmallArticleItem {...props} />
291 else return <LargeArticleItem {...props} />
292 }}
293</Responsive>
294```
295
296</td>
297<td>
298
299```tsx
300<>
301 <Media at="sm">
302 <SmallArticleItem {...props} />
303 </Media>
304 <Media greaterThan="sm">
305 <LargeArticleItem {...props} />
306 </Media>
307</>
308```
309
310</td></tr>
311</table>
312
313See the [server-side rendering](examples/ssr-rendering) app for a working
314example.
315
316## API
317
318### createMedia
319
320First things first. You’ll need to define the breakpoints and interaction needed
321for your design to produce the set of media components you can use throughout
322your application.
323
324For example, consider an application that has the following breakpoints:
325
326- A viewport width between 0 and 768 (768 not included) points, named `sm`.
327- A viewport width between 768 and 1024 (1024 not included) points, named `md`.
328- A viewport width between 1024 and 1192 (1192 not included) points, named `lg`.
329- A viewport width from 1192 points and above, named `xl`.
330
331And the following interactions:
332
333- A device that supports hovering a pointer device, named `hover`.
334- A device that does not support hovering a pointer device, named `notHover`.
335
336You would then produce the set of media components like so:
337
338```tsx
339// Media.tsx
340
341const ExampleAppMedia = createMedia({
342 breakpoints: {
343 sm: 0,
344 md: 768,
345 lg: 1024,
346 xl: 1192,
347 },
348 interactions: {
349 hover: "(hover: hover)",
350 notHover: "(hover: none)",
351 landscape: "not all and (orientation: landscape)",
352 portrait: "not all and (orientation: portrait)",
353 },
354})
355
356export const { Media, MediaContextProvider, createMediaStyle } = ExampleAppMedia
357```
358
359As you can see, breakpoints are defined by their _start_ offset, where the first
360one is expected to start at 0.
361
362### MediaContextProvider
363
364The `MediaContextProvider` component influences how `Media` components will be
365rendered. Mount it at the root of your component tree:
366
367```tsx
368import React from "react"
369import { MediaContextProvider } from "./Media"
370
371export const App = () => {
372 return <MediaContextProvider>...</MediaContextProvider>
373}
374```
375
376### Media
377
378The `Media` component created for your application has a few mutually exclusive
379props that make up the API you’ll use to declare your responsive layouts. These
380props all operate based on the named breakpoints that were provided when you
381created the media components.
382
383```tsx
384import React from "react"
385import { Media } from "./Media"
386
387export const HomePage = () => {
388 return (
389 <>
390 <Media at="sm">Hello mobile!</Media>
391 <Media greaterThan="sm">Hello desktop!</Media>
392 </>
393 )
394}
395```
396
397The examples given for each prop use breakpoint definitions as defined in the
398above ‘Setup’ section.
399
400If you would like to avoid the underlying div that is generated by `<Media>` and
401instead use your own element, use the render-props form but be sure to **not**
402render any children when not necessary:
403
404```tsx
405export const HomePage = () => {
406 return (
407 <>
408 <Media at="sm">Hello mobile!</Media>
409 <Media greaterThan="sm">
410 {(mediaClassNames, renderChildren) => {
411 return (
412 <MySpecialComponent className={mediaClassNames}>
413 {renderChildren ? "Hello desktop!" : null}
414 </MySpecialComponent>
415 )
416 }}
417 </Media>
418 </>
419 )
420}
421```
422
423#### createMediaStyle
424
425> Note: This is only used when SSR rendering
426
427Besides the `Media` and `MediaContextProvider` components, there's a
428`createMediaStyle` function that produces the CSS styling for all possible media
429queries that the `Media` instance can make use of while markup is being passed
430from the server to the client during hydration. If only a subset of breakpoint
431keys is used those can be optional specified as a parameter to minimize the
432output. Be sure to insert this within a `<style>` tag
433[in your document’s `<head>`](https://github.com/artsy/fresnel/blob/master/examples/ssr-rendering/src/server.tsx#L28).
434
435It’s advisable to do this setup in its own module so that it can be easily
436imported throughout your application:
437
438```tsx
439import { createMedia } from "@artsy/fresnel"
440
441const ExampleAppMedia = createMedia({
442 breakpoints: {
443 sm: 0,
444 md: 768,
445 lg: 1024,
446 xl: 1192,
447 },
448})
449
450// Generate CSS to be injected into the head
451export const mediaStyle = ExampleAppMedia.createMediaStyle() // optional: .createMediaStyle(['at'])
452export const { Media, MediaContextProvider } = ExampleAppMedia
453```
454
455#### onlyMatch
456
457Rendering can be constrained to specific breakpoints/interactions by specifying
458a list of media queries to match. By default _all_ will be rendered.
459
460#### disableDynamicMediaQueries
461
462By default, when rendered client-side, the browser’s [`matchMedia`
463api][match-media-api] will be used to _further_ constrain the `onlyMatch` list
464to only the currently matching media queries. This is done to avoid triggering
465mount related life-cycle hooks of hidden components.
466
467Disabling this behaviour is mostly intended for debugging purposes.
468
469#### at
470
471Use this to declare that children should only be visible at a specific
472breakpoint, meaning that the viewport width is greater than or equal to the
473start offset of the breakpoint, but less than the next breakpoint, if one
474exists.
475
476For example, children of this `Media` declaration will only be visible if the
477viewport width is between 0 and 768 (768 not included) points:
478
479```tsx
480<Media at="sm">...</Media>
481```
482
483The corresponding css rule:
484
485```css
486@media not all and (min-width: 0px) and (max-width: 767px) {
487 .fresnel-at-sm {
488 display: none !important;
489 }
490}
491```
492
493#### lessThan
494
495Use this to declare that children should only be visible while the viewport
496width is less than the start offset of the specified breakpoint.
497
498For example, children of this `Media` declaration will only be visible if the
499viewport width is between 0 and 1024 (1024 not included) points:
500
501```tsx
502<Media lessThan="lg">...</Media>
503```
504
505The corresponding css rule:
506
507```css
508@media not all and (max-width: 1023px) {
509 .fresnel-lessThan-lg {
510 display: none !important;
511 }
512}
513```
514
515#### greaterThan
516
517Use this to declare that children should only be visible while the viewport
518width is equal or greater than the start offset of the _next_ breakpoint.
519
520For example, children of this `Media` declaration will only be visible if the
521viewport width is equal or greater than 1024 points:
522
523```tsx
524<Media greaterThan="md">...</Media>
525```
526
527The corresponding css rule:
528
529```css
530@media not all and (min-width: 1024px) {
531 .fresnel-greaterThan-md {
532 display: none !important;
533 }
534}
535```
536
537#### greaterThanOrEqual
538
539Use this to declare that children should only be visible while the viewport
540width is equal to the start offset of the specified breakpoint _or_ greater.
541
542For example, children of this `Media` declaration will only be visible if the
543viewport width is 768 points or up:
544
545```tsx
546<Media greaterThanOrEqual="md">...</Media>
547```
548
549The corresponding css rule:
550
551```css
552@media not all and (min-width: 768px) {
553 .fresnel-greaterThanOrEqual-md {
554 display: none !important;
555 }
556}
557```
558
559#### between
560
561Use this to declare that children should only be visible while the viewport
562width is equal to the start offset of the first specified breakpoint but less
563than the start offset of the second specified breakpoint.
564
565For example, children of this `Media` declaration will only be visible if the
566viewport width is between 768 and 1192 (1192 not included) points:
567
568```tsx
569<Media between={["md", "xl"]}>...</Media>
570```
571
572The corresponding css rule:
573
574```css
575@media not all and (min-width: 768px) and (max-width: 1191px) {
576 .fresnel-between-md-xl {
577 display: none !important;
578 }
579}
580```
581
582## Pros vs Cons
583
584Pros:
585
586- Built on top of simple, proven technology: HTML and CSS media queries.
587- Users see rendered markup at the correct breakpoint for their device, even
588 before React has been loaded.
589
590Cons:
591
592- If utilizing SSR rendering features, when the markup is passed down from the
593 server to the client it includes _all_ breakpoints, which increases the page
594 size. (However, once the client mounts, the unused breakpoint markup is
595 cleared from the DOM.)
596- The current media query is no longer something components can access; it is
597 determined only by the props of the `<Media>` component they find themselves
598 in.
599
600That last point presents an interesting problem. How might we represent a
601component that gets styled differently at different breakpoints? (Let’s imagine
602a `matchMedia` example.)
603
604```tsx
605<Sans size={sm ? 2 : 3}>
606```
607
608```tsx
609<>
610 <Media at="sm">
611 {this.getComponent('sm')
612 </Media>
613 <Media greaterThan="sm">
614 {this.getComponent()
615 </Media>
616</>
617```
618
619```tsx
620getComponent(breakpoint?: string) {
621 const sm = breakpoint === 'sm'
622 return <Sans size={sm ? 2 : 3} />
623}
624```
625
626We're still figuring out patterns for this, so please [let us know][new-issue]
627if you have suggestions.
628
629## Development
630
631<details>
632
633This project uses [auto-release](https://github.com/intuit/auto-release#readme)
634to automatically release on every PR. Every PR should have a label that matches
635one of the following
636
637- Version: Trivial
638- Version: Patch
639- Version: Minor
640- Version: Major
641
642Major, minor, and patch will cause a new release to be generated. Use major for
643breaking changes, minor for new non-breaking features, and patch for bug fixes.
644Trivial will not cause a release and should be used when updating documentation
645or non-project code.
646
647If you don't want to release on a particular PR but the changes aren't trivial
648then use the `Skip Release` tag along side the appropriate version tag.
649
650</details>
651
652[ci]: https://circleci.com/gh/artsy/fresnel
653[ci-icon]: https://circleci.com/gh/artsy/fresnel.svg?style=shield
654[npm]: https://www.npmjs.com/package/@artsy/fresnel
655[npm-icon]: https://badge.fury.io/js/%40artsy%2Ffresnel.svg
656[react-responsive]: https://github.com/contra/react-responsive
657[react-media]: https://github.com/ReactTraining/react-media
658[match-media-api]:
659 https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia
660[new-issue]: https://github.com/artsy/fresnel/issues/new
661[release-tags]: https://github.com/artsy/fresnel/blob/master/package.json