Functional Programming in React
Functional Programming
Let's start with some terminology
Imperative vs. Declarative Code
Declarative programming is a programming paradigm … that expresses the logic of a computation without describing its control flow.
Imperative programming is a programming paradigm that uses statements that change a program’s state.
Imperative vs. Declarative Code
Declarative programming is a programming paradigm in which you describe how to do something.
Imperative programming is a programming paradigm in which you describe what to do.
Functional Programming is a form of Declarative Programming
Not exactly, but let's not be pedantic.
Pure Functional Programming is a form of Declarative Programming
Actually, let's be pedantic ...
Referential Transparency
A given function does not access the mutable state that is external to the function.
Less precisely, a function may not have side-effects.
Referential Transparency
Given the same input, a function should always return the same output.
A function call can be replaced by its return value or another referentially transparent call with the same result.
Pure Functions
are Referentially Transparent
No observable side-effects. Easy to read and reason about.
Benefits
Explicit arguments. Expressions are self-documenting and portable.
Cacheable. Based on arguments.
Easily testable. Assert output by input. No mocking.
Safe parallelization. No race conditions.
Pure Functional Programming in React
"In an ideal world, most of your components would be stateless functions because in the future we’ll also be able to make performance optimizations specific to these components by avoiding unnecessary checks and memory allocations. This is the recommended pattern, when possible."
The Toolbox
Currying
f(x)(y)(z) === f(x, y)(z) === f(x, y, z)
Allows to call a function with fewer arguments. It returns a function that takes the remaining arguments until all arguments are there.
You can choose to call it all at once or simply feed in each argument piecemeal.
Currying
function add(a, b, c, d) {
return a + b + c + d;
}
add(1, 2, 3, 4) // 10
typeof curry(add) // 'function'
typeof curry(add)(5, 2) // 'function'
curry(add)(5, 5, 5, 5) // 20
curry(add)(10)(20)(30)(40) // 100
Composing
f(a) === b
g(b) === c
g(f(a)) === c
Function breeding
Takes functions with traits you'd like to combine and mashes them together to spawn a brand new one.
First function can have any arity, all others have to be unary.
Composing
function emphasize(string) {
return string + '!';
}
function uppercase(string) {
return string.toUpperCase();
}
const yell = compose(
emphasize,
uppercase,
);
yell('hello') // 'HELLO!'
Ramda
Documentation:
http://ramdajs.com/docs/
Better documentation: https://github.com/ramda/ramda/wiki/What-Function-Should-I-Use%3F
A practical functional library for JavaScript programmers.
import R from 'ramda';
const props = { state: { counter: { count: 10 }}};
const selectCount = R.path(['state', 'counter', 'count']);
selectCount(props) // 10;
// props && props.state && props.state.counter && props.state.counter.count
const props = { state: { counter: {}}};
const selectCountOr = R.pathOr(0, ['state', 'counter', 'count']);
selectCount(props) // 0;
const props = { count: 10 };
const selectCount = R.propOr(0, 'count');
selectCount(props) // 10
Selecting props
import R from 'ramda';
import React from 'react';
const mapProps = R.curry((transformProps, Component) =>
R.compose(Component, transformProps));
const getCurrentCount = R.pathOr(0, ['state', 'counter', 'current']);
const withCountFromState = mapProps((props) => ({
...props,
count: getCurrentCount(props),
}));
const MyCounter = (props) => (<div>Count: {props.count}</div>);
export default withCountFromState(MyCounter);
Computing props
Adding static props
const mapProps = R.curry((transformProps, Component) =>
R.compose(Component, transformProps));
const withProps = input =>
mapProps(props => ({...props, ...input}));
const EnhancedComponent = withProps({
myExtraProp: 'some value',
})(MyComponent);
Adding default props
const mapProps = R.curry((transformProps, Component) =>
R.compose(Component, transformProps));
const withDefaults = input =>
mapProps(props => ({ ...input, ...props }));
const EnhancedComponent = withDefaults({
myExtraProp: 'some value',
})(MyComponent);
Computed or static
const mapProps = R.curry((transformProps, Component) =>
R.compose(Component, transformProps));
const isFunction = R.is(Function);
const withProps = mapProps(props => ({
...props,
...(isFunction(input) ? input(props) : input),
}));
const EnhancedComponent = withProps((props) => ({
myCalculacedExtraProps: doSomething(props),
}))(MyComponent);
Branching
const branch = R.curry((predicateFn, Fallback, Component) => R.ifElse(
predicateFn,
props => (<Component {...props} />),
props => (<Fallback {...props} />),
));
const isVisible = R.propOr(false, 'visible');
const withFallback = branch(isVisible, RenderNothing);
const EnhancedComponent = withFallback((props) => (
<div>This will only show if 'props.visible' is true.</div>
));
Hoisting state management
const withState = R.curry(
(stateName, stateUpdateFn, initialState, BaseComponent) => {
return class extends Component {
state = { value: initialState, };
stateUpdateFn = (fn) => {
this.setState(({ value }) => ({ value: fn(value) }))
}
render() {
const stateProps = {
[stateName]: this.state.value,
[stateUpdateFn]: this.stateUpdateFn,
};
return (<BaseComponent {...this.props} {...stateProps} />);
}
};
});
Hoisting state management
const withCounter = withState('count', 'setCount', 0);
const Counter = withCounter(({ count, setCount }) => (
<div>
<button onClick={() => setCount(count - 1)}>Decrement</button>
{count}
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
));
Accessing lifecycle
const withLifecycle = R.curry((methods = {}, fn) => createClass({
...methods,
render() {
return fn(this.props);
},
}));
const neverUpdate = withLifecycle({
shouldComponentUpdate(nextProps) {
return false; // Do never update this component.
},
});
const IdleComponent = neverUpdate(MyComponent);
Compose everything
const enhance = compose(
mapProps(R.omit(['foo'])),
withReducer('counter', 'dispatch', counterReducer, 10),
withDefaultProps({ something: 'foo' }),
);
const EnhancedComponent = enhance(MyComponent);
Redux in a nutshell
const redux = (reducers, state) => {
const listeners = [];
const reducingFns = R.mapObjIndexed((reducer, key) =>
R.useWith(reducer, [R.prop(key), R.identity]));
const update = R.converge(R.applySpec, [reducingFns]);
const updateState = update(reducers);
return {
dispatch: (action) => {
state = updateState(state, action)
R.forEach(f => f(state), listeners)
},
subscribe: (f) => {
listeners.push(f)
},
};
};
Functional Programming and React
By Sebastian Siemssen
Functional Programming and React
- 1,770