Thinkmill / design-system

🎨 Thinkmill's Design System Style Guide
MIT License
32 stars 3 forks source link

Clarify state management in CSS/Style functions #2

Open MadeByMike opened 5 years ago

MadeByMike commented 5 years ago

A little "rant" (hopefully a good one) about UI state management... which is different to application state in container components.

In my opinion this example is not fantastic CSS/CSS-in-JS:

const getButtonStyles = ({ isPressed, tokens }) => ({
  background: isPressed ? 'blue' : 'white',
  color: !isPressed ? 'blue' : 'white',
});

export const Button = ({ children, ...props }) => {
  const { isPressed, events } = useButton(props);
  const styles = getButtonStyles({ isPressed });
  return (
    <button {...props} {...events}>
      {children}
    </button>
  );
};

My apologies to whoever wrote this example :) I don't mean to pick on it. It's a pretty typical example I've see on a lot of projects. I want to describe a few problems I've had with this approach as projects get larger.

In examples like this, the value of individual CSS properties depends on the resolution of state within a style function. I'm slowly solidifying my opinion that this is a code-smell for CSS-in-JS.

It's great that the props are resolved down to sensible descriptive flags like isPressed before handing off to he style function. That helps. But as it scales it will become impossible to know the number of different states or variations a UI component has.

As a front-end developer knowing the number of variations a UI component has and what CSS is applied in each case is about 90% of the job. And I want to know this quickly when traversing large projects.

The way to solve this is to map out a set of finite state categories like this:

Modifiers Behavioural Pseudo
Large Pressed focus
Small Disabled hover

You should only have one Modifier and one Behaviour active at any given time. If you find 2 behaviours can be active at the it's usually an indication that this component could be two components but you can add another state category if needed.

This makes it possible to know the number of UI states a component can have 2 x 2 x 2 = 8. Suddenly we can validate this against the design. With the props resolved against individual CSS properties it's not possible to know or test that the resolution of the style function results in something valid and intended.

The next part is to make these styles more 'ergonomic'. We want to know quickly what the small + pressed variation is without resolving everything in our head. If I can't resolve UI state to set to applied styles in under 5 sec it makes me sad. Not making me sad should be a primary goal of a design system.

My solution is this:

const modifiers = {
  'large': {
    fontSize: '2rem'
  },
 'small': {
    fontSize: '0.8rem'
  }
}
const behaviours = {
  'pressed': {
    background: 'blue'
    color: 'white'
  },
 'disabled': {
    color: '#888'
  }
}

const getButtonStyles = ({ modifier, behaviour }) => ({
  background: 'white',
  color: 'blue',
  ...modifiers[modifier]
  ...behaviours[behaviour]
});

export const Button = ({ children, ...props }) => {
  const { modifier, behaviour, events } = useButton(props);
  const styles = getButtonStyles({ modifier, behaviour });
  return (
    <button css={styles} {...props} {...events}>
      {children}
    </button>
  );
};

(I've modified it so that the useButton function resolves the modifier and behaviour states)

If a modifier changes the value of a behaviour we can use CSS properties:

const modifiers = {
  'large': {
    fontSize: '2rem'
    '--pressedBackground': 'red'
  }
}
const behaviours = {
  'pressed': {
    background: 'val(--pressedBackground, 'blue')'
  }
}

Now for the large variation only the pressed state will be red/white rather than the default blue/white.

I'm not fixed on my suggested implementation but rather the goals of:

You'll notice I kinda borrow naming conventions here from BEM (modifier/behaviour) I have other ideas about how to communicate all intentions and give semantic meaning to styles\components with large systems and I think this is pretty important too. -- Another issue.

Thank you for coming to my TED talk.

elisechant commented 5 years ago

That's a great insight @MadeByMike. I learnt something 👍! I agree with your position, and I think it's right. I agree that it's early days but assumptions should be clarified before they become indoctrinated.

Two schools of thought for how to move forward:

  1. Perhaps this opens up a need for fleshing out higher level concepts and principles this framework (code wise) wants to be guided by, ie, pattern best-practices, composability etc.
  2. Or, if the focus here is ship then refactor? to identify that as a guiding principle.
gwyneplaine commented 5 years ago

@MadeByMike completely agreed, we should have a chat a lot of the problems you've stated are things we intend to solve with design tokens (and in fact your example aligns quite well with how we're currently thinking about design tokens)

Resolving state outside of our style function and having clearly reduced and evaluated state inside of our component (that gets passed down to the style fn to resolve) not only solves for cognitive load and testability but also unlocks a lot of potential around performance.

For example: (modifier: string, behaviour: string) => CSSObject, could be easily memoized such that our css only gets re-evaluated when state changes.

The alternative view here however is how this affects customizability of our components, which I need to think more deeply about. Consumer customisation is a primary driver around passing all enumerable states into the style function (because we can never be sure of which states a user wants to be able to resolve CSS based on).

I don't yet know what consumer customisation looks like in a view of the world with clearly reduced state. I think it's possible, but this is something I'd love to collaborate with you on to shape going forward.

Additionally would be good to explore the edge cases that do occur, for example the checkbox and radio examples we talked about two weeks ago, around focusedAndChecked being a state we need to be able to resolve. Both of these sit within the behavioral state class at the moment, and we do need to think about edge cases like this that do occur.

tldr;