Skip to content

Instantly share code, notes, and snippets.

@SeeThruHead
Last active August 25, 2016 12:40
Show Gist options
  • Save SeeThruHead/fce7d20b39b4f24a1820bf5dedd67b78 to your computer and use it in GitHub Desktop.
Save SeeThruHead/fce7d20b39b4f24a1820bf5dedd67b78 to your computer and use it in GitHub Desktop.

#Recompose Workshop

##Stateless Function Components

Review stateless function components

  • best way to code markup
  • they take an object props, as their first arg, and context as second
  • they return jsx
  • they have no state...
  • best practice

they look like this:

const Square = ({ color, width }) => (
  <div style={{ width, height: width, backgroundColor: color }}></div>
);
const Square = ({ pred, color, width }) => {
  if (!pred()) return <span></span>; // better as a ternary or branch from recompose
  
  return <div style={{ width, height: width, backgroundColor: color }}></div>
};

###Why src

Forget ES6 classes vs. createClass().

An idiomatic React application consists mostly of function components.

const Greeting = props => (
  <p>
    Hello, {props.name}!
  </p>
 );

Function components have several key advantages:

They help prevent abuse of the setState() API, favoring props instead. They encourage the "smart" vs. "dumb" component pattern. They encourage code that is more reusable and modular. They discourage giant, complicated components that do too many things. In the future, they will allow React to make performance optimizations by avoiding unnecessary checks and memory allocations. (Note that although Recompose encourages the use of function components whenever possible, it works with normal React components as well.)

##Higher Order Components

Higher order components are function that receive a react component, and return a new react component "augmented" with new behaviour. I put augmented in quotes here because generally you are not mutating the react component that you take into the HOC, but rather wrapping it with functions that manipulate it's props and contain other behaviours in themselves.

###References

HigherOrderComponent : Component => EnhancedComponent
HigherOrderComponent : (args, Component) => EnhancedComponent
HigherOrderComponent : args => Component => EnhancedComponent

#####React Redux

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

#####Theming Component

This component is completely reuseable, and can be applied to any react component.

// takes in a function for later use
// takes in the component to enhance
export const connectTheme = mapThemeToCss => Composed => {

  // returns a new stateless component
  const wrapped = (props, context) => {
  
	 // return the original component if there is no theme available on context
    if (!context || !context.theme) return <Composed {...props} />;

	 // return a wrapped component directly (useSheet is a third party HOC)
    return React.createElement(useSheet(PluckClasses(Composed), mapThemeToCss(context.theme)), props);
    
  };

  // apply the proper context types to the top level returned component
  wrapped.contextTypes = { theme: PropTypes.object };
  
  // set the displayName to something readable
  wrapped.displayName = `Theme(${Composed.displayName || 'component'})`;

  return wrapped;
};

##Composition The eventual return function of these HOC's (after we supply all initial args) is:

HigherOrderComponent : Component => EnhancedComponent

Like any unary (accepting one argument) function of Type => Type we can compose this function with any other function of Type => Type

ie:

const enhance = compose(
  connect( // returns a function of Component => Component
   mapStateToProps,
   mapDispatchToProps
  ),
  connectTheme(mapThemeToCss) // ditto
);

Now we can use our enhanced HOC on any react component

export default enhance(() => <div>derp</div>);

or more commonly

const enhance = /*...*/;
const Markup = () => (
  <div>Some Larger Tree</div>
);

export default enhance(Markup);

##Recompose Recompose is a React utility belt for function components and higher-order components. Think of it like lodash for React. Uses:

// counter is the stateProp, setCounter is a setState function for that specific prop, 0 is the initialstate
const enhance = withState('counter', 'setCounter', 0) 
// purely functional component decorated with some state!
const Counter = enhance(({ counter, setCounter }) =>
  <div>
    Count: {counter}
    <button onClick={() => setCounter(n => n + 1)}>Increment</button>
    <button onClick={() => setCounter(n => n - 1)}>Decrement</button>
  </div>
)

same thing but more redux-like

// pure reducer
const counterReducer = (count, action) => {
  switch (action.type) {
  case INCREMENT:
    return count + 1
  case DECREMENT:
    return count - 1
  default:
    return count
  }
}

const enhance = withReducer('counter', 'dispatch', counterReducer, 0)
const Counter = enhance(({ counter, dispatch }) =>
  <div>
    Count: {counter}
    <button onClick={() => dispatch({ type: INCREMENT })}>Increment</button>
    <button onClick={() => dispatch({ type: DECREMENT })}>Decrement</button>
  </div>
)

Create Context (like react-redux 'provide')

export const ThemeProvider = withContext(
  { theme: PropTypes.object },
  props => ({ theme: props.theme }) // pure function
);

Get Context

const enhance = getcontext({ theme: proptypes.object });

Branch (apply one or another HOC based on predicate)

const enhance = branch(
  props => props.theme, // predicate  
  omitProp('sheet'),
  c => c // identity function (an HOC that does nothing)
);

We can bring all those together with composition to create easy to understand libraries of reusable functionality like Gild: our themeing library.

Gilds connectTheme (similar to connect from react-redux) before recompose

// takes in a function for later use (mapThemeToCss)
// return a function that takes a component and returns a component (standard HOC signature)
export const connectTheme = mapThemeToCss => Composed => {

  // create the eventual stateless component
  const wrapped = (props, context) => {
  
	 // return the original component if there is no theme available on context
    if (!context || !context.theme) return <Composed {...props} />;

	 // return a wrapped component directly (useSheet is a third party HOC)
    return React.createElement(useSheet(PluckClasses(Composed), mapThemeToCss(context.theme)), props);
  };

  // apply the proper context types to the top level returned component
  wrapped.contextTypes = { theme: PropTypes.object };
  
  // set the displayName to something readable
  wrapped.displayName = `Theme(${Composed.displayName || 'component'})`;
  
  // finally return the new component
  return wrapped;
};

After recompose

export const connectTheme = mapThemeToCss =>
  compose(
    comp => setDisplayName(wrapDisplayName(comp, 'gild'))(comp),
    getContext({ theme: PropTypes.object }),
    branch(
      props => props.theme,
      compose(
        withSheet(mapThemeToCss),
        mapProps(props => ({ ...props, theme: props.sheet.classes })),
        omitProp('sheet')
      ),
      c => c
    )
  );

explained:

export const connectTheme = mapThemeToCss =>
  compose(
    // an HOC that wraps the displayName with GILD
    comp => setDisplayName(wrapDisplayName(comp, 'gild'))(comp), 
    
    // hoc that gets context, getContext is self documenting
    getcontext({ theme: proptypes.object }),
    
    // if getContext passed theme to props
    branch(
      props => props.theme, 
    
      // a further composition of recompose utils
      compose(
        // a more complicated HOC defined in another file
        withSheet(mapThemeToCss),
        
        // unnesting a prop
        mapProps(props => ({ ...props, theme: props.sheet.classes })), 
        
        // omitting the prop we extracted classes from
        omitProp('sheet') 
      ),
      
      // if there's no theme prop we just return a HOC that does nothing but return the original Comp
      c => c
    )
  );

full code here -- https://github.com/influitive/Gild

###Pros

  • Much less code to reason about
  • Code that is left is all pure functions (easier to reason about)
  • Consistency across projects, similar to how lodash/ramda is a common toolset
  • No coupling
  • Very high reuse capability
  • We're already using tons of third party HOC's so compose(...) is natural
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment