An open API service indexing awesome lists of open source software.

https://github.com/timwis/choo-workshop

choo: the good parts of react & redux without the boilerplate
https://github.com/timwis/choo-workshop

Last synced: 25 days ago
JSON representation

choo: the good parts of react & redux without the boilerplate

Awesome Lists containing this project

README

        

# choo: the good parts of react & redux without the boilerplate

Tim Wisniewski

Chief Data Officer, City of Philadelphia

@timwis -- [email protected]

Repo: github.com/yoshuawuyts/choo

---
## Outline

1. Quick poll
2. What you'll get out of this session
3. Why choo?
4. Dependencies
5. Your first app

---
## Quick poll

1. Software devs?
2. Built an app in JS?
3. Used react or redux?

---
## What you'll get out of this session

- Build a basic todo app in a modern, functional front-end framework
- We'll cover 90% of the library's footprint
- The whole workshop is written down so you can pick it up later

---
## Why choo?

```javascript
const choo = require('choo')
const html = require('choo/html')
const app = choo()

app.model({
state: { title: 'Not quite set yet' },
reducers: {
update: (data, state) => ({ title: data })
}
})

const mainView = (state, prev, send) => html`

Title: ${state.title}


send('update', e.target.value)}>

`

app.router((route) => [
route('/', mainView)
])

const tree = app.start()
document.body.appendChild(tree)
```

???

- 5kb
- FUNctional (transparent side effects, immutable, uni-directional data flow)
- Small api footprint (easy to remember)
- Minimal tooling
- Just JavaScript

---
## Assumptions

- You have NodeJS w/npm
- Concept of **models** and **views** (as in MVC)
- Okay with a couple new ES6 features (const, arrow functions, template strings)

### Backup plan

http://c9.io

---
## Your first app

1. github.com/yoshuawuyts/choo
2. Click **Handbook**
3. Click **02 your first app**

https://yoshuawuyts.gitbooks.io/choo/content/02_your_first_app.html

---
### Boilerplate

```bash
npm init --yes
```

```bash
npm install --save choo
```

---
### Rendering data

```javascript
const choo = require('choo')
const html = require('choo/html')
const app = choo()

app.model({
state: {
todos: [
{ title: 'Buy milk' },
{ title: 'Call mum' }
]
}
})

const view = (state, prev, send) => {
return html`


Todos



    ${state.todos.map((todo) => html`
  • ${todo.title}
  • `)}

`
}

app.router((route) => [
route('/', view)
])

const tree = app.start()
document.body.appendChild(tree)
```

???

- Models are where **state** is contained and where methods for
updating the state are defined
- Views are just functions that return a DOM tree of elements.
They are passed the current state, the previous state, and a
callback function that can be used to change the state.
- `1.js`

---
### Running the app

```bash
npm install --global budo
```

```bash
budo index.js --live --open
```
or on **c9.io**:
```bash
budo index.js --port $PORT
```

```
http://workspace-username.c9users.io
```

---
### Adding items

```javascript
app.model({
state: {
todos: []
},
reducers: {
addTodo: (data, state) => {
const newTodos = state.todos.slice()
newTodos.push(data)
return { todos: newTodos }
}
}
})
```

```javascript
const view = (state, prev, send) => {
return html`


{
send('addTodo', { title: e.target.children[0].value })
e.preventDefault()
}}>



    ${state.todos.map((todo) => html`
  • ${todo.title}
  • `)}

`
}
```

???

- First thought might be to `.push()` into `state.todos`
- Immutability makes a copy, alters it, returns the copy
- Allows us to compare state over time
- Try it out! Woah!
- `2.js`

---
### Adding items

```javascript
const view = (state, prev, send) => {
return html`


{
const input = e.target.children[0]
send('addTodo', { title: input.value })
input.value = ''
e.preventDefault()
}}>



    ${state.todos.map((todo) => html`
  • ${todo.title}
  • `)}

`
}
```

Yikes...

---
### Adding items

```javascript
const view = (state, prev, send) => {
return html`






    ${state.todos.map((todo) => html`
  • ${todo.title}
  • `)}

`

function onSubmit (e) {
const input = e.target.children[0]
send('addTodo', { title: input.value })
input.value = ''
e.preventDefault()
}
}
```

Ah, that's better.

???

- Notice it doesn't reset the text in your input
- `3.js`

---
### Completion status

```bash
npm install xtend
```

```javascript
const extend = require('xtend')
const choo = require('choo')
const html = require('choo/html')
const app = choo()
```

---
### Completion status

```javascript
app.model({
state: {
todos: []
},
reducers: {
addTodo: (data, state) => {
const todo = extend(data, {
completed: false
})
const newTodos = state.todos.slice()
newTodos.push(todo)
return { todos: newTodos }
}
}
})
```

???

- Now new items will be stored as `{ title: 'Our title', complete: false }`

---
### Completion status

```javascript
const view = (state, prev, send) => {
return html`






    ${state.todos.map((todo) => html`


  • ${todo.title}
  • `)}

`

function onSubmit (e) {
. . .
}
```

???

- You'll notice, though, that adding new items resets the checkboxes
because checking them doesn't do anything
- `4.js`

---
### Completion status

```javascript
const view = (state, prev, send) => {
return html`






    ${state.todos.map((todo, index) => html`

  • {
    const updates = { completed: e.target.checked }
    send('updateTodo', { index: index, updates: updates })
    }} />
    ${todo.title}
  • `)}

`

function onSubmit (e) {
. . .
}
```

???

- State stores todos as an array
- To update, we need to know index
- Here we pass `index` and `updates` to reducer

---
### Completion status

```javascript
app.model({
state: {
todos: []
},
reducers: {
addTodo: (data, state) => {
// ...
},
updateTodo: (data, state) => {
const newTodos = state.todos.slice()
const oldItem = newTodos[data.index]
const newItem = extend(oldItem, data.updates)
newTodos[data.index] = newItem
return { todos: newTodos }
}
}
})
```

???

- Here we create the reducer to update the state,
making a copy and returning it
- Now your app maintains completed state, but refreshing
will lose all items
- `5.js`

---
### Effects

- Effects are similar to reducers except instead of modifying
the state they cause side effects by interacting servers,
databases, DOM APIs, etc. Often they'll call a reducer when
they're done to update the state.

#### Example

```javascript
{
state: {
users: []
},
reducers: {
receiveUsers: (data, state) => {
return { users: data }
}
},
effects: {
fetchUsers: (data, state, send, done) => {
http('api.com/users', (err, response, body) => {
send('receiveUsers', body.users, done)
})
}
}
}
```

---
### Effects

```javascript
// localStorage wrapper
const store = {
getAll: (storeName, cb) => {
try {
cb(JSON.parse(window.localStorage[storeName]))
} catch (e) {
cb([])
}
},
add: (storeName, item, cb) => {
store.getAll(storeName, (items) => {
items.push(item)
window.localStorage[storeName] = JSON.stringify(items)
cb()
})
},
. . .
```

https://yoshuawuyts.gitbooks.io/choo/content/02_your_first_app.html

---
### Effects

```javascript
app.model({
state: {
todos: []
},
reducers: {
receiveTodos: (data, state) => {
return { todos: data }
}
// ...
},
effects: {
getTodos: (data, state, send, done) => {
store.getAll('todos', (todos) => {
send('receiveTodos', todos, done)
})
}
}
})
```

???

- We'll use a method from the snippet called `getAll`
- Once it completes, we'll use `send()` to pass the data to a reducer
- Note third param, `done`, which allows effects to be chained together

---
### Effects

```javascript
const view = (state, prev, send) => {
return html`

send('getTodos')}>




    // ...

`

function onSubmit (e) {
// ...
}
```

```javascript
localStorage.todos = '[{"title": "Test", "completed": false}]'
```

???

- Now we'll trigger `getTodos` when our view is rendered
- `6.js`

---
### Effects

```javascript
app.model({
state: {
todos: []
},
reducers: {
receiveTodos: (data, state) => {...},
receiveNewTodo: (data, state) => {
const newTodos = state.todos.slice()
newTodos.push(data)
return { todos: newTodos }
}
},
effects: {
getTodos: (data, state, send, done) => {...},
addTodo: (data, state, send, done) => {
const todo = extend(data, {
completed: false
})

store.add('todos', todo, () => {
send('receiveNewTodo', todo, done)
})
}
}
})
```

???

- We want `addTodo` to interact w/localStorage as well, so we'll
replace it with an effect and add a reducer to receive its data
- Similar to before: we split functionality and added a side effect

---
### Effects

```javascript
app.model({
state: {
todos: []
},
reducers: {
receiveTodos: (data, state) => {...},
receiveNewTodo: (data, state) => {...},
replaceTodo: (data, state) => {
const newTodos = state.todos.slice()
newTodos[data.index] = data.todo
return { todos: newTodos }
}
},
effects: {
getTodos: (data, state, send, done) => {...},
addTodo: (data, state, send, done) => {...},
updateTodo: (data, state, send, done) => {
const oldTodo = state.todos[data.index]
const newTodo = extend(oldTodo, data.updates)

store.replace('todos', data.index, newTodo, () => {
send('replaceTodo', { index: data.index, todo: newTodo }, done)
})
}
}
})
```

???

- Let's do the same for `updateTodo`
- When you call `send` it looks for reducers _and_ effects by that name,
so our view should already be wired up.
- You should not be able to add items, mark them complete, and refresh
- `7.js`

---
### This presentation

timwis.com/choo-workshop

Oh, and we're hiring!

* Data Engineer
* Front-end / WordPress Developer
* Product Manager for beta.phila.gov

Email me: [email protected]