0.1.1 • Published 4 years ago

react-adaptive-props v0.1.1

Weekly downloads
-
License
MIT
Repository
-
Last release
4 years ago

Adaptive Props

Make your components screen-size aware.

Your app/website needs to handle different "classes" of screens. CSS can help to apply different styles for different screen classes, but why stop at styles?

Let's say that you have a Carousel component that has a prop called slidesToShow. You might want to show 4 slides on large screens, but only 2 slides on smallish screens, and probably only 1 on a phone-sized device.

What if you could just write:

<Carousel
  slidesToShow={4}
  phone={{ slidesToShow: 1 }}
  smallishScreen={{ slidesToShow: 2 }}
/>

The idea is: each of your components could have props that correspond to your own custom screen classes (maybe that's mobile and desktop, or maybe sm, md. lg). These props would contain any overrides that you want to apply to the component based on the screen class.

That's exactly what Adaptive Props can do for you, and the best part is: you can drop-in this solution to any existing component in a snap!

Getting Started

There are 3 steps to enabling this functionality in your components:

1. Define your breakpoints in JS

const breakpoints = {
  xs: 500, // 0 - 500px -> "xs"
  sm: 750, // 501 - 750px -> "sm"
  md: 1000, // 751 - 1000px -> "md"
  lg: Infinity, // 1001+ -> "lg"
};

Tip: Each key in this object will become a prop on your components--watch out for naming conflicts!

The values that you provide are the "maximum pixel-widths" for that screen class. In order to make sure that all possible screen pixel-sizes are handled, there should be exactly one screen class with a value of Infinity which tells us that there is no maximum pixel-width for that screen class.

2. Generate your customized utilities, and render/export the ScreenClassProvider

// at the root of your app
import { createScreenClassProvider } from 'react-adaptive-props';

const breakpoints = {
  // your breakpoints here
};

// generate the custom Provider and a hook that will Consume it
const { ScreenClassProvider, useAdaptiveProps } = createScreenClassProvider({
  breakpoints,
  // this is the screenClass that will be used if we can't determine the width of the window (e.g. during SSR)
  defaultScreenClass: 'lg',
});

// export the useAdaptiveProps hook so that other components can use it
export { useAdaptiveProps };

// render the ScreenClassProvider at (or near) the root of your app
ReactDOM.render(
  <ScreenClassProvider>
    <App />
  </ScreenClassProvider>,
  document.getElementById('root')
);

Tip: if you're building a component library, you'll want to export the ScreenClassProvider for your users to render in their apps!

3. useAdaptiveProps in your components

import { useAdaptiveProps } from '../index.js'; // or where ever you exported it from

// before
const Button = props => {
  const { buttonSize, buttonType, buttonText } = props;

  // return ...
};

// after
const Button = props => {
  const { buttonSize, buttonType, buttonText } = useAdaptiveProps(props);

  // return ...
};

This is what I meant when I said that you could drop-in the functionality! All you need to do is replace props with useAdaptiveProps(props). The hook will consume the screenClass props e.g. xs, sm, md and will return a clean props that matches your existing API!

4. Profit?

Once you've completed those 3 steps, you can start adding Adaptive Props to your component. Each key from your breakpoints will be a valid prop!

<Button
  buttonText="Default text"
  sm={{ buttonText: 'Small screen text', buttonSize: 'mini' }}
  lg={{ buttonText: 'Large screen text', buttonSize: 'large' }}
/>

Organizing

The way that you organize your project is entirely up to you, but I've found it to be convenient to configure Adaptive Props in its own file and then to import ScreenClassProvider and useAdaptiveProps where ever they're needed. This keeps the index file tidy.

// {root}/AdaptiveProps.ts

import { createScreenClassProvider, AdaptiveProps } from 'react-adaptive-props';

const breakpoints = {
  xs: 500,
  sm: 750,
  md: 1000,
  lg: Infinity,
};

export const {
  ScreenClassProvider,
  useAdaptiveProps,
} = createScreenClassProvider({
  defaultScreenClass: 'lg',
  breakpoints,
});

andLarger + andSmaller

<Button
  buttonText="Default text"
  sm={{ buttonText: 'Small screen text', buttonSize: 'mini' }}
  lg={{ buttonText: 'Large screen text', buttonSize: 'large' }}
/>

In the above example, the "Default text" would be overridden on sm and lg screens, but on xs and md screens, you'd still see "Default text". That's because, by default, the Adaptive Props will only apply to their own screen class.

But what if you wanted to use that "Small screen text" and "mini" button on xs screens too?

Well, you could copy and paste the overrides from sm into the xs prop, but that gets 0 likes.

Instead, you can make use of the andSmaller directive to indicate that you want to apply the overrides on "sm screens andSmaller"!

Here's what it looks like:

<Button
  buttonText="Default text"
  sm={{ andSmaller: true, buttonText: 'Small screen text', buttonSize: 'mini' }}
  lg={{ buttonText: 'Large screen text', buttonSize: 'large' }}
/>

Now, your sm overrides will apply on sm screens and any smaller screens as well.

You can probably guess how andLarger works, so let's not waste time with that.

Tips

1. Ambiguous directives will result in an error

If you start to use multiple andSmaller or andLarger, you might be wondering how conflicting overrides might be applied.

First of all, don't do this:

// bad!
<Button
  xs={{ andLarger: true, buttonSize: 'microscopic' }}
  sm={{ buttonSize: 'mini' }}
  lg={{ andSmaller: true, buttonSize: 'humongous' }}
/>

Here we have the xs prop saying that all screen sizes that are larger should have a "microscopic" button, and we also have the lg props saying that all screen sizes that are smaller should have a "humongous" button. What size do you think the button should be on sm screens?

I don't know either! So that's an error.

In general terms: if some screen class prop uses andLarger and a larger screen class prop uses andSmaller, that's an error. And likewise if some screen class prop uses andSmaller and a smaller screen class prop uses andLarger, that's an error too.

This is okay though:

//
<Button
  sm={{ andSmaller: true, buttonSize: 'mini' }}
  md={{ andLarger: true, buttonSize: 'humongous' }}
/>

Whenever you see andSmaller or andLarger, picture an arrow pointing in the direction that they're referring to (andSmaller points up, andLarger points down) if two arrows point at one another, they crash and that's an error. In the example above, the arrows would point away from each other, and that works just fine!

But what if the arrows are pointing in the same direction...

2. Overlapping directives will be applied in order from furthest away to closest

Take this example:

<Button
  sm={{ andSmaller: true, buttonSize: 'mini' }}
  lg={{ andSmaller: true, buttonSize: 'humongous', buttonText: 'Click me!' }}
/>

Here's the result:

{
  xs: { buttonSize: 'mini', buttonText: 'Click me!' }
  sm: { buttonSize: 'mini', buttonText: 'Click me!' }
  md: { buttonSize: 'humongous', buttonText: 'Click me!' }
  lg: { buttonSize: 'humongous', buttonText: 'Click me!' }
}

Hopefully that's what you expected! Notice that all sizes got the "Click me!" text from lg, and that md got the "humongous" buttonSize from lg, but xs got the "mini" buttonSize from sm.

When calculating the props for xs, we started as far away as possible (at lg) and applied any overrides that were necessary. We assigned "humongous" buttonSize and "Click me!" for the buttonText. Then we moved to md; it had no andSmaller directive, so we skipped it. Next, we looked at sm; it did have an andSmaller directive, so we applied its overrides on top of all previous overrides. That's why the "humongous" buttonSize was overridden to be "mini" instead.

Generally, if two directives try to override the same prop, the "closest" one wins. Indeed, if xs had defined its own override e.g.

<Button
  xs={{ buttonSize: 'microscopic' }}
  sm={{ andSmaller: true, buttonSize: 'mini' }}
  lg={{ andSmaller: true, buttonSize: 'humongous', buttonText: 'Click me!' }}
/>

The final buttonSize would be "microscopic" which fits with our mental model since nothing can be "closer" to xs than xs itself!

TypeScript

Everybody loves nice types. This lib was written with TypeScript, so utilities themselves are well-typed, but their types depend on the specific breakpoints that you've configured. Because of this, you'll want to "configure" the types before using them.

Here are the two most useful types that we export:

/**
 * A union containing all of your custom screen classes
 */
type ScreenClass<B extends ScreenClassBreakpoints> = keyof B;

/**
 * A type that can be wrapped around your components' props in order to represent the new Adaptive Props that they have
 */
type AdaptiveProps<B extends ScreenClassBreakpoints, P extends {}> = Omit<
  P,
  keyof B
> &
  {
    [K in keyof B]?: Partial<P> & {
      andLarger?: boolean;
      andSmaller?: boolean;
    };
  };

As you can see, both of these types require you to provide your own custom breakpoints. So, you could export your breakpoints and import them everywhere that you need to use one of these types, or you could configure these types in one place and then re-export them!

import { AdaptiveProps, ScreenClass } from 'react-adaptive-props';

// at the root of your app
const breakpoints = {
  // your breakpoints here
};

export type MyAdaptiveProps<P extends {}> = AdaptiveProps<
  typeof breakpoints,
  P
>;
export type MyScreenClass = ScreenClass<typeof breakpoints>;

Now you can import MyAdaptiveProps and MyScreenClass all over your app!

Here's a fully-typed example for reference:

import { AdaptiveProps } from 'react-adaptive-props';

const breakpoints = {
  // your breakpoints here
}

type MyAdaptiveProps<P extends {}> = AdaptiveProps<typeof breakpoints, P>;

type CustomComponentProps = {
  someColor?: string;
  someText: string;
};

const CustomComponent: React.FC<MyAdaptiveProps<CustomComponentProps>> = props => {
  const {
    someColor = '#000000',
    someText = 'Unknown screen size',
  } = useAdaptiveProps<CustomComponentProps>(props);

Under the Hood

For folks who are curious about the implementation:

Fundamentally, we need our components to be aware of the current screen class so they know when to re-render. We do this by setting up a single resize event-listener at the root of the app, and Providing the screen class via React Context.

For better perf while your users are rapidly resizing their screens to see if your site breaks, there's a little debounce. You can configure the debounce delay by supplying a resizeUpdateDelay to the config object when you createScreenClassProvider.

0.1.1

4 years ago

0.1.0

4 years ago