Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clarify state management in CSS/Style functions #2

Open
MadeByMike opened this issue Sep 19, 2019 · 2 comments
Open

Clarify state management in CSS/Style functions #2

MadeByMike opened this issue Sep 19, 2019 · 2 comments

Comments

@MadeByMike
Copy link

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:

  • Knowing how many UI states a can component has
  • Ensuring these states can be easily validated against the design
  • Ensuring styles are ergonomic and easy to read

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.

@MadeByMike MadeByMike changed the title Clarify state management in CSS Clarify state management in CSS/Style functions Sep 20, 2019
@elisechant
Copy link

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
Copy link
Contributor

@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;

  • Agreed on having well categorised and reduced UI state passed into our style functions
  • Tokens are a good tool for us to leverage to get to this
  • Would love to think more about how this affects customisability of design system components
  • We should make sure we have strategies around cases where there are mixed modifier / behavioral states.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants