The Elegance Of React

Writing Elegant Code With React, Redux and Ramda

A. Sharif
A. Sharif
Aug 28, 2016 · 8 min read

Compose Components

Let’s begin with what might be the most obvious of all approaches. Think about the following:

const comp = (f, g) => x => f(g(x))
const TodoList = (List, mapItems) => s => List(mapItems(s))
const List = c => <ul>{c}</ul>
const Item = todo => <li key={todo.id}>{todo.name}</li>
const TodoList = compose(List, map(Item))
const result = TodoList([{id: 1, name: 'foo'}])
const App = state => List(map(Item, state)))
import React from 'react'
import
{ render } from 'react-dom'
import
{ compose, map, prop } from 'ramda'
const List = items => <ul>{items}</ul>
const Item = todo => <li key={todo.id}>{todo.text}</li>
const getTodos = prop('todos')
const TodoList = compose(List, map(Item), getTodos)
const props = {todos: [{id: 1, text: 'foo'}, {id: 2, text: 'bar'}]}
render(<TodoList {...props} />, document.getElementById('root'))

Limitations of Compose and Components

We’ve been able to compose components and render a Todo list. Next, let’s see a more common approach, where props are being passed top down. These props can be anything, from callbacks, other components to arrays and objects. Here’s the same Todo List but with an additional Header component.

const Header = title => <h1>A Todo List: {title}</h1>
const List = items => <ul>{items}</ul>
const Item = todo => <li key={todo.id}>{todo.text}</li>
const TodoList = compose(List, map(Item), getTodos)
const TodoHeader = mapStateToProps => Header(mapStateProps)
const TodoHeader = todoState =>
Header(getTitleFromTodoState(todoState))
const mapStateToProps = curry((f, g) => compose(g, f))
const TodoHeader = mapStateToProps(s => s.title, Header)
const result = TodoHeader(state)
const TodoList = mapStateToProps(getTodos, compose(List, map(Item))
const result = TodoList(state)
const combine = curry((c, o) => x => (<div>{c(x)} {o(x)}</div>))
const TodoHeader = mapStateToProps(s => s.title, Header)
const TodoList = mapStateToProps(getTodos, compose(List, map(Item)))
const App = combine(TodoHeader, TodoList)
render(<App {...state} />, document.getElementById('root'))

Reducing Components

Imagine we need to add a Footer to our App just to be able to print out the current year. How would we achieve this? This is what might come to mind first:

const App = combine(TodoHeader, combine(TodoList, TodoFooter))
// array of components 
const comps = [TodoHeader, TodoList, TodoFooter]
const App = comps =>
reduce((acc, x) => combine(acc, x), init, comps)
const combineComponents = (...args) => {
const [first, ...rest] = args
return reduce((acc, c) => combine(acc, c), first, rest)
}
const App = combineComponents(TodoHeader, TodoList, TodoFooter)
render(<App {...state} />, document.getElementById('root'))
const mapStateToProps = curry((f, g) => compose(g, f))
const add = x => x + 1
const multiplyByFour = x => x * 4
// pipe === flip(compose)
const rCompose = flip(compose)
rCompose(add, multiplyByFour)(1) === compose(multiplyByFour, add)(1)
rCompose(add, multiplyByFour)(1) === pipe(add, multiplyByFour)(1)
const mapStateToProps = pipe

Adding Redux

Reduce everything? What follows is some pseudo code, that should help us with creating a mental model of what we’re setting out to achieve.

const App = (state, action) => TodoList
// constants
const ADD_TODO = 'ADD_TODO'
const DELETE_TODO
= 'DELETE_TODO'
// actions
const addTodo = text => ({type: ADD_TODO, text })
const deleteTodo = id => ({ type: DELETE_TODO, id })
// reducers
const todos = createReducer([], {
[ADD_TODO]: (state, action) => [
{ id: getNextId(state), completed: false, text: action.text },
...state
],
[DELETE_TODO]:(state, action) =>
reject(propEq('id', action.id), state),
})
// redux utils
// alternative is to use
defaultTo instead propOr
const createReducer = (init, handlers) =>
(state = init, action) =>
propOr(identity, prop('type', action), handlers)(state, action)
const addOne = add(1)
const getAllIds = pluck('id')
const getMax = reduce(max, 0)
const getNextId = compose(addOne, getMax, getAllIds)
const Add = onSave => (
<div>
<button onClick={() => onSave('foobar')}>Add</button>
</div>
)
const Item = ({todo, removeTodo }) => (
<li key={todo.id}>
{todo.text} <button onClick={removeTodo}>Remove</button>
</li>
)
const getRender = node => app => ReactDOM.render(app, node)
const render = getRender(document.getElementById('root'))
render(<App {...state} />)
// define a bindActionCreator
const bindAction = curry((dispatch, actionCreator) =>
compose(dispatch, actionCreator))
const bindActionCreator = bindAction(store.dispatch)
const run = store.subscribe(() =>
render(
<App {...store.getState()} dispatch={bindActionCreator} />
)
)
const Item = ({todo, onDelete}) => (
<li key={todo.id}>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Remove</button>
</li>
)
// for clearer understanding extracted mapItems
const mapItems = ({todos, onDelete}) =>
map(todo => Item({todo, onDelete}), todos)
const TodoList = pipe(props =>
({todos: props.todos, onDelete: props.dispatch(deleteTodo)}),
compose(List, mapItems)
)

Outro

This should have been an introduction into how to combine Ramda with React and Redux to write more elegant code.

const getPropTypes = prop('propTypes')
const pickKeys = compose(pick, keys)
const mapStateToProps = compose(pickKeys, getPropTypes)
// map state to defined propTypes.
export default connect(mapStateToProps(App))(App)
export default connect(pick(['todos']))(App)

Links

The example

JavaScript Inside

All things JavaScript.

A. Sharif

Written by

A. Sharif

Focusing on quality. Software Development. Product Management.

JavaScript Inside

All things JavaScript.