ClojureScript
Reagent
Reframe
Javascript
React
Redux
vs
DongWoo Kim @NHNEnt
Clojure
Clojure
- Functional Language
- Immutable Data + First Class Function
- Modern Lisp
- Code as Data -> map, vector
- Dynamic Language
- Polymorphism - Multimethod / Protocol
- Runs on JVM (symbiotic)
Why Clojure?
- A Lisp
- for Functional Programming
- symbiotic with an established Platform
- designed for Concurrency
By Rich Hickey
Why Clojure?
- Functional : Solution to the multi-core problem
- Lisp : Simple Syntax, Homoiconic
- Runs on JVM : Interoperate with Java
- Concurrncy : Software Transactional Memory
- Performance : Shared Immutable Data Structure
- Support : Communities and Tools
by Uncle Bob
Javascript
Javascript
- Designed by Brendan Eich in 1995
- Dynamic & Weakly typed Language
- Multi-paradigm Language
- OO + Functional + Imperative
- Interpreted Language (JIT)
Influenced by...
C | Syntaxes (if, while, for, switch …) Distinguishes statement and expression |
Java | Name, Syntaxes for OOP Core APIs (Math, Date…) |
Scheme | Closure, Lexical Scope First Class Function, Dynamic Type |
Self | Prototype based Inheritence |
Most Popular Language
Most Popular Language
- Browsers can only run Javascript
(...except Web Assembly) - Node.js : Server-Side Application
- Electron : Desktop Application
- React Native : Native Mobile Application
Any application that can be written in JavaScript, will eventually be written in JavaScript
- Jeff Atwood 2007 -
Most Blamed Language
Most Blamed Language
- Book: Javascript Good Parts
- wtfjs
- JavaScript Gotchas
- Common JavaScript Errors
Most Blamed Language
- Started as a trivial script language
- Compatibility: Can't break exisiting web pages
- Has improved significantly since ES2015
-
2017 Most loved Language Ranked #11
(Same as Clojure)
Caveats
Compile to Javascript
- 2006 Google Web Toolkit
- 2007 CoffeeScript
- 2011 Dart
- 2011 ClojureScript
- 2012 TypeScript
- 2012 Elm
- 2013 PureScript
- 2016 BuckleScript / Reason
ClojureScript
Rational
- JavaScript’s Reach
- JavaScript is not Robust
- Client-service Applications are on the Rise
- JavaScript Engines Gain Power
- Google Leads the Way
- The Library Problem
ClojureScript
- Compiler for Clojure that targets Javascript
- Written in Clojure
- Runs on JVM (Cross Compiler)
- Using Google Clojure Library
- Using Google Closure Compiler (GCC)
Closure Library
- Open Source Library set used by Google Services
- Cross-Browser, OOP, Math, String, DOM...
- Used by ClojureScript-generated Javascript
- Intended for use with the Clojure Compiler
Closure Compiler
- Written in Java (runs on JVM)
- Compiles from JavaScript to better JavaScript
- Using JSDoc annotation for better optimizing
- Check syntax, types, dangerous codes
- Minifying & Mangling
- Dead Code Elimination
- Code Splitting
Persistent Data Structure
JS vs CLJS
Similarities
- First Class Function
- Dynamic Typing
- Anonymous function (Lambda)
- Closure & Lexical Scoping
- Destructuring
- Rest Arguments
- Literal Syntax
- JS: Object / Array
- CLJS: Map / Vector / Set / List
Mutable vs Immutable
const state = {count: 0}
state.count = state.count + 1
(def state (atom {:count 0}))
(swap! state #(update % :count inc))
JS
CLJS
Reference vs Value
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
Core Library
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)
Async
JS
CLJS
core.async
Promise, Generator
Async / Await
+
js-csp
(RxJS)
Tools
JS | CLJS | |
---|---|---|
Dependency | npm / yarn | Leiningen / Boot |
Scaffolding | Yeoman | Leiningen / Boot |
Compile | Babel / TypeScript | Leiningen / Boot |
Bundling | Webpack / Rollup | Leiningen / Boot |
Interactive Programming |
Webpack Dev Server | Figwheel |
Comparision
Hot Module Replace ++
Dead Code Elimination ++
Lisp!
Javascript
lodash.js
Immutable.js
js-csp
Babel
Webpack
npm / Yeoman
ClojureScript
Leiningen
Figwheel
JS Interop
Global Vars / API
(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))})
Method / Property
;; 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)
Object / Array
;; 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
React
Declarative
-
Imperative-> Declarative - Don't need to manipulate DOM directly
- Using Virtual DOM Structure (React Element)
- Optimized DOM manipulation by Reconcile Engine
Component Based
- Build encapsulated components that manage their own state
- Build pages by composing these Components
- Send data between components using props
-
Template-> JSX (Javascript Data Structure)
Learn Once,
Write Anywhere
- React Native
- React VR
Sample Code
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>
);
}
}
React
Functional Approach
JSX = Javascript
<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
)
);
Component = Function
- Input : Props
- Ouput : V-DOM(React Element) Tree
All React components must act like pure functions with respect to their props.
Immutable Props
- Props are read-only
Whether you declare a component as a function or a class,
it must never modify its own props
Immutable State
- call setState() for explicit state change
Never mutate this.state directly,
as calling setState() afterwards may replace the mutation you made.
Treat this.state as if it were immutable.
SholuldComponentUpdate
SCU
Immutability for Performance
- shallow compare to determine updating
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return Object.keys(nextProps).every(
key => nextProps[key] === this.props[key]
);
}
render() {
return <div className={this.props.className}>foo</div>;
}
}
class MyComponent extends React.PureComponent {
render() {
return <div className={this.props.className}>foo</div>;
}
}
Reagent
- Define components using plain function
- Define DOM structures using vector, map (Hiccup)
- Use R-atom (extended atom) to manage state
- Automagically re-rendered R-atom changes
- Optimized performance using value compare
Simple React Wrapper
Reagent provides a minimalistic interface between ClojureScript and React.
Sample Code
(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"])
React vs Reagent
Simple Component
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
Simple Component
(defn simple-component []
[:div
[:p "I am a component!"]
[:p.someclass
"I have "
[:strong "bold"]
[:span {:style {:color "red"}} " and red "]
"text."]])
Reagent
Nested Component
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
Nested Component
(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
State
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
State
(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
Controlled Component
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} />
);
}
}
Controlled Component
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}])))
Performance
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.Component {
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>
);
}
}
Performance
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)])
Conclusion
React | Reagent | |
---|---|---|
HTML | JSX (Syntax) | Hiccup (Data) |
Component | function, Class | function |
Pass data | props (this.props) object | arguments |
Change state | this.setState() | change R-atom |
Change state (parent) | Pass a function calling this.setState() via props | change R-atom |
Performance | showComponentUpdate() PureComponent |
Automatic |
Redux
React & State
- Managing complex state with React is hard
- Another layer for managing state is required
- Redux, Relay, MobX...
Redux - Concept
- Uni-directional data flow
- Single store
- CQRS / Event Sourcing
Redux
traditional MVC
Single source of truth
- Store whole application state within a single tree
- Makes it easier to debug or inspect an application
console.log(store.getState())
/*
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/
State is Read-Only
- No setter for the state
- The only way to change the state is to emit an Action
- Actions are just plain object
- Event sourcing -> easy to keep track of state history
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
Pure function & Immutable State
- Reducers are just pure function
- Take the previous state and action
- Return new state
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
}
}
Why Immutable?
- Performance
- Change detection : shallow equality check
- Great fit for React's paradigm
- Time-Travel Debugging
- Easy to store every changes of the state
- Hot Module Replacement
- Easy to test
Immutability
in Javascript
Example
const state = {
artist: {
name: {
first: 'Michael',
last: 'Jackson'
},
born: '1958-08-29'
},
genre: ['pop', 'soul', 'disco', 'rock'],
albums: []
}
Plain Javascript
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
Lodash-fp / Ramda
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.js
// 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();
ClojureScript
(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
Reframe
Reframe
- Release in early 2015 (older than Redux)
- Based on the Reagent (using R-atom)
- Similarities with Redux
- Unidirectional data flow
- Single state / Event sourcing
- Differences with Redux
- Event + Effect (vs Action)
- Subscription
- Everything is data (effect, interceptor)
Action
- Plain object with 'type' property
- Passed to reducers to change the state
- Use middleware to manage side effects
{
type: 'ADD_TODO',
text: 'Learn Reframe'
}
{
type: 'TOGGLE_TODO',
id: 10
}
Event
- Vector (first element is the name of the event)
- Passed to the event handler -> Emit effects
- Can not affect to the state of the application
[:add-todo "Learn Reframe"]
[:toggle-todo 10]
Effect
- Map data describing side effects
- Passed to the effect handlers
{: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]}}
Subscription
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)
}
}
)
Subscription
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 vs Reframe
Redux
Reframe
Conclusion
Redux
Reselect
Reframe
Javascript
Immutable.js
lodash.js
ClojureScript
React
Recompose
Reagent
npm / webpack
Babel / react-create-app
Leiningen
Figwheel
And...
- Simple syntax
- macro
- Clojure libraries
- Interactive Programming
- clojure.spec
- core.async
But...
- Steep learning curve
- JVM...
- Debugging...
Thank you :)
clojurescript-reagent-reframe (ENG)
By DongWoo Kim
clojurescript-reagent-reframe (ENG)
Comparing "Javascript - React - Redux" with "ClojureScript - Reagent - Reframe"
- 837