Redux/Reselect

SA-UI Patterns & Practices

Make Components Stupid Again

Over-arching Theme

Stupid?

  • Less knowledgeable
  • Loosely or, when possible, in no way coupled to the components around them
  • Improve code reusability, don't reinvent the wheel
  • Let the architecture/patterns do the work

Really we mean:

In practice...

Business rules, out

Unnecessary state, out

Proxying state for other components, ideally no

Because...

  • Simplify re-use
  • Simplify testing
  • Simplify refactoring
  • Make development decisions quicker
  • Reduce cognitive load for the next developer

Demo App

Business Rules

Components are a great place to keep your business rules...

...said no one ever.

Business Rules

What do we mean by "Business Rules"?

Logic that makes decisions about state....

...or that translates user actions into state updates.

Rules in Actions

...

const PagingComponent = Component.extend({

  ...

  actions: {  
    firstPage() {
      this.send('goToPage', 1);
    },
    nextPage() {
      this.send('goToPage', this.get('pageNumber') + 1);
    },
    previousPage() {
      this.send('goToPage', this.get('pageNumber') - 1);
    },
    lastPage() {
      this.send('goToPage', this.get('lastPageNumber'));
    }
  }
});

Take user intent, merge it with state, send out new state.

Rules via @computed

...

const PagerComponent = Ember.Component.extend({
  numberItems: Ember.computed('items', function() {
    return this.get('items').length;
  }),
  lastPageNumber: Ember.computed('pageSize', 'numberItems', function() {
    return Math.ceil(this.get('numberItems') / this.get('pageSize'));      
  }),
  isFirstPage: Ember.computed('pageNumber', function() {
    return this.get('pageNumber') === 1;
  }),
  isLastPage: Ember.computed('pageNumber', 'lastPageNumber', function() {
    return this.get('pageNumber') === this.get('lastPageNumber');
  }),

  ...

})

Logic that generates new state or truth

Why out?

  • Rules in Components are not re-usable
    • Mixin?
      • Only save keystrokes
      • For @computed, still calculating twice
      • Not truly composeable
      • More overhead
    • Pass down from some higher level?
      • Selectively may work well
      • With nested structures, becomes house of cards
      • Want consistent patterns for ease of dev and maintenance

Why out?

  • Rules in Components complicate the Component
    • Simple components are simpler
    • Out of sight, out of mind
  • Eventual destination...
    • ...is easier to test
      • Selectors, for instance, are embarrassingly easy to test
      • Business rules by their nature should be unit testable
    • ...is more intuitive
      • Finding a rule in a component isn't as intuitive than in a big bucket of rules
      • Especially if those rules are co-located with the state with which they work

Demo App

  • Action creators
  • Selectors
  • Fix the bug

Unnecessary State

Stare deep into the heart of your stateToComputed...

...and unsuck it.

stateToComputed

  • component interface, defines inputs
    • not always all
  • first place you look when approaching a new component
    • should always be at the top
  • keep sanitized

stateToComputed

const stateToComputed = ({ page: { pageNumber, pageSize, items } }) => ({
  pageNumber,
  pageSize,
  items
});

How can something so simple be so unfortunate?

Unused State

  • make sure you aren’t wiring in state that isn’t used
    • Often left over post-refactor
    • stateToComputed copied in from elsewhere
    • This is easy to check. Check names of stateToComputed variables, verify not in…
      • component
      • template
      • component’s mixins

Improperly Used State

Don't wire state in, if all you are doing is sending it back out via actionCreator.

actionCreators can get any state they want via redux-thunk's `getState`, let them.

Derived State

Avoid wiring in state that is only used to derive other state.

 

 

Any logic that turns state into more state is a business rule.

const stateToComputed = ({ kitchen }) => ({
  limes: kitchen.limes,
  salt: kitchen.salt,
  cumin: kitchen.cumin,
  cayenne: kitchen.cayenne,
  onion: kitchen.onion,
  jalapeno: kitchen.jalapeno,
  tomatoes: kitchen.tomatoes,
  cilantro: kitchen.cilantro,
  garlic: kitchen.garlic,
  avocados: kitchen.avocados,
  knife: kitchen.knife,
  teaspoon: kitchen.teaspoon,
  tablespoon: kitchen.tablespoon
});


limeJuice: Ember.computed('limes', 'knife', function() {
  ...
}),
mincedJalapenos: Ember.computed('jalapenos', 'knife', function() {
  ...
}),
measuredSalt: Ember.computed('salt', 'teaspoon', function() {
  ...
}),
measuredCumin: ...
measuredCayenne: ...

...

guacamole: Ember.computed('limeJuice', 'mincedJalapenos', ..., function() {
  ...
});
{{guacamole}}

Steve

import { guac } from 'selectors/steve';

const stateToComputed = (state) => ({
  guacamole: guac(state)
});


{{guacamole}}

What complexity has Steve removed?

  • We do not need to know the ingredients
  • We do not need to know how those ingredients are mixed
  • What if the recipe changes?
    • Component stays the same

Does Steve make other things?

  • Steve only makes guac now, but eventually...
    • import { guac, salsa } from 'selectors/steve';
    • Gets to re-use things like `limeJuice` and `choppedOnion`
  • Co-located, composeable
    • You know when you need another complicated thing involving that state, can probably see if Steve already has it

Another Example

const stateToComputed = ({ items }) => ({
  items
});

const ItemsComponent = Ember.Component.extend({
  hasItems: Ember.computed('items', function() {
    return this.get('items').length > 0;
  })
});
const stateToComputed = (state) => ({
  hasItems: hasItems(state)  
});

const ItemsComponent = Ember.Component.extend({});
{{#if hasItems}}
  {{some-component}}
  {{another-component}}
  {{a-third-component}}
{{/if}}

Demo App & itemsToRender

Proxied State

What do I mean by proxied state

  • State that is pulled into a higher level component solely to be passed someplace else
  • Often conceptually the result of pre-Redux thinking

Recon before Redux

How did it get that way?

  • State needed all over the component heirarchy
  • End up storing at common ancestor
  • Have to send state and actions down to modify state

What state does it actually need?

{{#if showMetaDetails}}
  ...
{{/if}}

Recon after Redux

How did it get that way?

Bound downstream components into Redux so they can fetch their own state

Not without cost

  • The more components you bind, the more notifications have to go out that state has changed
    • Running more code means more code is run
    • Each component bound into state takes a minor performance hit
  • This isn't a concern if binding more components is the difference between binding 1 and 10.
  • This is a concern if you are binding significant numbers of components

Do not...

  • ...connect anything iterable
    • If it is rendered in an {{#each}}, probably best to not bind to Redux
  • ...connect anything reusable directly to state
    • component-lib components, for instance, take different state depending on their context

In both cases, some parent may be bound to Redux, but data needs to be passed in via template params.

Dev benefit is high

  • Very hard for someone coming into this component to do harm to the children
    • Don't really even need to be aware what the child components do
  • I can move the child components without cost
    • Component is entirely self-sufficient, can fend for itself no matter where you put it
  • Avoid the cognitive overhead of parent components binding to state it doesn't directly need
    • "why is that here?"

#MCSA

  • Get rules and data crunching out of components
    • Move data crunching and user interaction side effects in to selectors/action creators
  • Only bind the state you need
    • Don't bind state you need to compute more state
  • Don't build a house of cards
    • When possible, don't bind state some other component needs
    • Let components declare their own Redux dependencies

#MCSA Why?

  • Reduce development overhead
  • Reduce maintenance overhead
  • Re-use code
  • Testing 

redux/reselect best practices

By David Bashford

redux/reselect best practices

  • 1,275