Skip to content

Instantly share code, notes, and snippets.

@frontsideair
Last active February 7, 2017 07:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frontsideair/fa2b7e6775c242a76489c0ebc329ba7f to your computer and use it in GitHub Desktop.
Save frontsideair/fa2b7e6775c242a76489c0ebc329ba7f to your computer and use it in GitHub Desktop.
Some ideas to make React component state management look more like redux
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>ESNextbin Sketch</title>
<!-- test here: https://esnextb.in/?gist=fa2b7e6775c242a76489c0ebc329ba7f -->
</head>
<body>
<div id="app"></div>
</body>
</html>
import React from 'react';
import { render } from 'react-dom';
// This example is about propagating component state in time.
// React's setState is useful if component state is a shallow
// object, as you can change a key and not touch the others.
// But if you want to change something deep within the state,
// things get brittle and complicated. You can use official
// immutability helpers but I find them unwieldly. Here's
// an alternative.
// When you change state, you are usually doing this in respect
// to the previous state. If we pass a function to setState method
// of the Component, we can say, `take previous state and do this
// to it`.
// Let's build helpers for a simple todo list and build the app
// using these helpers.
// This helper is a simple immutable (and curried) variant of
// push method of Array. You can read the signature as, `it takes a
// value and an array`. As expected, it pushes the value to the array.
// But unlike builtin `push`, it returns the pushed array.
const push = (value) => (array) => {
array.push(value)
return array
}
// The curried (or partially applied) nature allows us to create
// a function that will push given value to any array. If you only pass
// the value and not the array, you'll have this function that you
// can do anything with. We'll use this power later.
// This is the `pop` version. Since it doesn't need a value to do its job,
// it only takes an array that it'll pop from. (Popped value is discarded.)
const pop = (array) => {
array.pop()
return array
}
// Previous helpers were about arrays. This is about objects.
// In React, you usually need to change the value of a key in
// an object. This helper takes necessary info to create a function,
// which will take any object, do the transformation, and return
// the object. For simplicity let's call this resulting function
// a `transformer`.
const atKey = (key, changer) => (obj) => {
obj[key] = changer(obj[key])
return obj
}
// Let's think about the required parameters to create a transformer.
// We need a `key` and a `changer`. The key is the one we want to change
// in the object, but what is a changer? Let's assume we passed the key and
// the changer to this function and got back a transformer as a result.
// The object we pass to this transfomer has the key we provided earlier.
// It also has a value at this key. We want to change this value.
// If we set another value to this, we have the same problem we had
// in the beginning. So we have to take previous value of this key
// and do something with it to produce its next value. This is what the
// changer does. It takes previous value at the key of the object and
// generates next value of it, so we don't have the same problems as we go
// deeper in the tree.
// This helper is almost the same as the previous one, but this takes one
// parameter instead of two to generate a transformer. This single parameter
// is an object where keys are the keys we want to change and values are
// the changers which will do the job. This function changes multiple keys
// of the given object at once.
const atKeys = (changersByKeys) => (obj) => {
for (let key in changersByKeys) {
if (changersByKeys.hasOwnProperty(key)) {
const changer = changersByKeys[key]
obj[key] = changer(obj[key])
}
}
return obj
}
// Now let's get back to the transformers. They sound a lot like changers.
// That's because changers ARE transformers. If a value in an object is a number,
// the changer can increment it. If this value is another object, then we can
// reuse these helpers to manipulate it. If the value is an array, we can
// use `push` and `pop`, as they are transformers too. In fact, any function
// that takes X and returns X after performing some instructions IS a transformer.
// And since object helpers take transformers too, we can compose them to change
// more complex structures.
// (To clarify, these helpers are functions that take instructions to generate
// transformers. Except `pop`, since it doesn't need any instructions, it already
// is a transformer.)
// Let's see how that all works in action.
class Todos extends React.Component {
constructor(props) {
super(props)
// The state is an object, where `todos` is the list of strings,
// and `todo` is the string representing the value of input box.
this.state = { todos: [], todo: '' }
}
// This function moves the input text to todos array and clears the input.
addTodo() {
this.setState( // We pass it a function instead of an object
atKeys( // Since out state is an object, we use an object transformer
{
// Key is todos, value is the changer. Since our todos is an array,
// and we want to push a new todo, we use the push array helper.
// To generate a transformer, we provide the value it will push,
// which is the input value.
'todos': push(this.state.todo),
// We want to discard previous value and set it to empty string,
// so our transformer function disregards its arguments and sets it
// to empty string.
'todo': () => ''
}
)
)
}
// This removes the last added todo from todos.
removeTodo() {
// This is easy, since we know we want to pop the last value from the array
// at the todos key of our state object. Since we don't need any knowledge
// to pop, we don't call it with any arguments; it's already a transformer.
this.setState(atKey('todos', pop))
}
// We take the new input value and overwrite it in our state.
// This might've been better with setState since we don't use previous
// value for transformation, but it's not too bad either and it's here for
// illustration purposes.
changeText(text) {
this.setState(atKey('todo', () => text))
}
// The view; it draws todos, input, buttons and attaches callbacks.
render() {
return (
<div>
<ul>
{ this.state.todos.map((todo, index) => <li key={index}>{todo}</li>) }
</ul>
<input
type='text'
onChange={e => this.changeText(e.target.value)}
value={this.state.todo}
/>
<button
onClick={this.addTodo.bind(this)}
disabled={this.state.todo.length === 0}
>
add
</button>
<button
onClick={this.removeTodo.bind(this)}
disabled={this.state.todos.length === 0}
>
remove
</button>
</div>
)
}
}
// I hope this helped you in any way.
render(<Todos />, document.querySelector('#app'));
{
"name": "esnextbin-sketch",
"version": "0.0.0",
"dependencies": {
"babel-runtime": "6.9.2",
"react": "15.0.2",
"react-dom": "15.0.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment