Giter Site home page Giter Site logo

artsy / fresnel Goto Github PK

View Code? Open in Web Editor NEW
1.2K 41.0 64.0 2.45 MB

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

Home Page: https://artsy.github.io/blog/2019/05/24/server-rendering-responsively

License: Other

TypeScript 100.00%
react responsive media-queries ssr nextjs react-18

fresnel's Introduction

@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

Installation

  # React 18+
  yarn add @artsy/fresnel

  # React 17
  yarn add @artsy/fresnel@6

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.

fresnel's People

Contributors

alloy avatar alyustik avatar artsyit avatar autarc avatar chandelieraxel avatar chrissantamaria avatar damassi avatar dblandin avatar dependabot[bot] avatar dwilt avatar icirellik avatar inomdzhon avatar jhamberg avatar joeyaghion avatar kirkness avatar l2succes avatar mc-jones avatar mdole avatar mgreenw avatar ovasdi avatar pepopowitz avatar redbar0n avatar renovate-bot avatar renovate[bot] avatar roderickhsiao avatar russcoder avatar snyk-bot avatar tb-campbell avatar wendtcode avatar zephraph avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fresnel's Issues

Warning: Prop `className` did not match. on NextJs

Warning: Prop className did not match. Server: "MuiButtonBase-root MuiIconButton-root makeStyles-prevIconButton-29" Client: "MuiButtonBase-root MuiIconButton-root makeStyles-prevIconButton-20"

This is the warning I started getting once I created different header for two different breakpoints using the component. When this warning pops up, it breaks all the CSS in the application.

I noticed that if I comment out one of the two components I get this warning only when requesting the page at the commented out breakpoint, like the following picture:
Screenshot 2021-02-10 at 14 56 44 (2)

I had a similar problem with apollo-cache-persist, when reloading a page the CSS would always break, I solved with a different initialization, but I can't seem to find a working solution for this specific problem.

Doesn't work on Internet Explorer

Hello,

I tried the examples and they do not work on Internet Explorer v11. I am using this package in production and recently i discovered this which is a very big problem for me.
Unfortunately i do not have more information about the root cause of this issue.

Best regards,
Mitko

[Feature Request] Optional container-less rendering by using React.Fragments

Feature request

To optionally render by fragments instead of containers with the <Media /> tag

CSS Flexbox can result in pretty ugly css if you have to deal with multiple optional containers for any given component. It is made worse when another container is thrown in to handle responsiveness can lead to significant headaches driving users away from using this project. I personally created this hell for myself with a page builder that can have nested elements break out of its containers widths, or apply container effects, so adding your project in, while exactly the kind of behavior I was looking for, broke layout and made me eye a bottle of whiskey longingly. With your project added, it could be anywhere between 2 to 4 containers deep at any component, leading to some seriously gnarly css.

Trust me, I plan on reconciling this container madness, however, I haven't found a good ancient IE friendly way for what I am doing. So, beatings will continue on that front another day. Anyway...

Proposal

An optional flag fragment for <Media /> that uses a React fragment and child mapping to apply the fresnel classes to child components.

Two obvious drawbacks form from this:

  • Text nodes can't have classes applied to them. So, easy change for that-- automatically wrap it with an element. Currently rendering a span that applies the fresnel classes, but I'm not sure if it should be customizable, e.g. fragment="span" or fragment={{text: (t, klass) => <span className={klass}>{t}</span>}} to handle cases where users would want to specify how to handle text nodes.
  • All child components must accept and apply a className prop that is modified automatically to append (or create) a className on the component. This is an easy fix for child components, however, it does require some kind of notification in documents components within a, e.g. <Media fragment at="sm" /> must have this behavior. Without accepting and applying a className prop, elements will not have the appropriate fresnel-* breakpoint class causing it to be visible before hydration, as well as triggering hydration warnings when react notices those same elements that will be deleted due to the breakpoint constraints.

Rollin my own

I've already started work on this via fork https://github.com/xiata/fresnel where it appears to be working against React 16.13.1.

I imagine you've been down this road before, I'd like to know of any bombs to throw at this if you know of any. Thanks!

Also-- I've noticed a lack of tests, what's your preferred framework so I can write some tests against it?

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Repository problems

These problems occurred while renovating this repository. View logs.

  • WARN: Using npm packages for Renovate presets is now deprecated. Please migrate to repository-based presets instead.

Pending Approval

These branches will be created by Renovate only once you click their checkbox below.

  • chore(deps): pin dependencies (@types/node, @types/react, @types/react-dom, tslint, typescript, webpack, webpack-cli, webpack-dev-middleware, webpack-dev-server)
  • chore(deps): pin dependencies (react, react-dom, react-test-renderer)
  • chore(deps): update dep @types/jest from 23.3.1 to v23.3.14
  • chore(deps): update dep @types/react-test-renderer from 17.0.0 to v17.0.9
  • chore(deps): update dep chalk from 2.4.1 to v2.4.2
  • chore(deps): update dep styled-components from 3.4.5 to v3.4.10
  • chore(deps): update dep typescript-styled-plugin from 0.18.1 to v0.18.3
  • chore(deps): update dep webpack-cli from 3.3.2 to v3.3.12
  • chore(deps): update dep webpack-dev-middleware from 3.7.0 to v3.7.3
  • chore(deps): update babel (@babel/cli, @babel/core, @babel/node, @babel/plugin-proposal-class-properties, @babel/preset-env, @babel/preset-react, @babel/preset-typescript, @babel/register)
  • chore(deps): update dep @types/express from 4.16.0 to v4.17.21
  • chore(deps): update dep @types/node from 10.9.4 to v10.17.60
  • chore(deps): update dep @types/react-dom to v18.3.0
  • chore(deps): update dep @types/webpack-dev-server from 3.1.1 to v3.11.6
  • chore(deps): update dep babel-jest from 23.4.2 to v23.6.0
  • chore(deps): update dep babel-loader to v8.3.0
  • chore(deps): update dep jest-styled-components from 6.2.2 to v6.3.4
  • chore(deps): update dep prettier from 1.17.1 to v1.19.1
  • chore(deps): update dep tslint-config-prettier from 1.15.0 to v1.18.0
  • chore(deps): update dep webpack from 4.46.0 to v4.47.0
  • chore(deps): update dep @types/node to v20.12.8
  • chore(deps): update dep @types/react to v18.3.1
  • chore(deps): update dep @types/react-test-renderer from 17.0.0 to v18
  • chore(deps): update dep babel-jest from 23.4.2 to v29
  • chore(deps): update dep babel-loader to v9.1.3
  • chore(deps): update dep chalk from 2.4.1 to v5
  • chore(deps): update dep concurrently from 3.6.1 to v8
  • chore(deps): update dep husky from 0.14.3 to v9
  • chore(deps): update dep jest from 24.9.0 to v29 (jest, @types/jest)
  • chore(deps): update dep jest-styled-components from 6.2.2 to v7
  • chore(deps): update dep lint-staged from 7.3.0 to v15
  • chore(deps): update dep next to v14.2.3
  • chore(deps): update dep prettier from 1.17.1 to v3
  • chore(deps): update dep styled-components from 3.4.5 to v6
  • chore(deps): update dep tslint-react from 3.6.0 to v5
  • chore(deps): update dep typescript to v5.4.5
  • chore(deps): update dep webpack from 4.46.0 to v5
  • chore(deps): update dep webpack-cli from 3.3.2 to v5
  • chore(deps): update dep webpack-dev-middleware to v7.2.1
  • chore(deps): update dep webpack-dev-server from 4.2.1 to v4.15.2 (webpack-dev-server, @types/webpack-dev-server)
  • chore(deps): update dep webpack-dev-server to v5.0.4
  • 🔐 Create all pending approval PRs at once 🔐

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

circleci
.circleci/config.yml
  • yarn 6.5.0
  • auto 2.1.0
npm
examples/basic/package.json
  • @babel/node ^7.4.5
  • express ^4.17.1
  • react ^18.2.0
  • react-dom ^18.2.0
  • @babel/core 7.4.5
  • @babel/plugin-proposal-class-properties 7.4.4
  • @babel/preset-env 7.4.5
  • @babel/preset-react 7.0.0
  • @babel/preset-typescript 7.3.3
  • @babel/register 7.4.4
  • @types/react ^18.0.17
  • @types/react-dom ^18.0.6
  • babel-loader 8.0.6
  • webpack ^5.89.0
  • webpack-cli 3.3.2
  • webpack-dev-middleware 3.7.0
examples/kitchen-sink/package.json
  • @artsy/detect-responsive-traits 0.1.0
  • @babel/node ^7.4.5
  • express ^4.17.0
  • react ^18.2.0
  • react-dom ^18.2.0
  • @babel/core 7.4.5
  • @babel/plugin-proposal-class-properties 7.4.4
  • @babel/preset-env 7.4.5
  • @babel/preset-react 7.0.0
  • @babel/preset-typescript 7.3.3
  • @babel/register 7.4.4
  • @types/react ^18.0.17
  • @types/react-dom ^18.0.6
  • babel-loader 8.0.6
  • webpack ^5.89.0
  • webpack-cli ^5.1.4
  • webpack-dev-middleware ^6.1.1
  • webpack-dev-server ^4.15.1
examples/nextjs/app-router/package.json
  • react ^18
  • react-dom ^18
  • next 14.1.0
  • typescript ^5
  • @types/node ^20
  • @types/react ^18
  • @types/react-dom ^18
examples/nextjs/pages-router/package.json
  • next ^12.2.4
  • react ^18.2.0
  • react-dom ^18.2.0
  • @types/react ^17.0.37
  • typescript 4.9.5
examples/ssr-rendering/package.json
  • @babel/node ^7.4.5
  • express ^4.17.0
  • react ^18.2.0
  • react-dom ^18.2.0
  • @babel/core 7.4.5
  • @babel/plugin-proposal-class-properties 7.4.4
  • @babel/preset-env 7.4.5
  • @babel/preset-react 7.0.0
  • @babel/preset-typescript 7.3.3
  • @babel/register 7.4.4
  • @types/react ^18.0.17
  • @types/react-dom ^18.0.6
  • babel-loader 8.0.6
  • webpack ^5.89.0
  • webpack-cli 3.3.2
  • webpack-dev-middleware 3.7.0
package.json
  • @artsy/auto-config 1.2.0
  • @artsy/detect-responsive-traits 0.1.0
  • @babel/cli 7.15.4
  • @babel/core 7.0.0
  • @babel/node 7.0.0
  • @babel/plugin-proposal-class-properties 7.1.0
  • @babel/preset-env 7.0.0
  • @babel/preset-react 7.0.0
  • @babel/preset-typescript 7.0.0
  • @types/chalk 2.2.0
  • @types/express 4.16.0
  • @types/jest 23.3.1
  • @types/node 10.9.4
  • @types/react ^18.0.17
  • @types/react-dom ^18.0.6
  • @types/react-test-renderer 17.0.0
  • @types/webpack-dev-server 3.1.1
  • babel-core 7.0.0-bridge.0
  • babel-jest 23.4.2
  • babel-loader 8.0.4
  • babel-preset-env 1.7.0
  • chalk 2.4.1
  • concurrently 3.6.1
  • express 4.19.2
  • husky 0.14.3
  • jest 24.9.0
  • jest-styled-components 6.2.2
  • lint-staged 7.3.0
  • prettier 1.17.1
  • react ^18.2.0
  • react-dom ^18.2.0
  • react-test-renderer ^18.2.0
  • static-extend 0.1.2
  • styled-components 3.4.5
  • tslint ^6.1.3
  • tslint-config-prettier 1.15.0
  • tslint-react 3.6.0
  • typescript 4.9.5
  • typescript-styled-plugin 0.18.1
  • webpack 4.46.0
  • webpack-dev-server 4.2.1
  • react >=18.0.0

  • Check this box to trigger a request for Renovate to run again on this repository

Styled-components

Hi, how can I implement with styled components

const Text = styled.div`
  font-size: ${(props) => (props.isMobile ? 26 : 54)}px;
  color: #ffffff;
  font-weight: bold;
`

`<img>` and `<video>` tags in media queries

When using media queries to load different images or videos per viewport, correct assets are rendered in browser but all of them are being loaded which is a performance issue.

E.g.:

<Media at="sm">
  <img src="https://dummyimage.com/600x400/000/fff.png&text=Image+1"/>
</Media>
<Media greaterThan="sm">
  <img src="https://dummyimage.com/600x400/000/fff.png&text=Image+2"/>
</Media>

(This particular example could be solved by using <picture> tag instead but the problem persists in case of using <video> tag or Next/Image)

In this case Image+1 is shown on smaller viewports, Image+2 on larger ones but if you look at the networks tab in dev tools both of them are being loaded.

This behavior can also be observed in the examples in the repo (e.g. /examples/nextjs or /examples/ssr-rendering) by packing some media contents inside of the media queries.

Match media not removing parent fresnel containers

Description

We are using the createMedia API to create an SSR friendly solution in react-native-web. Multiple fresnel parent containers are still rendering in the browser even after the initial mount (Screenshot below).

Screen Shot 2020-08-12 at 4 51 06 PM

Is this intended behavior? I'm just getting familiar with fresnel any feedback would be great.

[RFC] Property aliases

@alloy - based on this comment: artsy/reaction#1466 (comment)

I wonder if it makes sense to use greaterThanOrEqual here for if/when we ever add a bigger breakpoint?

Wondering if you have any thoughts on adding some property aliases so that things are a bit less verbose? Something like:

<Media greaterThan='xs'>

could also be written like

<Media gt='xs'>

or

<Media greaterThanOrEqual='sm'>

being used like

<Media gte='sm'>

Same goes for lessThan -> lt.

Having the ability to use both would be a nice touch I think.

cc @zephraph, @javamonn

Compatibility with React Native Web + NextJS

Using artsy/fresnel with RNW in next.js had a few problems:

necolas/react-native-web#1688 (comment)

1: The wrapping DOM element

  1. the lib appends div as a wrapping element in the DOM, which is not a RNW component so you need to apply hacky styles to the wrapper div at various places if you combine them

RNW has deprecated all uses of the className prop (since RNW's style will overwrite it). But Fresnel uses the className prop (Media.tsx). RNW also requires data-* attributes to be explicitly set with the dataSet prop. So what I think is needed is an option to config Fresnel to work with RNW. Enabling that config could make the <Media> components output a wrapping:

<View dataSet={( fresnel-container: "true" breakpoint: "sm" )}>

Instead of:

<div class="fresnel-container fresnel-at-sm">

So that on web RNW would output the final:

<div data-fresnel-container="true" data-breakpoint="sm">

Which Fresnel could target using a CSS attribute selector like this:

[data-fresnel-container="true"][data-breakpoint="sm"] {
  display:none !important;
}

To hide components for non-applicable breakpoints on the first render client-side.

Actually, I see that you can use the render props form, which can output a custom component other than a <div>.

// in RNW then View will output either for native or for web
<Media greaterThan="sm">
        {(className, renderChildren) => {
          return (
            <View dataSet={( fresnel-container: "true" breakpoint: className )}>
              {renderChildren ? "Hello Native or Web!" : null}
            </View>
          )
        }}
</Media>

That's good. The only problem is that on web, Fresnel CSS will target the class attribute. But RNW doesn't allow setting this directly, as mentioned initially. So if Fresnel's CSS could target data-* attributes, like <div data-fresnel-container="true" data-breakpoint="sm">, with a CSS attribute selector, like mentioned above, it would be compatible with RNW.

Is that possible? Maybe as a config option?

2: Inline responsive styles

  1. the lib provides no easy way to apply responsive inline styles to a component (e.g. <View style={[isMobile && { color: 'red' }] />)

I think speaks to the last con with using Fresnel that you mentioned in your Readme:

The current media query is no longer something components can access; it is determined only by the props of the component they find themselves in.

This makes it harder to style components conditionally.

You asked:

How might we represent a component that gets styled differently at different breakpoints?

And gave an example:

<>
  <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} />
}

Maybe this is a stupid question, but for cases like that example, couldn't you simply do this?

<>
  <Media at="sm">
    <Sans size={2} />
  </Media>
  <Media greaterThan="sm">
    <Sans size={3} />
  </Media>
</>

Anyway, I think that

<Sans size={sm ? 2 : 3} />

is actually really nice to have as an optional API. Because it allows very fine-grained targeting of styles, without worrying about larger markup differences. Maybe you have only one component tree / markup, but want to simply tweak some styles for various resolutions (classic responsivity).

So what about this?

When rendering server-side, put the current breakpoints in a DOM node outside of the React tree, using a vanilla JS DOM selector. So that it is accessible on the client, and doesn't interfere with React's hydration (which has to match server-side). Then provide a hook clientWindowMatchesBreakpoint() that only on the client will access the breakpoints from that DOM node, and run something like this:

const isAppliedBreakpoint = bp => {
  if (onClient()) {
    return clientWindowMatchesBreakpoint(bp)
  } else {
    return isUserAgentBreakpoint(bp)
}
const onClient = () => typeof window !== ‘undefined’
const clientWindowMatchesBreakpoint = bp => {
  // should work if window is resized later
  window.matchMedia((max-width: {getBreakpointFromDomNode(bp).width} )).matches
}
const isUserAgentBreakpoint = (bp) => { /* compare with the breakpoint @artsy/detect-reponsive-traits selected based on user-agent (which Fresnel also sent to the client in the DOM node) */ }

Then when styling components conditionally developers could do:

<Sans size={isAppliedBreakpoint("sm") ? 2 : 3} />

Maybe?

WebKit bug with `display: contents`

There’s a bug in WebKit (Safari 11 & 12) that makes it so content that has been displayed with display: contents never hide again when display: none is applied afterwards. (A reproduction can be found here).

Either way, currently this feature isn’t supported on the current Edge browser, which is probably a browser people want to be able to target.

I guess in 6 years time we can make safely make use of this feature 🤷‍♂️

So for now we’re going to change this to use more traditional display values, but it will require some API additions. C’est la vie.

Intelligently SSR Responsive Children of Responsive Parents

tweaking the basic example

const { MediaContextProvider, Media } = createMedia({
  breakpoints: {
    sm: 0,
    md: 768,
  },
})

const App = () => (
  <MediaContextProvider>
    <Media at="sm">
      <Child />
    </Media>
    <Media at="md">
      <Child />
    </Media>
  </MediaContextProvider>
)

const Child  = () => (
  <>
    <Media at="sm">
      foo
    </Media>
    <Media at="md">
      bar
    </Media>
  </>
)

it actually renders foo twice and bar twice:

<div class="fresnel-container fresnel-at-sm">
  <div class="fresnel-container fresnel-at-sm">foo</div>
  <div class="fresnel-container fresnel-at-md">bar</div>
</div>
<div class="fresnel-container fresnel-at-md">
  <div class="fresnel-container fresnel-at-sm">foo</div>
  <div class="fresnel-container fresnel-at-md">bar</div>
</div>

but wouldn't it be possible to know that the second foo would never be visible?

this multiplicative growth could even make some trees larger than simply rendering the entire tree for each breakpoint on the server (also allowing the benefit that lower components can use the current breakpoint as a prop rather than wrapping everything in fresnel).

If you createMedia() with only one breakpoint you cannot display two different components using "lessThan" and "greaterThanOrEqual".

If you createMedia() with only one breakpoint you cannot display two different components using "lessThan" and "greaterThanOrEqual".

I would have assumed if you want to display only one of two different components, you could define one breakpoint and then use "lessThan" for one Media component and "greatherThanOrEqual" for the other. But, if you do that, the "lessThan" Media will not be displayed unless you define another smaller breakpoint.

The test program below will show either "Mobile" or "Desktop" depending on whether width is <1001 or >=1001.

If you comment out the breakpoint "mobile: 0", then nothing is displayed below 1001 width.

https://codesandbox.io/s/pensive-snyder-y6ubj?file=/src/App.js

Doesn't work with NextJs

The library used to work fine with NextJs but since 10 or 10.1 (I think) it stopped working. Today I tried that again with the new releases of both packages but it still doesn't. What I mean by that is that both of the following get rendered:

<Media greaterThanOrEqual='md'>greaterThanOrEqual</Media>
<Media lessThan='md'>lessThan</Media>

Possible issue with `interaction` prop in Media component

While working in Reaction yesterday I attempted to replace our deprecated <Responsive> component with the new <Media> component, which specifically guarded against favoriting interactions unless hover was enabled in the browser. Something like:

<Media interaction='hover'>
  ... code to render ...
</Media>

However, when hovering the guarded component would not appear, though it was written to the dom with display: none. Toggling that value to visible still left the interaction handlers unbound, so mousing over had no effect.

See artsy/reaction#1970 (comment) for more info.

matchMedia doesn't seem to be working with mediaClassNames

Hey All,

When trying to use the method described for passing mediaClassNames to my own components in order to avoid the extra div, it seems that all content is always present. I was under the impression that an eventListener and call to matchMedia should be selectively rendering only the matching components after rehydration. Is this a bug or am I mistaken as to how this should be working?

Also, I can't seem to understand how the onlyMatch prop works. Any clarity you can provide there?

Thanks,

-Josh-

Loading All JS Chunks for all Screen Size

Hey, thanks for this great library,
I was doing some POC before updating from react-responsive

I found that it's loading JS for all screen sizes even when code splitting is done.

const ContactUs = dynamic(() => import(/* webpackChunkName: "magic-contact" */ "./contact-us"));
const AboutUs = dynamic(() => import(/* webpackChunkName: "magic-about" */ "./about-us"));

function XC() {
  React.useEffect(() => {
    console.log("XC");
  }, []);
  return <AboutUs />;
}

function YV() {
  React.useEffect(() => {
    console.log("YV");
  }, []);
  return <ContactUs />;
}

export default function HomePage() {
  return (
    <MediaContextProvider>
      <Media at="xs"><XC /></Media>
      <Media greaterThan="xs"><YV /></Media>
    </MediaContextProvider>
  );
}

It's loading both the JS
image

Is this right, or I am doing something wrong?
Because if we load both the js, then the page will become slow.

Option to disable SSR

First I'd like to say thank you for this library and its unique approach to solving SSR media-queries.

I'm wondering if its possible to add a prop option to disable SSR rendering for improved performance and a more concise API. In portions of my site I know that certain components will only be rendered on the client. For these components rather than rendering all breakpoint possibilities I'd like to only render one and have access to the props.

Something roughly like:

render() {
   return (
      <MediaClientSide greaterThanOrEqual="tabletLarge">
          {(props) => (
             <Component size={props.tabletLarge ? 'small' : 'large' />
          )}
      </MediaClientSide>
   );
}

At the moment i'll have to use 2 libraries together to achieve this. It would be awesome if frensel could operate in both modes.

Is this a possibility?

Kind regards,

Make invalid states unrepresentable

Hello and thank you for your work!

I really like the API of your Media component. I think I know how we can improve its type safety.

Problem

Let's assume we're using 4 breakpoints: ['sm', 'md', 'lg', 'xl'].

With breakpoints like these, the following states look like mistakes:

// What would you expect to happen? Shouldn't you use `at="sm"` instead?
<Media between={["sm", "sm"]}>ohai</Media>

// Here, the order is descending. Will "from large to small" be properly interpreted?
<Media between={["lg", "sm"]}>ohai</Media>

// There is no breakpoint smaller than "sm" — what's going to happen?
<Media lessThan="sm">ohai</Media>

Solution

Tuple operations are easier in the upcoming TypeScript 4. If we could redefine B to be a tuple of strings (as opposed to a union of strings — the order is important), we could eliminate those unwanted states.

const props: MediaBreakpointProps<['sm', 'md', 'lg', 'xl']> = {
  lessThan: 'sm', // Compile-time error (there is nothing left from 'sm')
}
const props: MediaBreakpointProps<['sm', 'md', 'lg', 'xl']> = {
  greaterThan: 'xl', // Compile-time error (there is nothing right from 'xl')
}

With a bit of magic, we can even simulate dependent types and... type the between prop!
In our example, the only valid pairs for between are:

type Between =
  | ['sm', 'md']
  | ['sm', 'lg']
  | ['sm', 'xl']
  | ['md', 'lg']
  | ['md', 'xl']
  | ['lg', 'xl'];

We can pre-compute the valid pairs ahead of type. See demo:

Kapture 2020-07-06 at 17 11 17

As you can see, the value on the right must be greater than the value on the left.

TypeScript Playground

Downsides

  1. It's a breaking change. config.breakpoints should now accept an array instead of an object. This is necessary, since order matters, and TypeScript has no recollection of the order of properties on an object.
  2. It depends on TypeScript 4. I could probably make it work with older version, only the implementation would be more obscure.
  3. It adds complexity. Especially the Between type. It uses a trick only few people know about.

tl;dr

I'm happy to open a PR for you, but because my solution adds complexity, I decided to ask what do you think first!

Mocking `matchMedia` in jest causes component tree not to render

Mocking matchMedia in Jest causes children of <Media /> components not to render.

In our application we have a useMedia() hook which uses matchMedia. In order to test, we've mocked this in our Jest setup file (similar to this). But when testing (using testing-library), the component is not accessible.

Any thoughts on how to get around this issue?

Open source this repo

This repo is closed right now, and it should be opened as soon as we can. Artsy practices Open Source by Default which, as described here means that our stuff should be open unless there's a reason to keep it secret.

A common response from Artsy engineers when I open this kind of ticket is "we just want to clean up the code before opening it up", to which I say: our messy setup and cleanup should be open by default, too. When we open sourced Eigen, we cleaned the repo of secrets and then immediately opened it, with all our cleanup happening after it was public. We should do the same with this repo, as quickly as possible.

Child nodes are missing at certain window widths with Next JS

Using the Next JS example, the child nodes are not rendered when window is set to certain widths.

Environment
Browsers: Firefox, Chrome
Framework: Next.js 11.1.2 and 10.0.3 (only versions tested)

Description
Resizing the window resulted in flickering which upon further review resulted in certain window widths in which the child node was not rendered even though the artsy fresnel wrapper div was rendered and styled to display In other words, the artsy-fresnel wrapper div is there, but is empty at certain window widths. The example below produces the issue and is a duplicate of the the Next.js example provided in this repository copied into a project created with create-next-app.

screenshot
example

Getting error when resizing to mobile

Error

Unhandled Runtime Error
NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

Call Stack
removeChild
node_modules/react-dom/cjs/react-dom.development.js (7601:0)
unmountHostComponents
node_modules/react-dom/cjs/react-dom.development.js (20449:0)
commitDeletion
node_modules/react-dom/cjs/react-dom.development.js (20500:0)
commitMutationEffects
node_modules/react-dom/cjs/react-dom.development.js (22782:0)
HTMLUnknownElement.callCallback
node_modules/react-dom/cjs/react-dom.development.js (188:0)
Object.invokeGuardedCallbackDev
node_modules/react-dom/cjs/react-dom.development.js (237:0)
invokeGuardedCallback
node_modules/react-dom/cjs/react-dom.development.js (292:0)
commitRootImpl
node_modules/react-dom/cjs/react-dom.development.js (22509:0)
unstable_runWithPriority
node_modules/scheduler/cjs/scheduler.development.js (653:0)
runWithPriority$1
node_modules/react-dom/cjs/react-dom.development.js (11039:0)
commitRoot
node_modules/react-dom/cjs/react-dom.development.js (22381:0)
finishSyncRender
node_modules/react-dom/cjs/react-dom.development.js (21807:0)
performSyncWorkOnRoot
node_modules/react-dom/cjs/react-dom.development.js (21793:0)
eval
node_modules/react-dom/cjs/react-dom.development.js (11089:0)
unstable_runWithPriority
node_modules/scheduler/cjs/scheduler.development.js (653:0)
runWithPriority$1
node_modules/react-dom/cjs/react-dom.development.js (11039:0)
flushSyncCallbackQueueImpl
node_modules/react-dom/cjs/react-dom.development.js (11084:0)
flushSyncCallbackQueue
node_modules/react-dom/cjs/react-dom.development.js (11072:0)
scheduleUpdateOnFiber
node_modules/react-dom/cjs/react-dom.development.js (21199:0)
Object.enqueueSetState
node_modules/react-dom/cjs/react-dom.development.js (12639:0)
ResponsiveProvider.Component.setState
node_modules/react/cjs/react.development.js (471:0)
MediaQueryList.eval
node_modules/@artsy/fresnel/dist/DynamicResponsive.js (82:0)

This is my layout:

pages/products/index.js

// @flow
import React, { Fragment } from 'react';
import {
  Container,
  Header,
  Segment,
  Grid,
  Button,
  Image,
  Divider,
  Responsive
} from 'semantic-ui-react';
import Link from 'next/link';
import Layout from 'components/Layout';
import ResponsiveContainer from 'components/ResponsiveContainer';
import FooterGrid from 'components/FooterGrid';
import ErrorDetail from 'components/ErrorDetail';
import { Media } from 'components/media';
import Interweave from 'interweave';
import { makeUrl } from 'lib/common';
import withApollo, { clientFn } from 'lib/withApollo';
import gql from 'graphql-tag';
import groupBy from 'lodash.groupby';

const Products = (props: any) => {
  const { result } = props;

  const mainProducts =
    result &&
    result.false.map((product, key) => {
      return (
        <Segment
          key={key}
          padded
          size="large"
          style={{ background: '#f9f9f9' }}>
          <Container>
            <Media lessThan="tablet">
              <Image
                src={makeUrl(`${product.icon.url}`)}
                size="tiny"
                centered
              />
              <Header textAlign="center">
                <Link href={`/products/${product.slug}`}>
                  <a
                    style={{
                      color: 'black',
                      fontSize: '1.3em'
                    }}>
                    {product.name}
                  </a>
                </Link>
                <Header.Subheader>{product.slogan}</Header.Subheader>
              </Header>
            </Media>
            <Media greaterThanOrEqual="tablet">
              <Header>
                <Image
                  src={makeUrl(`${product.icon.url}`)}
                  floated="left"
                  size="large"
                />
                <Link href={`/products/${product.slug}`}>
                  <a
                    style={{
                      color: 'black',
                      fontSize: '1.3em'
                    }}>
                    {product.name}
                  </a>
                </Link>
                <Header.Subheader>{product.slogan}</Header.Subheader>
              </Header>
            </Media>
            <Divider hidden />
            <Interweave content={product.description} />
          </Container>
        </Segment>
      );
    });

  const otherProducts =
    result &&
    result.true.map((product, key) => {
      return (
        <Segment vertical size="tiny" padded="very" key={key}>
          <Container>
            <Header>
              <Link href={`/products/${product.slug}`}>
                <a
                  style={{
                    color: 'black',
                    fontSize: '1.3em'
                  }}>
                  {product.name}
                </a>
              </Link>
            </Header>
            <p>{product.description}</p>
            <Button>
              <Link href={`/products/${product.slug}/releases`}>
                <a>Descargas</a>
              </Link>
            </Button>
          </Container>
        </Segment>
      );
    });

  return (
    <Layout title="Productos">
      <ResponsiveContainer>
        <Segment vertical basic padded>
          <Container>
            <Header as="h1">
              Productos
              <Header.Subheader>
                La cartera de productos que ofrecemos
              </Header.Subheader>
            </Header>
          </Container>
        </Segment>
        <Segment vertical padded>
          <Container>
            {result && (
              <Fragment>
                <Media greaterThanOrEqual="tablet">
                  <Grid>
                    <Grid.Row>
                      <Grid.Column width={10}>{mainProducts}</Grid.Column>
                      <Grid.Column width={6}>{otherProducts}</Grid.Column>
                    </Grid.Row>
                  </Grid>
                </Media>
                <Media lessThan="tablet">
                  {mainProducts}
                  {otherProducts}
                </Media>
              </Fragment>
            )}
          </Container>
        </Segment>
        <FooterGrid />
      </ResponsiveContainer>
    </Layout>
  );
};

export const PRODUCTS_QUERY = gql`
  query {
    products(where: { discontinued: false }) {
      book {
        slug
      }
      icon {
        url
      }
      slug
      name
      slogan
      description
    }
  }
`;

export async function getStaticProps() {
  const client = clientFn();
  var obj = {};

  const productsResult = await client.query({
    query: PRODUCTS_QUERY
  });
  const grouped = groupBy(productsResult.data.products, p => p.book === null);
  obj.result = grouped;

  return {
    props: obj
  };
}

export default withApollo(Products);

components/ResponsiveContainer.js

// @flow
import React from 'react';
import DesktopContainer from './DesktopContainer';
import MobileContainer from './MobileContainer';
import { MediaContextProvider } from './media';

const ResponsiveContainer = (props: any) => {
  const { desktop, mobile, children } = props;
  return (
    <MediaContextProvider>
      <DesktopContainer {...desktop}>{children}</DesktopContainer>
      <MobileContainer {...mobile}>{children}</MobileContainer>
    </MediaContextProvider>
  );
};

export default ResponsiveContainer;

components/media.js 

import { createMedia } from '@artsy/fresnel';

const AppMedia = createMedia({
  breakpoints: {
    mobile: 320,
    tablet: 768,
    computer: 992,
    largeScreen: 1200,
    widescreen: 1920,
  },
});

export const { MediaContextProvider, Media } = AppMedia;
export const mediaStyles = AppMedia.createMediaStyle();

Add example that constrains SSR for mobile devices based on user-agent

Using a list of user-agents/device-sizes like this, we want to be able to limit SSR to just what the device needs.

It could be nice to add a small function to the API that gives you the list of breakpoints for a given device width. Might be overkill, though, as it’s also easy to write that function in your app based on your own list of breakpoints.

React v17

My current project is running react 17 and I am unable to install @arsty/fresnel due to a dependency on react 16.3.0.

Any idea when you may be updating this dependency?

Thanks

How to render based on a JS client side variable alongside this package?

Hi,

Great work on this. There is a parallel problem that I am facing.

The issue is that I render a slightly different Header navigation bar after checking if the user is logged in or not. But I cannot know this on the server side. So I get a hydration warning when doing server side rendering. I am using this alongside your package which allows me to show the navigation bar is collapsed or expanded state depending on whether mobile or desktop.

Orientation media query

Hi,

I'm using this package and I stumbled upon weird behaviour defining interactions. I took over the docs example:

export const interactions: { [key in Interaction]: string } = {
  landscape: "(orientation: landscape)",
  portrait: "(orientation: portrait)",
};

Assuming that this would work like this:

<Media interaction="portrait"> 
    <p>portrait view</p>
</Media>
<Media interaction="landscape"> 
    <p>landscape view</p>
</Media>

However, it is working the other way round which had me switch the interactions config up:

export const interactions: { [key in Interaction]: string } = {
  landscape: "(orientation: portrait)",
  portrait: "(orientation: landscape)",
};

It seems that, looking at the docs, this is not intentional and a bug.

Media flickering on load

There are elements shown when loading and then disappearing. Here's a video to demonstrate the issue. I assume it's the CSS media-breakpoints being loaded after the elements are on the site. Any suggestions for how to solve this?

Question: effects on performance / SEO?

First of all, thank you so much for this library. I'm building a responsive design system for React Native (+ Web) and this project is a life-saver for SSR support and good web integration.

I came across this project here: necolas/react-native-web#1318

I have a question regarding the approach of rendering every breakpoint from the server. I definitely agree in principle that this is the best solution to the many trade-offs.

That said, in your experience, is there any significant speed delay loading a website that might have 4x the content? Since I'll be using 4 breakpoints to render the same content with different styles, this would be my experience.

Also, have you had any negative experience SEO-wise printing the same content multiple times on a page?

I haven't done enough testing to know if either of the above are real problems. I just figured I'd ask people who have been using fresnel if they ran into either, or if they're non-issues altogether.

Thanks again!

Use gatsby-plugin-fresnel in examples

👋 really been enjoying using this lib! The included Gatsby example was nice, though I personally wanted something simpler for my own projects so I created gatsby-plugin-fresnel. At its core it's doing the same sort of style injection as the current example, though its usage is much simpler.

If you'd like, I'm happy to make a PR to promote usage of this in the README + update the example to use the plugin. No worries if you'd rather keep these separate.

Rename to have a better branded name and [let's be honest] is cuter

In response to artsy/artsy.github.io#574 (comment) and to unblock artsy/artsy.github.io#566 (comment)

How about @artsy/fresnel?

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

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

Or there may be other names to be found in the sphere of reflection (response) https://en.wikipedia.org/wiki/Reflection_(physics)

Issue with styling being applied to incorrect Media instance.

When server-side rendering all these breakpoints, the following CSS:

/* sc-component-id: sc-jDwBTQ */
. cWAwXI{
    display:none;
}
 @media (min-width:0px) and (max-width:767px){
    . cWAwXI{
        display:contents;
    }
}

/* … */

…and these 2 styled divs are emitted:

screenshot 2018-11-09 at 21 40 13

The top element is the xs one and that lines up with the emitted styling, all good. However, when the client-side uses the dynamic MatchMedia API and only renders the second Media component, the styled div somehow ends up receiving the same class-name as the first one when doing SSR rendering and that ends-up using the wrong previously emitted CSS:

screenshot 2018-11-09 at 21 39 43

This leads to the wrong styled div being hidden.

Remove dependency on styled-components

For us at Artsy this dependency is fine, but it’s not like this library makes heavily use of it, so to be more agnostic a patch to remove the styled-components dependency would be appreciated.

ClassName prop not working as expected

Hi there. Love this library, and am currently feverishly ripping out react-media and replacing it with fresnel in my project. However, I have noticed that the className prop doesn't seem to work as expected.

When I pass something like <Media className="column tile is-parent is-vertical" ... > it ends up creating a div with all of the built-in fresnel classes and a few additional that look like this:

fresnel-container fresnel-className-column tile is-parent is-vertical

Seems like its taking that prop and creating a classname called fresnel-className- and appending whatever is inside that prop. I can get around this by making the first class in my className prop a dummy one, like custom-classes, but I think expected behavior should be that it appends the class names as passed in. So <Media className="column tile is-parent is-vertical" ... > should produce a div with the following classes:

fresnel-container column tile is-parent is-vertical

I came across this because using fresnel to conditionally render columns with Bulma seems to mess up the intended column sizing behavior due to the added div wrapper. Would love to see this work without the added div, but I understand why its there.

Anyways, any advice on how I can better pass in classes would be appreciated, thanks.

-Josh

EDIT

I just noticed that if I put the className prop before the responsive prop it doesnt even add the responsive class names, and if I do it the other way, my custom classes are not included either. So I guess with this there is no way to pass custom classes to fresnel?

WebpackError: Minified React error #130

When running a webpack build, I'm seeing the below

  40 | var BreakpointConstraint;
  41 | /**
> 42 |  * Encapsulates all breakpoint data needed by the Media component. The data is
     | ^
  43 |  * generated on initialization so no further runtime work is necessary.
  44 |  */
  45 |

error TS2769: No overload matches this call.

Hi is it possible to overload the property or if there is a type definition that allows An element type to render as (string or function)? [A working example by Semantic UI React without typescript can be seen here]: https://github.com/Semantic-Org/Semantic-UI-React/blob/master/docs/src/layouts/HomepageLayout.js

import React from 'react';
import {
  Button,
  Container,
  Icon,
  Menu,
  Segment,
  Sidebar
} from 'semantic-ui-react';

import { createMedia } from '@artsy/fresnel'

import LandingPageHeading from './LandingPageHeading';

interface IProps {
  isLandingPage?: boolean;
}
interface IState {
  sidebarOpened: boolean;
}

const { Media } = createMedia({
  breakpoints: {
    mobile: 0,
    tablet: 768,
    computer: 1024,
  },
})

export default class MobileContainer extends React.PureComponent<IProps, IState> {
  constructor(props: IProps) {
    super(props);

    this.state = {
      sidebarOpened: false
    };
  }

  handleSidebarHide = () => this.setState({ sidebarOpened: false })

  handleToggle = () => this.setState({ sidebarOpened: true })

  render() {
    const { children, isLandingPage } = this.props
    const { sidebarOpened } = this.state

    return (
      <Media as={Sidebar.Pushable} at='mobile'>
        <Sidebar.Pushable>
          <Sidebar
            as={Menu}
            animation='push'
            inverted
            onHide={this.handleSidebarHide}
            vertical
            visible={sidebarOpened}
          >

Better structured Fresnel docs

I think Fresnel is the type of project that could really benefit from better structured docs. Implementation wise it’s a pretty lean lib, but conceptually it does some complex things and the required negation of media queries (which is done for you with breakpoints but you have to do on your own for interactions) doesn’t help.

In any case, I recently found these docs to technical docs and think it could really help Fresnel instead of trying to cover it all in a single README.

Doubt about content duplication

Hello! I was testing this lib, which i found awesome :) But i was wondering something: inspecting the server html response (inspect page source) i've noticed that there's a lot of duplicated elements. Could be this a SEO problem?

Thanks for your time :)

Write guide for README repo that outlines process of writing responsive components

  1. Use styled-system’s responsive props when layout only slightly adapts (TODO come up with better terminology for this)
  2. When design changes in a way that’s not solvable by responsive props, use Media API, but on the component nearest to the leaf of the tree.
  3. Add a link to the README guide in the Responsive deprecation warning.

SSR | Render props gives console warning

Thanks for the React v17 update, working fine now :)

My Project:
Next.js App implementing SSR as per your example https://github.com/artsy/fresnel/tree/master/examples/nextjs.

When I use the standard component implementation everything works fine.
<Media at="sm">Hello mobile!</Media>

When I try the render props implementation

<Media at="sm">
  {(mediaClassNames, renderChildren) => {
    return (
      <section className={mediaClassNames}>
        {renderChildren ? <article>Hello mobile!</article> : null}
       </section>
     )
  }}
</Media>

I get a console warning:
Warning: Did not expect server HTML to contain a <article> in <section>

Any help appreciated.

Render props pattern

Hi!

I was wondering what was the best approach for testing React components along with using the render prop pattern :)

Consider this:

 <Media at="xs">
     {(fresnelStyles): React.ReactNode => (
        <div className={fresnelStyles}>content for mobile</div>
     )}
   </Media>,

Works perfectly in the browsers and in writing tests without the render prop pattern, I can test if certain elements are visible nicely, however when using the render prop, it doesn't work anymore. Is there any way to solve this?

Thanks!

Accept string breakpoint values

It's a fairly common pattern for packages like theme-ui to accept breakpoints as string values. In the past I've gotten around this with something like:

const regex = /^(\d+)/
const { breakpoints: themeBreakpoints } = theme // eg {  breakpoints: ["32em", "48em", "64em", "80em"] }
const breakpointNames = ["sm", "md", "lg", "xl"]

if (themeBreakpoints.length !== breakpointNames.length) {
  throw new Error("breakpointNames.length must equal theme.breakpoints.length")
}

const breakpoints: { [name: string]: number } = breakpointNames.reduce<{
  [name: string]: number
}>(
  (acc, name, index) => {
    const stringValue = themeBreakpoints[index] // +1? mobile is default
    if (stringValue) {
      const numericCapture = stringValue.match(regex)[0]
      const numericValue = Number(numericCapture)
      if (numericValue) {
        return { ...acc, [name]: numericValue }
      }
    } else {
      return acc
    }
  },
  // theme-ui starts from 0 by default
  { xs: 0 }
)

const AppMedia = createMedia({
  breakpoints,
})

This only works for a px breakpoint scale, leading to broken styles on the client if using an alternative scale like rem. Any reason we cannot accept string values for the breakpoints object?

SSR | Render props hyration issue

I have the same issue as #165. This ticket was closed but actually this is a serious hydration error. Because the renderChildren is true on the server and false on the client for the specified breakpoints. This leads to a is a mismatch. As pointed out in the other issue in the component this is working fine. The warning is not shown in production because React suppresses all the warnings there.

The solution should be that renderChildren is always true on the hydration of the client and then turns false in the second step if the breakpoints do not match.

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.