Last active
February 7, 2017 07:24
-
-
Save frontsideair/fa2b7e6775c242a76489c0ebc329ba7f to your computer and use it in GitHub Desktop.
Some ideas to make React component state management look more like redux
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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