npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@fluentui-react-native/use-styling

v0.13.0

Published

Opinionated use styling hook implementation

Downloads

7,012

Readme

@fluentui-react-native/use-styling

This package provides a framework for building a hook function which produces opinionated styles, for both simple and higher order components. This is built with the following principles in mind:

  • Theming - the useStyling hook allows for styles to be built from information contained in a theme object. This package is not opinionated about the shape of the theming system, it is injected when building the hook.
  • Caching - props and styles should not be calculated more often than necessary. Results will be cached both to improve performance, and to maintain consistent object references for style objects.
  • Reuse - the system should be designed to ensure caching uses as few keys as necessary and that generated styles be shared as much as is possible. This is important in situations like generating class names for the styles.

An extremely simple example might look like:

// create the styling hook which can be used in the component, note that this gets created once and
// should not be done inside the component itself
const useSyling = buildUseStyling(myComponentOptions, themeHelper);

// component implementation, consuming the styling hook
const MyComponent = (props) => {
  // get some default/styled props for the container and content child components
  const { container, content } = useStyling(props);
  // render as normal but mixin those values to pass the props to the sub-components
  const { textValue, ...rest } = props;
  const merged = { ...container, ...rest };
  return (
    <View {...merged}>
      <Text {...content}>{textValue}</Text>
    </View>
  );
};

Now this example isn't particularly useful but demonstrates the basic role that the styling hook can play in a HOC.

Quick Reference

At the core of this system is the idea that: Tokens + Theme ==> Styles. This can be thought of as:

  • Your styles are produced via a recipe, defined as a function in slotProps
  • The tokens are the ingredients for your recipe
  • Some of those tokens come from constants
  • Some of those tokens come from the theme
  • Some tokens could even come from props but this is not the default and needs to be declared in tokensThatAreAlsoProps

The overall signature of the method is as follows:

const useStyling = buildUseStyling(
  /* options that define the component */
  {
    /* Step 1: Build up a single Tokens object with constants for the component */
    tokens?: [
      /* object */
      { },
      /* theme function */
      (theme: Theme) => ({ /* created object */ }),
      /* name to lookup */
      'NameToLookup'
    ],

    /* Step 2: Potentially apply states (if applicable) to the Tokens */
    states?: ['hovered', 'pressed', 'disabled'],

    /* Step 3: Copy some/none/all props to Tokens */
    tokensThatAreAlsoProps?: 'all' /* all props are tokens */
              | 'none' | undefined /* no props are tokens */
              | ['token1', 'token2'] /* list of tokens which also appear in props */,

    /* Step 4: Create props for child components from Tokens and Theme */
    slotProps?: {
      /* props as a constant object */
      slot1: { },
      /* props as a function of tokens and theme, with smart caching/filtering */
      slot2: buildProps((tokens: Tokens, theme: Theme) => {
        // function to generate props
        return { style: { color: tokens.token1 }}
      }, ['token1'])
      /* props as a custom function of tokens, theme, cache */
      slot3: (tokens: Tokens, theme: Theme, cache: GetMemoValue) => {
        // function to generate props
      }
    }
  },
  /* theme helper plugin, global per theming system */
  {
    /* get the active theme, usually from the context */
    useTheme: () => { return useContext(ThemeProvider) || getDefaultTheme() };

    /* lookup a component in the theme to see if token overrides are available */
    getComponentInfo: (theme: Theme, name: string) => lookupComponentInTheme(theme, name);)
  }
);

Tokens

A token is simply a setting that informs the styling of the component. These can be thought of as an additional set of component props that are automatically built-up by the framework. Typically these are set at component creation time or overriden by a theme but can can come from a variety of sources:

  • Objects - this is a set of values contained within an object. These values are assumed to be unchanging over the lifetime of the object.
  • Theme Functions - this allows values to be set in relation to values in a theme. An example would be a color being set to a particular entry in a color palette in the theme.
  • Name Lookup - this will query the theme for a token object or theme function for this component.

Example

/** Tokens interface for this component */
interface Tokens {
  backgroundColor?: ColorValue;
  borderWidth?: number;
  borderRadius?: number;
}

/** Build the styling hook */
const useStyling = buildUseStyling({
  tokens: [
    /* fixed object */
    {
      borderWidth: 1,
      borderRadius: 2,
    },
    /* theme function */
    (theme: Theme) => {
      backgroundColor: theme.palette.bodyBackground || 'white';
    },
    /* name lookup */
    'MyComponentName',
  ],
  ...otherSettings,
});

Layering

These tokens will be deeply merged in the order they appear in the array.

const mergedTokens = merge(
  ...options.tokens.map(entry => {
    // string gets converted to object or function (or empty object)
    if (typeof entry === 'string') {
      entry = themeHelper.getComponentInfo(entry) || {};
    }
    // return either the function call (which will produce an object) or the object
    return typeof entry === 'function' ? entry(theme) : entry;
  });
);

Caching

The only variable input to this entire process is the theme. As a result the result of all of this will be cached based on:

  • useStyling closure, each closure will have its own unique cached values
  • theme object reference, stored in a WeakMap

As a result this calculation will run one time per theme, per component type.

States

Some components may have additional states or modes that can be applied to them. As an example a Button component might have a state called hovered or pressed. In this case the Tokens interface should be as follows:

interface ButtonTokens {
  // base values
  backgroundColor?: ColorValue;
  color?: ColorValue;
  borderColor?: ColorValue;

  // states
  hovered?: ButtonTokens;
  pressed?: ButtonTokens;
}

In this case the states option should be provided as follows:

const useStyling = buildUseStyling({
  tokens: [
    /* token settings */
  ],
  states: ['hovered', 'pressed'],
  ...otherSettings,
});

For each entry in the states array, in the order they appear, the following will happen:

  • Evaluate whether the state applies. This will be true if props[state] is truthy, or if a passed in closure evaluates as truthy. More about this below.
  • If the state is true, and tokens contains a sub-object matching that name, the sub-object will be merged into the parent object.

The generated useStyling hook has an optional second parameter which is a function of the form:

type HasLayer = (state: string) => boolean;

This allows states to be set based on states in addition to props. An example might look like:

interface ButtonState {
  hovered?: boolean;
  pressed?: boolean;
}

const Button = (props: ButtonProps) => {
  // get some state value that figures out whether the button is pressed and/or hovered
  const buttonState: ButtonState = useButtonState(props);
  // get styling values based on props and the current state
  const { container, icon, label } = useStyling(props, (stateName) => buttonState[stateName]);

  const buttonProps = useButtonMagic(props, container);
  return (
    <View {...buttonProps}>
      <Image {...icon} source={props.iconSrc} />
      <Text {...label}>{props.text}</Text>
    </View>
  );
};

Tokens and Props

The ideal scenario from a caching perspective, is for tokens and props to be completely separate. This keeps variability very low and reduces the need for frequent style recalculations. From the perspective of someone using the components this sharply limits flexibility. As a result it may be useful for some tokens to be surfaced as props on the component.

Because props are not provided to the final stage of the useStyling workflow, if any props are also tokens, they need to be declared so that they can override the token values coming from the theme or constants. This is done via the tokensThatAreAlsoProps property. This has three behaviors:

| Value | Behavior | | ----------------------- | ----------------------------------------------------------------------------------------------- | | 'none' or undefined | No properties are tokens and tokens will be left as is. | | 'all' | All properties should be considered tokens. This equates to: tokens = { ...tokens, ...props } | | (keyof Tokens)[] | Provides a discrete list of values to copy from props to tokens (if present) |

Slot Props

Part of the challenge of building a higher order component, particularly one that is customizeable, is figuring out how to map a single set of props into a multiple internal components. While it is standard to expose a style on the props interface of a component, this can only map to one of the internal components.

Customizing HOCs by directly targeting the styles of the sub-components is problematic in that it leaks the implementation details. As a result, it becomes challenging to maintain or update the component long term without breaking customers.

The solution used by the useStyling module is to allow focus customization on the various Token values, then have a defined mapping of how those Tokens translate to props for each child component. In the parlance of this library, each child component is referred to as a slot.

Each slot returns its own props object, either directly or via a function.

Direct Objects

The simplest way to provide props for a slot is by providing an object directly. While limited in applicability, it looks as follows:

const useStyling = buildUseStyling({
  ...precedingOptions,
  slotProps: {
    slot1: { style: { display: 'flex' } },
  },
});

Functions

The functions are assumed to have the following signature:

function propBuilder(tokens: Tokens, theme: Theme, cache: GetMemoValue<Props>): Props;

While tokens and theme have been discussed at length above, the cache will be a cache instance keyed on:

  • The theme
  • The tokens coming from the theme
  • This slot

If there are no tokens which are also props, i.e. if tokensProps is falsy, this is sufficient and the function could be written as:

const slotFn = (tokens: Tokens, theme: Theme, cache: GetMemoValue<Props>) =>
  cache(
    () => {
      return {
        style: {
          backgroundColor: tokens.backgroundColor,
          // etc.
        },
      };
    },
    [
      /* no keys, just direct cache the function result*/
    ],
  )[0];

If some all props are tokens then those keys should be considered in the caching. This would then turn into:

const slotFn = (tokens: Tokens, theme: Theme, cache: GetMemoValue<Props>) => {
  const { backgroundColor, color, borderRadius } = tokens;
  return cache(
    () => ({
      style: { backgroundColor, color, borderRadius },
    }),
    [backgroundColor, color, borderRadius],
  )[0];
};

buildProps helper

The library provides a helper called buildProps to make this more ergonomic. It provides an implementation for the full caching function above and would be used as follows:

const slotFn = buildProps(
  (tokens: Tokens, theme: Theme) => ({
    style: {
      backgroundColor: tokens.backgroundColor,
      color: tokens.color,
      borderRadius: tokens.borderRadius,
    },
  }),
  ['backgroundColor', 'color', 'borderRadius'],
);

The primary benefit this function provides is that buildUseStyling knows how to refine these lists when constructing the hooks. As a result the caching will adjust automatically based on what is specified in tokenProps.