A better JavaScript for React/Redux
김동우 @NHNEnt
Clojure는...
2016년부터 (외롭게) 공부중
Spacemacs!
Eric S. Raymond
Paul Graham
Alan Kay
1세대
2세대
80년대
90년대
최근
LISP (1958)
John McCarthy
Clojure (2007)
Rich Hichey
By Rich Hickey
(defn flatten-and-remove-nils
"`interceptors` might have nested collections, and contain nil elements.
return a flat collection, with all nils removed.
This function is 9/10 about giving good error messages."
[id interceptors]
(let [make-chain #(->> % flatten (remove nil?))]
(if-not debug-enabled?
(make-chain interceptors)
(do ;; do a whole lot of development time checks
(when-not (coll? interceptors)
(console :error "expected a collection of interceptors, got:" interceptors))
(let [chain (make-chain interceptors)]
(when (empty? chain)
(console :error "given an empty interceptor chain"))
(when-let [not-i (first (remove interceptor/interceptor? chain))]
(if (fn? not-i)
(console :error "got a function instead of an interceptor" not-i)
(console :error "expected interceptors, but got:" not-i)))
chain)))))
C | 문법 (if, while, for, switch …) statement 와 expression 구분 |
Java | 이름, 문법 (new…) Core 및 라이브러리 API (Math, Date…) |
Scheme | Closure, Lexical Scope 일급함수, 동적 타입, (eval?) |
Self | Prototype 상속 |
Any application that can be written in JavaScript, will eventually be written in JavaScript
- Jeff Atwood 2007 -
// Object
{
name: 'Kim',
age: 32
}
// Array
[1, 2, 3, 4, 5]
;; Vector
[1 2 3 4 5]
;; List
'(1 2 3 4 5)
;; Map
{:name "Kim"
:age 32}
;; Set
#{1 2 3 4 5}
JS
CLJS
// function declaration
function hello(name) {
return 'Hello, ' + name;
}
// function expression
const hello = function(name) {
return 'Hello, ' + name;
}
// arrow function (ES6)
const hello = (name) => 'Hello, ' + name;
// call
hello('Kim');
;; defn
(defn hello [name]
(str "Hello, " name))
;; def + fn
(def hello
(fn [name]
(str "Hello, " name)))
;; def + #()
(def hello #(str "Hello, " %))
;; call
(hello "Kim")
JS
CLJS
const state = {count: 0}
state.count = state.count + 1
;; var
(def state {:count 0})
(def state-a (assoc state :count 1)}
(def state-b (update state :count inc)}
;; atom
(def state (atom {:count 0}))
(swap! state #(update % :count inc))
JS
CLJS
const personA = {
age: 32,
name: {first: 'John', last: 'Doe'}
}
const personB = {
age: 32,
name: {first: 'John', last; 'Doe'}
}
console.log(personA === personB)
// false
(def person-a {:age 32
:name {:first "John"
:last "Doe"}})
(def person-b {:age 32
:name {:first "John"
:last "Doe"}})
(println (= person-a person-b))
;; true
JS
CLJS
JS
CLJS
map, map-indexed, mapcat, reduce, first, filter, find, flatten, get, get-in, group-by, identical?, interleave, interpose, juxt, keys, last, next, nfirst, nnest, partial, partition, partition-all, partition-by, take, take-last, take-nth, take-while, trampoline, transduce, vals....
map, filter, reduce, concat, assign, some, every, keys, find...
+
Lodash.js
(Ramda.js)
JS
CLJS
core.async
Promise, Generator
Async / Await
+
js-csp
(RxJS)
JS | CLJS | |
---|---|---|
Dependency | npm / yarn | Leiningen / Boot |
Scaffolding | Yeoman / CRA | Leiningen / Boot |
Compile | Babel / TypeScript | Leiningen / Boot |
Bundling | Webpack / Rollup | Leiningen / Boot |
Interactive Programming |
Webpack Dev Server | Figwheel |
Hot Module Replace ++
Dead Code Elimination ++
Lisp!
Javascript
lodash.js
Immutable.js
js-csp
Babel
Webpack
npm / Yeoman
ClojureScript
Leiningen
Figwheel
(js/alert "Hello")
(js/document.getElementById "app")
(js/document.body.lastChild.innerHTML.charAt 7)
(js/some.of.my.libraries.api.method "args")
(js/$.ajax #js {:url "/"
:success (fn [res] (js/console.log res))})
;; same as javacript string
(def s "Hello Clojure")
;; method call
(.toUpperCase s) ;; "HELLO CLOJURE"
;; property access
(.-length s) ;; 13
;; chaning
(def my-div (js/document.querySelector "div"))
(.-length (.toUpperCase (.-innerHTML my-div)))
(->> my-div .-innerHTML .toUpperCase .-length)
(.. my-div -innerHTML toUpperCase -length)
;; macro
(def arr (array 1 2 3))
(def obj (js-obj "x" 1 "y" 2))
;; reader literal
(def arr #js [1 2 3])
(def obj #js {:x 1 :y 2})
;; clj->js function
(def arr (clj->js [1 2 3]))
(def obj (clj->js {:x 1 :y 2}))
;; jc->clj function
(def arr-clj (js->clj arr))
(def obj-clj (js->clj obj))
ClojureScript -> Javascript
class Counter extends React.Component {
state = {
count: 0
}
increase() {
this.setState({
count: this.state.count + 1
});
}
render() {
return (
<div>
<button onClick={() => this.increase()}>
Count: {this.state.count}
</button>
</div>
);
}
}
Functional Approach
<div>
<TodoInput />
<ul>
{todos.map(todo =>
<TodoItem todo={todo} />
)}
</ul>
<div>Completed: {completed.length}</div>
</div>
React.createElement(
"div",
null,
React.createElement(TodoInput, null),
React.createElement(
"ul",
null,
todos.map(function (todo) {
return React.createElement(
TodoItem, { todo: todo }
);
})
),
React.createElement(
"div",
null,
"Completed: ",
completed.length
)
);
All React components must act like pure functions with respect to their props.
Whether you declare a component as a function or a class,
it must never modify its own props
Never mutate this.state directly,
as calling setState() afterwards may replace the mutation you made.
Treat this.state as if it were immutable.
SCU
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const isPropsChanged = shallowEqual(this.props, nextProps);
const isStateChanged = shallowEqual(this.state, nextState);
return isPropsChanged || isStateChanged;
}
render() {
return <div className={this.props.className}>foo</div>;
}
}
class MyComponent extends React.PureComponent {
render() {
return <div className={this.props.className}>foo</div>;
}
}
Reagent provides a minimalistic interface between ClojureScript and React.
(def click-count (r/atom 0))
(defn counting-component []
[:div
"The atom " [:code "click-count"] " has value: "
@click-count ". "
[:input {:type "button" :value "Click me!"
:on-click #(swap! click-count inc)}]])
(defn hello-component [name]
[:p "Hello, " name "!"])
(defn say-hello []
[hello-component "world"])
function simpleComponent() {
return (
<div>
<p>I am a component!</p>
<p className="someclass">
I have
<strong>bold</strong>
<span style={{color: "red"}}> and red</span>
text.
</p>
</div>
)
}
React
(defn simple-component []
[:div
[:p "I am a component!"]
[:p.someclass
"I have "
[:strong "bold"]
[:span {:style {:color "red"}} " and red "]
"text."]])
Reagent
const todos = [
'Learn ClojureScript',
'Learn Reagent',
'Learn Reframe'
];
function TodoItem(props) {
return <li className="todo-item">{props.todo}</li>;
}
function TodoList() {
return (
<ul className="todo-list">
{todos.map(todo => <TodoItem todo={todo} />)}
</ul>
);
}
React
(def todos ["Learn ClojureScript"
"Learn Reagent"
"Learn Reframe"])
(defn todo-item [todo]
[:li {:class "todo-item"} todo])
(defn todo-list []
[:ul {:class "todo-list"}
(for [todo todos] [todo-item todo])])
Reagent
function TodoItem({todo, deleteTodo}) {
return (
<li className="todo-item">
{todo.text}
<button onClick={() => deleteTodo(todo.id)}>X</button>
</li>
);
}
class TodoList extends React.Component {
state = {
todos: [
{id: 1, text: 'Learn ClojureScript'},
{id: 2, text: 'Learn Reagent'},
{id: 3, text: 'Learn Reframe'}
]
}
deleteTodo = (targetId) => {
const {todos} = this.state;
this.setState({
todos: todos.filter(({id}) => targetId !== id)
});
}
render() {
return (
<ul className="todo-list">
{this.state.todos.map(todo =>
<TodoItem
key={todo.id}
todo={todo}
deleteTodo={this.deleteTodo} />
)}
</ul>
);
}
}
React
(def todos (r/atom [{:id 1, :text "Learn ClojureScrpt"}
{:id 2, :text "Learn Reagent"}
{:id 3, :text "Learn Reframe"}]))
(defn delete-todo [todo]
(swap! todos (partial remove #(= todo %))))
(defn todo-item [todo]
[:li {:class "todo-item"}
(:text todo)
[:button {:on-click #(delete-todo todo)} "X"]])
(defn todo-list []
[:ul
(for [todo @todos]
^{:key (:id todo)} [todo-item todo])])
Reagent
React
class TodoInput extends React.Component {
state = {value: ''}
onChange = ev => {
this.setState({value: ev.target.value});
}
onKeyPress = ev => {
if (ev.which === 13) {
this.props.addTodo(this.state.value);
}
}
render() {
return (
<input type="text"
onChange={this.onChange}
onKeyPress={this.onKeyPress}
value={this.state.value} />
);
}
}
Reagent
(defn todo-input []
(let [value (r/atom "")
on-change #(reset! value (.. % -target -value))
on-key-press #(if (= (.-which %) 13)
(add-todo @value))]
(fn []
[:input {:type :text
:on-change on-change
:on-key-press on-key-press
:value @value}])))
React
class Counter extends React.PureComponent {
render() {
const {idx, increase, data: {info, value}} = this.props;
return (
<button onClick={() => increase(idx)}>
{info.name} : {value}
</button>
);
}
}
class CounterApp extends React.PureComponent {
state = {
counters: [
{info: {name: "counter1", step: 5}, value: 0},
{info: {name: "counter2", step: 10}, value: 0},
{info: {name: "counter3", step: 15}, value: 0}
]
}
increase = idx => {
const counters = [...this.state.counters];
const target = counters[idx];
counters[idx] = {
...target,
value: target.value + target.info.step
}
this.setState({counters});
}
render() {
return (
<div>
{this.state.counters.map((data, idx) =>
<Counter key={idx} idx={idx}
data={data} increase={this.increase} />
)}
</div>
);
}
}
Reagent
(def counters (r/atom [{:info {:name "count1" :step 5} :value 0}
{:info {:name "count2" :step 10} :value 0}
{:info {:name "count3" :step 15} :value 0}]))
(defn counter [idx {{:keys [name step]} :info value :value}]
(let [increase #(+ step value)
on-click #(swap! counters update-in [idx :value] increase)]
[:button {:on-click on-click}
(str name " : " value)]))
(defn counter-app []
[:div
(map-indexed (fn [idx data]
^{:key idx} [counter idx data]) @counters)])
React | Reagent | |
---|---|---|
HTML | JSX (Syntax) | Hiccup (Data) |
Component | function, Class | function |
Data 전달 | props (this.props) 객체 | arguments |
State 변경 | this.setState() | r/atom 변경 |
State 변경 (부모) | this.setState() 호출하는 함수를 props로 전달 | r/atom 변경 |
성능 | showComponentUpdate() PureComponent |
자동 최적화 |
Redux
기존 MVC
console.log(store.getState())
/*
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }])
case 'TOGGLE_TODO':
return state.map(
(todo, index) =>
action.index === index
? { text: todo.text, completed: !todo.completed }
: todo
)
default:
return state
}
}
const state = {
artist: {
name: {
first: 'Michael',
last: 'Jackson'
},
born: '1958-08-29'
},
genre: ['pop', 'soul', 'disco', 'rock'],
albums: []
}
function updateLastName(state, lastName) {
return {
...state,
artist: {
...state.artist,
name: {
...state.artist.name,
last: lastName
}
}
};
}
const state1 = updateLastName(state, 'Jordan');
const state2 = updateLastName(state, 'Jordan');
console.log(state1 === state2); // false
import _ from 'lodash/fp'
function updateLastName(state, lastName) {
return _.set(state, ['artist', 'name', 'last'], lastName);
}
const state1 = updateLastName(state, 'Jordan');
const state2 = updateLastName(state, 'Jordan');
console.log(state1 === state2); // false
// Immutable 객체로 변경
const stateIM = Immutable.Map(state);
function updateLastName(state, lastName) {
return state.setIn(['artist', 'name', 'last'], lastName);
}
// 비교
const state1 = updateLastName(staetIM, 'Jordan');
const state2 = updateLastName(staetIM, 'Jordan');
console.log(state1 === state2); // false
console.log(state1.equal(state2)); // true
// 일반 JS로 변경
const stateJS = stateIM.toJS();
(def state
{:artist {:name {:first "Michael"
:last "Jackson"}
:born "1958-08-29"}
:genre ["pop" "soul" "disco" "rock"]
:albums []})
(defn update-last-name [state last-name]
(assoc-in state [:artist :name :last] last-name))
(def state1 (update-last-name state "Jordan"))
(def state2 (update-last-name state "Jordan"))
(js/console.log (= state1 state2)) ;; true
https://hackernoon.com/functional-programming-in-javascript-is-an-antipattern-58526819f21e
https://hackernoon.com/functional-programming-in-javascript-is-an-antipattern-58526819f21e
{
type: 'ADD_TODO',
text: 'Learn Reframe'
}
{
type: 'TOGGLE_TODO',
id: 10
}
[:add-todo "Learn Reframe"]
[:toggle-todo 10]
{:db {:todos [{:id 1
:text "Learn Reframe"
:completed false}]}
:http {:method :post
:url "http://my.app/todos"
:on-success [:process-response]
:on-fail [:failed-todos]}}
Redux (+ Reselect)
import { createSelector } from 'reselect'
const getShowing = (state) => state.showing
const getTodos = (state) => state.sortedTodos
export const getVisibleTodos = createSelector(
[getShowing, getTodos],
(showing, todos) => {
switch (showing) {
case 'ALL':
return todos
case 'DONE':
return todos.filter(t => t.completed)
case 'ACTIVE':
return todos.filter(t => !t.completed)
}
}
)
Reframe
(ns todoapp.subs
(:require [re-frame.core :refer [reg-sub]]))
(reg-sub :showing #(:showing %))
(reg-sub :todos #(vals (:sorted-todos %)))
(reg-sub :visible-todos
:<- [:showing]
:<- [:todos]
(fn [[showing todos] _]
(filter (case showing
:done :done
:active (complement :done)
:all identity) todos)))
Redux
Reframe
Redux
Reselect
Reframe
Javascript
Immutable.js
lodash.js
ClojureScript
React
Recompose
Reagent
npm / webpack
Babel / react-create-app
Leiningen
Figwheel