UNPKG

17.8 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: "(orientation: landscape)",
352 portrait: "(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:
402
403```tsx
404export const HomePage = () => {
405 return (
406 <>
407 <Media at="sm">Hello mobile!</Media>
408 <Media greaterThan="xs">
409 {(mediaClassNames) => {
410 return (
411 <MySpecialComponent className={mediaClassNames}>
412 Hello desktop!
413 </MySpecialComponent>
414 )
415 }}
416 </Media>
417 </>
418 )
419}
420```
421
422#### createMediaStyle
423
424> Note: This is only used when SSR rendering
425
426Besides the `Media` and `MediaContextProvider` components, there's a
427`createMediaStyle` function that produces the CSS styling for all possible media
428queries that the `Media` instance can make use of while markup is being passed
429from the server to the client during hydration. Be sure to insert this within a
430`<style>` tag
431[in your document’s `<head>`](https://github.com/artsy/fresnel/blob/master/examples/ssr-rendering/src/server.tsx#L28).
432
433It’s advisable to do this setup in its own module so that it can be easily
434imported throughout your application:
435
436```tsx
437import { createMedia } from "@artsy/fresnel"
438
439const ExampleAppMedia = createMedia({
440 breakpoints: {
441 sm: 0,
442 md: 768,
443 lg: 1024,
444 xl: 1192,
445 },
446})
447
448// Generate CSS to be injected into the head
449export const mediaStyle = ExampleAppMedia.createMediaStyle()
450export const { Media, MediaContextProvider } = ExampleAppMedia
451```
452
453#### onlyMatch
454
455Rendering can be constrained to specific breakpoints/interactions by specifying
456a list of media queries to match. By default _all_ will be rendered.
457
458#### disableDynamicMediaQueries
459
460By default, when rendered client-side, the browser’s [`matchMedia`
461api][match-media-api] will be used to _further_ constrain the `onlyMatch` list
462to only the currently matching media queries. This is done to avoid triggering
463mount related life-cycle hooks of hidden components.
464
465Disabling this behaviour is mostly intended for debugging purposes.
466
467#### at
468
469Use this to declare that children should only be visible at a specific
470breakpoint, meaning that the viewport width is greater than or equal to the
471start offset of the breakpoint, but less than the next breakpoint, if one
472exists.
473
474For example, children of this `Media` declaration will only be visible if the
475viewport width is between 0 and 768 (768 not included) points:
476
477```tsx
478<Media at="sm">...</Media>
479```
480
481The corresponding css rule:
482
483```css
484@media not all and (min-width: 0px) and (max-width: 767px) {
485 .fresnel-at-sm {
486 display: none !important;
487 }
488}
489```
490
491#### lessThan
492
493Use this to declare that children should only be visible while the viewport
494width is less than the start offset of the specified breakpoint.
495
496For example, children of this `Media` declaration will only be visible if the
497viewport width is between 0 and 1024 (1024 not included) points:
498
499```tsx
500<Media lessThan="lg">...</Media>
501```
502
503The corresponding css rule:
504
505```css
506@media not all and (max-width: 1023px) {
507 .fresnel-lessThan-lg {
508 display: none !important;
509 }
510}
511```
512
513#### greaterThan
514
515Use this to declare that children should only be visible while the viewport
516width is equal or greater than the start offset of the _next_ breakpoint.
517
518For example, children of this `Media` declaration will only be visible if the
519viewport width is equal or greater than 1024 points:
520
521```tsx
522<Media greaterThan="md">...</Media>
523```
524
525The corresponding css rule:
526
527```css
528@media not all and (min-width: 1024px) {
529 .fresnel-greaterThan-md {
530 display: none !important;
531 }
532}
533```
534
535#### greaterThanOrEqual
536
537Use this to declare that children should only be visible while the viewport
538width is equal to the start offset of the specified breakpoint _or_ greater.
539
540For example, children of this `Media` declaration will only be visible if the
541viewport width is 768 points or up:
542
543```tsx
544<Media greaterThanOrEqual="md">...</Media>
545```
546
547The corresponding css rule:
548
549```css
550@media not all and (min-width: 768px) {
551 .fresnel-greaterThanOrEqual-md {
552 display: none !important;
553 }
554}
555```
556
557#### between
558
559Use this to declare that children should only be visible while the viewport
560width is equal to the start offset of the first specified breakpoint but less
561than the start offset of the second specified breakpoint.
562
563For example, children of this `Media` declaration will only be visible if the
564viewport width is between 768 and 1192 (1192 not included) points:
565
566```tsx
567<Media between={["md", "xl"]}>...</Media>
568```
569
570The corresponding css rule:
571
572```css
573@media not all and (min-width: 768px) and (max-width: 1191px) {
574 .fresnel-between-md-xl {
575 display: none !important;
576 }
577}
578```
579
580## Pros vs Cons
581
582Pros:
583
584- Built on top of simple, proven technology: HTML and CSS media queries.
585- Users see rendered markup at the correct breakpoint for their device, even
586 before React has been loaded.
587
588Cons:
589
590- If utilizing SSR rendering features, when the markup is passed down from the
591 server to the client it includes _all_ breakpoints, which increases the page
592 size. (However, once the client mounts, the unused breakpoint markup is
593 cleared from the DOM.)
594- The current media query is no longer something components can access; it is
595 determined only by the props of the `<Media>` component they find themselves
596 in.
597
598That last point presents an interesting problem. How might we represent a
599component that gets styled differently at different breakpoints? (Let’s imagine
600a `matchMedia` example.)
601
602```tsx
603<Sans size={sm ? 2 : 3}>
604```
605
606```tsx
607<>
608 <Media at="sm">
609 {this.getComponent('sm')
610 </Media>
611 <Media greaterThan="sm">
612 {this.getComponent()
613 </Media>
614</>
615```
616
617```tsx
618getComponent(breakpoint?: string) {
619 const sm = breakpoint === 'sm'
620 return <Sans size={sm ? 2 : 3} />
621}
622```
623
624We're still figuring out patterns for this, so please [let us know][new-issue]
625if you have suggestions.
626
627## Development
628
629<details>
630
631This project uses [auto-release](https://github.com/intuit/auto-release#readme)
632to automatically release on every PR. Every PR should have a label that matches
633one of the following
634
635- Version: Trivial
636- Version: Patch
637- Version: Minor
638- Version: Major
639
640Major, minor, and patch will cause a new release to be generated. Use major for
641breaking changes, minor for new non-breaking features, and patch for bug fixes.
642Trivial will not cause a release and should be used when updating documentation
643or non-project code.
644
645If you don't want to release on a particular PR but the changes aren't trivial
646then use the `Skip Release` tag along side the appropriate version tag.
647
648</details>
649
650[ci]: https://circleci.com/gh/artsy/fresnel
651[ci-icon]: https://circleci.com/gh/artsy/fresnel.svg?style=shield
652[npm]: https://www.npmjs.com/package/@artsy/fresnel
653[npm-icon]: https://badge.fury.io/js/%40artsy%2Ffresnel.svg
654[react-responsive]: https://github.com/contra/react-responsive
655[react-media]: https://github.com/ReactTraining/react-media
656[match-media-api]:
657 https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia
658[new-issue]: https://github.com/artsy/fresnel/issues/new
659[release-tags]: https://github.com/artsy/fresnel/blob/master/package.json