• Stars
    star
    1,098
  • Rank 42,222 (Top 0.9 %)
  • Language
    TypeScript
  • License
    Other
  • Created about 6 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

An SSR compatible approach to CSS media query based responsive layouts for React.

@artsy/fresnel

CircleCI npm version

The Fresnel equations describe the reflection of light when incident on an interface between different optical media.

– https://en.wikipedia.org/wiki/Fresnel_equations

⚠️ React 18 Notice

Due to React 18 more strictly handling server-side rendering rehydration full SSR support is incompatable with any version greater than 17. See this issue to track our progress in fixing this issue, and Dan Abramov's comment here.

In the meantime -- for users of server-side rendering features -- you can disable DOM cleaning optimizations by setting disableDynamicMediaQueries on the context like so and things should work:

<MediaContextProvider disableDynamicMediaQueries>
  <Media at='xs'>
    <MobileApp />
  </Media>
  <Media greaterThan='xs'>
    <DesktopApp />
  </Media>
<MediaContextProvider>

Note that this will fire all effects within sub components for each breakpoint on re-hydration. For some users this could be an issue; for many others, no problem at all.

Installation

  yarn add @artsy/fresnel

Table of Contents

Overview

When writing responsive components it's common to use media queries to adjust the display when certain conditions are met. Historically this has taken place directly in CSS/HTML:

@media screen and (max-width: 767px) {
  .my-container {
    width: 100%;
  }
}
@media screen and (min-width: 768px) {
  .my-container {
    width: 50%;
  }
}
<div class="my-container" />

By hooking into a breakpoint definition, @artsy/fresnel takes this declarative approach and brings it into the React world.

Basic Example

import React from "react"
import ReactDOM from "react-dom"
import { createMedia } from "@artsy/fresnel"

const { MediaContextProvider, Media } = createMedia({
  // breakpoints values can be either strings or integers
  breakpoints: {
    sm: 0,
    md: 768,
    lg: 1024,
    xl: 1192,
  },
})

const App = () => (
  <MediaContextProvider>
    <Media at="sm">
      <MobileApp />
    </Media>
    <Media at="md">
      <TabletApp />
    </Media>
    <Media greaterThanOrEqual="lg">
      <DesktopApp />
    </Media>
  </MediaContextProvider>
)

ReactDOM.render(<App />, document.getElementById("react"))

Server-side Rendering (SSR) Usage

The first important thing to note is that when server-rendering with @artsy/fresnel, all breakpoints get rendered by the server. Each Media component is wrapped by plain CSS that will only show that breakpoint if it matches the user's current browser size. This means that the client can accurately start rendering the HTML/CSS while it receives the markup, which is long before the React application has booted. This improves perceived performance for end-users.

Why not just render the one that the current device needs? We can't accurately identify which breakpoint your device needs on the server. We could use a library to sniff the browser user-agent, but those aren't always accurate, and they wouldn't give us all the information we need to know when we are server-rendering. Once client-side JS boots and React attaches, it simply washes over the DOM and removes markup that is unneeded, via a matchMedia call.

SSR Example

First, configure @artsy/fresnel in a Media file that can be shared across the app:

// Media.tsx

import { createMedia } from "@artsy/fresnel"

const ExampleAppMedia = createMedia({
  breakpoints: {
    sm: 0,
    md: 768,
    lg: 1024,
    xl: 1192,
  },
})

// Generate CSS to be injected into the head
export const mediaStyle = ExampleAppMedia.createMediaStyle()
export const { Media, MediaContextProvider } = ExampleAppMedia

Create a new App file which will be the launching point for our application:

// App.tsx

import React from "react"
import { Media, MediaContextProvider } from "./Media"

export const App = () => {
  return (
    <MediaContextProvider>
      <Media at="sm">Hello mobile!</Media>
      <Media greaterThan="sm">Hello desktop!</Media>
    </MediaContextProvider>
  )
}

Mount <App /> on the client:

// client.tsx

import React from "react"
import ReactDOM from "react-dom"
import { App } from "./App"

ReactDOM.render(<App />, document.getElementById("react"))

Then on the server, setup SSR rendering and pass mediaStyle into a <style> tag in the header:

// server.tsx

import React from "react"
import ReactDOMServer from "react-dom/server"
import express from "express"

import { App } from "./App"
import { mediaStyle } from "./Media"

const app = express()

app.get("/", (_req, res) => {
  const html = ReactDOMServer.renderToString(<App />)

  res.send(`
    <html>
      <head>
        <title>@artsy/fresnel - SSR Example</title>

        <!–– Inject the generated styles into the page head -->
        <style type="text/css">${mediaStyle}</style>
      </head>
      <body>
        <div id="react">${html}</div>

        <script src='/assets/app.js'></script>
      </body>
    </html>
  `)
})

app.listen(3000, () => {
  console.warn("\nApp started at http://localhost:3000 \n")
})

And that's it! To test, disable JS and scale your browser window down to a mobile size and reload; it will correctly render the mobile layout without the need to use a user-agent or other server-side "hints".

Usage with Gatsby or Next

@artsy/fresnel works great with Gatsby or Next.js's static hybrid approach to rendering. See the examples below for a simple implementation.

Example Apps

There are four examples one can explore in the /examples folder:

While the Basic and SSR examples will get one pretty far, @artsy/fresnel can do a lot more. For an exhaustive deep-dive into its features, check out the Kitchen Sink app.

If you're using Gatsby, you can also try gatsby-plugin-fresnel for easy configuration.

Why not conditionally render?

Other existing solutions take a conditionally rendered approach, such as react-responsive or react-media, so where does this approach differ?

Server side rendering!

But first, what is conditional rendering?

In the React ecosystem a common approach to writing declarative responsive components is to use the browser’s matchMedia api:

<Responsive>
  {({ sm }) => {
    if (sm) {
      return <MobileApp />
    } else {
      return <DesktopApp />
    }
  }}
</Responsive>

On the client, when a given breakpoint is matched React conditionally renders a tree.

However, this approach has some limitations for what we wanted to achieve with our server-side rendering setup:

  • It's impossible to reliably know the user's current breakpoint during the server render phase since that requires a browser.

  • Setting breakpoint sizes based on user-agent sniffing is prone to errors due the inability to precisely match device capabilities to size. One mobile device might have greater pixel density than another, a mobile device may fit multiple breakpoints when taking device orientation into consideration, and on desktop clients there is no way to know at all. The best devs can do is guess the current breakpoint and populate <Responsive> with assumed state.

Artsy settled on what we think makes the best trade-offs. We approach this problem in the following way:

  1. Render markup for all breakpoints on the server and send it down the wire.

  2. The browser receives markup with proper media query styling and will immediately start rendering the expected visual result for whatever viewport width the browser is at.

  3. When all JS has loaded and React starts the rehydration phase, we query the browser for what breakpoint it’s currently at and then limit the rendered components to the matching media queries. This prevents life-cycle methods from firing in hidden components and unused html being re-written to the DOM.

  4. Additionally, we register event listeners with the browser to notify the MediaContextProvider when a different breakpoint is matched and then re-render the tree using the new value for the onlyMatch prop.

Let’s compare what a component tree using matchMedia would look like with our approach:

BeforeAfter
<Responsive>
  {({ sm }) => {
    if (sm) return <SmallArticleItem {...props} />
    else return <LargeArticleItem {...props} />
  }}
</Responsive>
<>
  <Media at="sm">
    <SmallArticleItem {...props} />
  </Media>
  <Media greaterThan="sm">
    <LargeArticleItem {...props} />
  </Media>
</>

See the server-side rendering app for a working example.

API

createMedia

First things first. You’ll need to define the breakpoints and interaction needed for your design to produce the set of media components you can use throughout your application.

For example, consider an application that has the following breakpoints:

  • A viewport width between 0 and 768 (768 not included) points, named sm.
  • A viewport width between 768 and 1024 (1024 not included) points, named md.
  • A viewport width between 1024 and 1192 (1192 not included) points, named lg.
  • A viewport width from 1192 points and above, named xl.

And the following interactions:

  • A device that supports hovering a pointer device, named hover.
  • A device that does not support hovering a pointer device, named notHover.

You would then produce the set of media components like so:

// Media.tsx

const ExampleAppMedia = createMedia({
  breakpoints: {
    sm: 0,
    md: 768,
    lg: 1024,
    xl: 1192,
  },
  interactions: {
    hover: "(hover: hover)",
    notHover: "(hover: none)",
    landscape: "not all and (orientation: landscape)",
    portrait: "not all and (orientation: portrait)",
  },
})

export const { Media, MediaContextProvider, createMediaStyle } = ExampleAppMedia

As you can see, breakpoints are defined by their start offset, where the first one is expected to start at 0.

MediaContextProvider

The MediaContextProvider component influences how Media components will be rendered. Mount it at the root of your component tree:

import React from "react"
import { MediaContextProvider } from "./Media"

export const App = () => {
  return <MediaContextProvider>...</MediaContextProvider>
}

Media

The Media component created for your application has a few mutually exclusive props that make up the API you’ll use to declare your responsive layouts. These props all operate based on the named breakpoints that were provided when you created the media components.

import React from "react"
import { Media } from "./Media"

export const HomePage = () => {
  return (
    <>
      <Media at="sm">Hello mobile!</Media>
      <Media greaterThan="sm">Hello desktop!</Media>
    </>
  )
}

The examples given for each prop use breakpoint definitions as defined in the above β€˜Setup’ section.

If you would like to avoid the underlying div that is generated by <Media> and instead use your own element, use the render-props form but be sure to not render any children when not necessary:

export const HomePage = () => {
  return (
    <>
      <Media at="sm">Hello mobile!</Media>
      <Media greaterThan="sm">
        {(className, renderChildren) => {
          return (
            <MySpecialComponent className={className}>
              {renderChildren ? "Hello desktop!" : null}
            </MySpecialComponent>
          )
        }}
      </Media>
    </>
  )
}

createMediaStyle

Note: This is only used when SSR rendering

Besides the Media and MediaContextProvider components, there's a createMediaStyle function that produces the CSS styling for all possible media queries that the Media instance can make use of while markup is being passed from the server to the client during hydration. If only a subset of breakpoint keys is used those can be optional specified as a parameter to minimize the output. Be sure to insert this within a <style> tag in your document’s <head>.

It’s advisable to do this setup in its own module so that it can be easily imported throughout your application:

import { createMedia } from "@artsy/fresnel"

const ExampleAppMedia = createMedia({
  breakpoints: {
    sm: 0,
    md: 768,
    lg: 1024,
    xl: 1192,
  },
})

// Generate CSS to be injected into the head
export const mediaStyle = ExampleAppMedia.createMediaStyle() // optional: .createMediaStyle(['at'])
export const { Media, MediaContextProvider } = ExampleAppMedia

onlyMatch

Rendering can be constrained to specific breakpoints/interactions by specifying a list of media queries to match. By default all will be rendered.

disableDynamicMediaQueries

By default, when rendered client-side, the browser’s matchMedia api will be used to further constrain the onlyMatch list to only the currently matching media queries. This is done to avoid triggering mount related life-cycle hooks of hidden components.

Disabling this behaviour is mostly intended for debugging purposes.

at

Use this to declare that children should only be visible at a specific breakpoint, meaning that the viewport width is greater than or equal to the start offset of the breakpoint, but less than the next breakpoint, if one exists.

For example, children of this Media declaration will only be visible if the viewport width is between 0 and 768 (768 not included) points:

<Media at="sm">...</Media>

The corresponding css rule:

@media not all and (min-width: 0px) and (max-width: 767px) {
  .fresnel-at-sm {
    display: none !important;
  }
}

lessThan

Use this to declare that children should only be visible while the viewport width is less than the start offset of the specified breakpoint.

For example, children of this Media declaration will only be visible if the viewport width is between 0 and 1024 (1024 not included) points:

<Media lessThan="lg">...</Media>

The corresponding css rule:

@media not all and (max-width: 1023px) {
  .fresnel-lessThan-lg {
    display: none !important;
  }
}

greaterThan

Use this to declare that children should only be visible while the viewport width is equal or greater than the start offset of the next breakpoint.

For example, children of this Media declaration will only be visible if the viewport width is equal or greater than 1024 points:

<Media greaterThan="md">...</Media>

The corresponding css rule:

@media not all and (min-width: 1024px) {
  .fresnel-greaterThan-md {
    display: none !important;
  }
}

greaterThanOrEqual

Use this to declare that children should only be visible while the viewport width is equal to the start offset of the specified breakpoint or greater.

For example, children of this Media declaration will only be visible if the viewport width is 768 points or up:

<Media greaterThanOrEqual="md">...</Media>

The corresponding css rule:

@media not all and (min-width: 768px) {
  .fresnel-greaterThanOrEqual-md {
    display: none !important;
  }
}

between

Use this to declare that children should only be visible while the viewport width is equal to the start offset of the first specified breakpoint but less than the start offset of the second specified breakpoint.

For example, children of this Media declaration will only be visible if the viewport width is between 768 and 1192 (1192 not included) points:

<Media between={["md", "xl"]}>...</Media>

The corresponding css rule:

@media not all and (min-width: 768px) and (max-width: 1191px) {
  .fresnel-between-md-xl {
    display: none !important;
  }
}

Pros vs Cons

Pros:

  • Built on top of simple, proven technology: HTML and CSS media queries.
  • Users see rendered markup at the correct breakpoint for their device, even before React has been loaded.

Cons:

  • If utilizing SSR rendering features, when the markup is passed down from the server to the client it includes all breakpoints, which increases the page size. (However, once the client mounts, the unused breakpoint markup is cleared from the DOM.)
  • The current media query is no longer something components can access; it is determined only by the props of the <Media> component they find themselves in.

That last point presents an interesting problem. How might we represent a component that gets styled differently at different breakpoints? (Let’s imagine a matchMedia example.)

<Sans size={sm ? 2 : 3}>
<>
  <Media at="sm">{this.getComponent("sm")}</Media>
  <Media greaterThan="sm">{this.getComponent()}</Media>
</>
getComponent(breakpoint?: string) {
  const sm = breakpoint === 'sm'
  return <Sans size={sm ? 2 : 3} />
}

We're still figuring out patterns for this, so please let us know if you have suggestions.

Development

This project uses auto-release to automatically release on every PR. Every PR should have a label that matches one of the following

  • Version: Trivial
  • Version: Patch
  • Version: Minor
  • Version: Major

Major, minor, and patch will cause a new release to be generated. Use major for breaking changes, minor for new non-breaking features, and patch for bug fixes. Trivial will not cause a release and should be used when updating documentation or non-project code.

If you don't want to release on a particular PR but the changes aren't trivial then use the Skip Release tag along side the appropriate version tag.

More Repositories

1

eigen

The Art World in Your Pocket or Your Trendy Tech Company's Tote, Artsy's mobile app.
TypeScript
3,480
star
2

eidolon

The Artsy Auction Kiosk App.
Swift
2,711
star
3

README

πŸ‘‹ - The documentation for being an Artsy Engineer
TypeScript
1,044
star
4

artsy.github.io

The Artsy Engineering Open-Source Developers Blog
SCSS
1,015
star
5

emission

⚠️ Deprecated repo, moved to artsy/eigen ➑️ React Native Components
TypeScript
621
star
6

force

The Artsy.net website
TypeScript
550
star
7

metaphysics

Artsy's GraphQL API
TypeScript
355
star
8

reaction

Artsy's publishing components
TypeScript
355
star
9

Emergence

TV. Shows.
Swift
353
star
10

garner

A set of Rack middleware and cache helpers that implement various caching strategies.
Ruby
347
star
11

scroll-frame

Retain your scroll position between pages using an iframe. Especially helpful for infinite scrolling views.
JavaScript
312
star
12

ezel

A boilerplate for Backbone projects that share code server/client, render server/client, and scale through modular architecture.
JavaScript
294
star
13

Swift-at-Artsy

Repo for the notes for Swift at Artsy
Swift
284
star
14

energy-legacy

LEGACY - Artsy Folio, The Partner iPhone / iPad app.
Objective-C
209
star
15

palette

Artsy's design system
TypeScript
203
star
16

react-redux-controller

Library for creating a controller layer to link React and Redux, on top of react-redux.
JavaScript
97
star
17

hokusai

Artsy's Docker / Kubernetes CLI and Workflow
Python
87
star
18

positron

Positron is Artsy Writer or the editorial tool and API for Artsy.
TypeScript
85
star
19

day-schedule-selector

A jQuery plugin to render a weekly schedule and allow selecting time slots in each day.
JavaScript
85
star
20

mobile

Mobile Team TODO
84
star
21

benv

Stub a browser environment in node.js and headlessly test your client-side code.
JavaScript
72
star
22

flare

Artsy iPhone Launch Marketing Page
CoffeeScript
63
star
23

team-navigator

An internal HR product for Artsy's team
JavaScript
62
star
24

sharify

Easily share data between Browserify modules meant to run on the server and client.
JavaScript
61
star
25

x-react-native

Conference Details for Artsy x React Native
TypeScript
49
star
26

backbone-super-sync

Isomorphic Backbone.sync adapter using superagent.
JavaScript
47
star
27

jenkins-backup-s3

A collection of scripts to backup Jenkins configuration to S3, as well as manage and restore those backups
Python
46
star
28

graphql-slack-updater

A weekly Travis task that sends our GraphQL updates to Slack
Ruby
40
star
29

meta

Artsy on Artsy.
39
star
30

Specs

The Artsy CocoaPods Specs
Ruby
37
star
31

doppler

Artsy.net developer website.
Ruby
37
star
32

detect-responsive-traits

Determine responsive traits to only server-side render markup truly needed.
TypeScript
36
star
33

gris

Gris is a framework for building hypermedia API services using Grape, Roar and ActiveRecord
Ruby
35
star
34

artsy-2013

The 2013.artsy.net static site using Node.js for some preprocessors.
CoffeeScript
33
star
35

the-art-genome-project

Gene names and definitions
JavaScript
33
star
36

elderfield

The Artsy Alexa (Echo) skill.
JavaScript
33
star
37

peril-settings

Artsy's peril settings
TypeScript
30
star
38

2014.artsy.net

CoffeeScript
25
star
39

artsy-ruby-client

Artsy API Ruby Client
Ruby
25
star
40

express-reloadable

Automatically hot-swap Express server code without the restart
JavaScript
25
star
41

Artsy-Authentication

Cocoa libraries dealing with Authentication for Artsy. Yawn, boring
Objective-C
22
star
42

dupe-report

A tool for reporting new webpack bundle duplicates to github and slack
TypeScript
21
star
43

Artsy-UILabels

This is our Artsy styled UILabel subclasses.
Objective-C
20
star
44

Artsy-OSSUIFonts

Open source variants of the Artsy fonts, wrapped as a CocoaPod
Beef
18
star
45

Mitosis

Artsy Chat Bot for Facebook Messenger
JavaScript
17
star
46

atomic-store

Atomic event store for Scala/Akka
Scala
17
star
47

momentum

Shared utilities for managing and deploying OpsWorks apps at Artsy. [DEPRECATED]
Ruby
15
star
48

orbs

CircleCI orbs used at Artsy
Shell
15
star
49

lint-changed

Lint files that have changed since master
TypeScript
14
star
50

artsy-eigen-web-association

A tiny app that serves the apple-app-site-association required for iOS Handoff related features.
JavaScript
14
star
51

javascriptures

Demo projects for javascripture sessions
JavaScript
14
star
52

estella

Make your Ruby objects searchable with Elasticsearch.
Ruby
13
star
53

echo

Eigen's floating administration lab up in the clouds
Shell
13
star
54

graphql-workshop

A workshop for understanding GraphQL by exploring Metaphysics
13
star
55

Artsy-UIColors

Consolidated UIColors used in the Artsy iOS Apps
Ruby
12
star
56

emission-nebula

Handles weekly deploys of Emission to TestFlight
Ruby
12
star
57

cohesion

Artsy's analytics schema
TypeScript
12
star
58

yeoman-generator-artsy

A Yeoman generate for Artsy CLI apps
JavaScript
11
star
59

stitch

Helps your Component and Template dependencies peacefully coexist
JavaScript
11
star
60

renovate-config

Shared renovate configurations
JavaScript
11
star
61

vscode-artsy

A VS Code extension for the Artsy Tech Stacks
11
star
62

money_helper

A simple module to assist in formatting unambiguous prices and price ranges in international currencies
Ruby
11
star
63

artsy-passport

Wires up the common auth handlers for Artsy's node based apps using http://passportjs.org.
JavaScript
10
star
64

aprb

[Deprecated] Artsy public radio notifications in Slack.
Elixir
10
star
65

next

Tools an utilites for Next.js by Artsy
JavaScript
10
star
66

eigen-artefacts

CocoaPods dependencies used in Eigen.
C++
10
star
67

Artsy-UIButtons

Artsy's UIButton subclasses
Objective-C
10
star
68

codemods

Various codemods used around Artsy
TypeScript
10
star
69

relay-workshop

Home for Artsy's Relay peer learning tutorials
TypeScript
9
star
70

studio

TypeScript
9
star
71

palette-zeplin-extension

See Artsy's palette components inside Zeplin directly
JavaScript
9
star
72

artsy.github.io-gatsby

re-write Engineering Blog
TypeScript
8
star
73

biesenbach

The Artsy Google Home application.
JavaScript
8
star
74

microgravity-deprecated

The Artsy.net mobile website:
CoffeeScript
8
star
75

palette-mobile

Artsy's Design System on Mobile
TypeScript
8
star
76

Aerodramus

An Objective-C API for interacting with artsy/echo
Objective-C
8
star
77

aprd

Artsy's Real-time Slack Notification Service (aka Artsy Public Radio)
Elixir
8
star
78

artsy-backbone-mixins

A library of Backbone mixins that DRY up some common domain logic and Artsy API rabbit holes.
JavaScript
8
star
79

energy

Artsy Folio, The Partner iPhone / iPad app.
TypeScript
7
star
80

backbone-cache-sync

Server-side Backbone.sync adapter that caches requests using Redis.
JavaScript
7
star
81

kaws

API for collections, our SEO optimized marketing landing pages
TypeScript
7
star
82

horizon

Visual representations of release pipelines
Ruby
7
star
83

guides

The Engineers Guide to the Artsy Universe
7
star
84

hack-playdate-gallery

Lua
7
star
85

forque

A home for modern admin UIs; successor to Torque
TypeScript
7
star
86

APR

Artsy Private Radio
Elixir
7
star
87

browserify-dev-middleware

Middleware to compile browserify files on request for development purpose.
JavaScript
7
star
88

artsy-xapp

Tiny lib to fetch and refresh an xapp token from Artsy
JavaScript
7
star
89

auto-config

Artsy's shared auto release config
6
star
90

volley

Datadog StatsD ingestion for client-side data
TypeScript
6
star
91

rosalind

Admin app for batch operations on genomes
JavaScript
6
star
92

bucket-assets

Uploads a folder of static assets to an s3 bucket with convenient defaults.
JavaScript
6
star
93

extraction

UI components shared between Eigen and Emission.
Objective-C
6
star
94

to-title-case

Capitalizes your titles as per The Chicago Manual of Style
TypeScript
6
star
95

miasma

Smoke tests for Artsy, and possible successor to Vapor
JavaScript
6
star
96

update-repo

JS library for updating other repositories
TypeScript
6
star
97

cli

TypeScript
6
star
98

convection

Rails application for consignments
Ruby
5
star
99

performance-monitor

TypeScript
5
star
100

graphql-page_cursors

Add page cursors to your Rails GQL schema
Ruby
5
star