{"id":16884628,"url":"https://github.com/timwis/choo-workshop","last_synced_at":"2026-01-30T05:03:51.658Z","repository":{"id":140604648,"uuid":"73532521","full_name":"timwis/choo-workshop","owner":"timwis","description":"choo: the good parts of react \u0026 redux without the boilerplate","archived":false,"fork":false,"pushed_at":"2016-11-12T15:42:45.000Z","size":7,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"gh-pages","last_synced_at":"2025-04-11T12:52:53.093Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://timwis.com/choo-workshop/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/timwis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-11-12T05:00:41.000Z","updated_at":"2019-09-09T00:08:20.000Z","dependencies_parsed_at":null,"dependency_job_id":"1717e114-a401-40da-9277-ec5810546772","html_url":"https://github.com/timwis/choo-workshop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/timwis/choo-workshop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timwis%2Fchoo-workshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timwis%2Fchoo-workshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timwis%2Fchoo-workshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timwis%2Fchoo-workshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/timwis","download_url":"https://codeload.github.com/timwis/choo-workshop/tar.gz/refs/heads/gh-pages","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timwis%2Fchoo-workshop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28904585,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-30T04:02:34.702Z","status":"ssl_error","status_checked_at":"2026-01-30T04:02:33.562Z","response_time":66,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-13T16:29:33.489Z","updated_at":"2026-01-30T05:03:51.638Z","avatar_url":"https://github.com/timwis.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# choo: the good parts of react \u0026 redux without the boilerplate\n\nTim Wisniewski\n\nChief Data Officer, City of Philadelphia\n\n@timwis -- tim@timwis.com\n\nRepo: github.com/yoshuawuyts/choo\n\n---\n## Outline\n\n1. Quick poll\n2. What you'll get out of this session\n3. Why choo?\n4. Dependencies\n5. Your first app\n\n---\n## Quick poll\n\n1. Software devs?\n2. Built an app in JS?\n3. Used react or redux?\n\n---\n## What you'll get out of this session\n\n- Build a basic todo app in a modern, functional front-end framework\n- We'll cover 90% of the library's footprint\n- The whole workshop is written down so you can pick it up later\n\n---\n## Why choo?\n\n```javascript\nconst choo = require('choo')\nconst html = require('choo/html')\nconst app = choo()\n\napp.model({\n  state: { title: 'Not quite set yet' },\n  reducers: {\n    update: (data, state) =\u003e ({ title: data })\n  }\n})\n\nconst mainView = (state, prev, send) =\u003e html`\n  \u003cmain\u003e\n    \u003ch1\u003eTitle: ${state.title}\u003c/h1\u003e\n    \u003cinput\n      type=\"text\"\n      oninput=${(e) =\u003e send('update', e.target.value)}\u003e\n  \u003c/main\u003e\n`\n\napp.router((route) =\u003e [\n  route('/', mainView)\n])\n\nconst tree = app.start()\ndocument.body.appendChild(tree)\n```\n\n???\n\n- 5kb\n- FUNctional (transparent side effects, immutable, uni-directional data flow)\n- Small api footprint (easy to remember)\n- Minimal tooling\n- Just JavaScript\n\n---\n## Assumptions\n\n- You have NodeJS w/npm\n- Concept of **models** and **views** (as in MVC)\n- Okay with a couple new ES6 features (const, arrow functions, template strings)\n\n### Backup plan\n\nhttp://c9.io\n\n---\n## Your first app\n\n1. github.com/yoshuawuyts/choo\n2. Click **Handbook**\n3. Click **02 your first app**\n\nhttps://yoshuawuyts.gitbooks.io/choo/content/02_your_first_app.html\n\n---\n### Boilerplate\n\n```bash\nnpm init --yes\n```\n\n```bash\nnpm install --save choo\n```\n\n---\n### Rendering data\n\n```javascript\nconst choo = require('choo')\nconst html = require('choo/html')\nconst app = choo()\n\napp.model({\n  state: {\n    todos: [\n      { title: 'Buy milk' },\n      { title: 'Call mum' }\n    ]\n  }\n})\n\nconst view = (state, prev, send) =\u003e {\n  return html`\n    \u003cdiv\u003e\n      \u003ch1\u003eTodos\u003c/h1\u003e\n      \u003cul\u003e\n        ${state.todos.map((todo) =\u003e html`\u003cli\u003e${todo.title}\u003c/li\u003e`)}\n      \u003c/ul\u003e\n    \u003c/div\u003e`\n}\n\napp.router((route) =\u003e [\n  route('/', view)\n])\n\nconst tree = app.start()\ndocument.body.appendChild(tree)\n```\n\n???\n\n- Models are where **state** is contained and where methods for\n  updating the state are defined\n- Views are just functions that return a DOM tree of elements.\n  They are passed the current state, the previous state, and a\n  callback function that can be used to change the state.\n- `1.js`\n\n---\n### Running the app\n\n```bash\nnpm install --global budo\n```\n\n```bash\nbudo index.js --live --open\n```\nor on **c9.io**:\n```bash\nbudo index.js --port $PORT\n```\n\n```\nhttp://workspace-username.c9users.io\n```\n\n---\n### Adding items\n\n```javascript\napp.model({\n  state: {\n    todos: []\n  },\n  reducers: {\n    addTodo: (data, state) =\u003e {\n      const newTodos = state.todos.slice()\n      newTodos.push(data)\n      return { todos: newTodos }\n    }\n  }\n})\n```\n\n```javascript\nconst view = (state, prev, send) =\u003e {\n  return html`\n    \u003cdiv\u003e\n      \u003cform onsubmit=${(e) =\u003e {\n        send('addTodo', { title: e.target.children[0].value })\n        e.preventDefault()\n      }}\u003e\n        \u003cinput type=\"text\" placeholder=\"New item\" id=\"title\"\u003e\n      \u003c/form\u003e\n      \u003cul\u003e\n        ${state.todos.map((todo) =\u003e html`\u003cli\u003e${todo.title}\u003c/li\u003e`)}\n      \u003c/ul\u003e\n    \u003c/div\u003e`\n}\n```\n\n???\n\n- First thought might be to `.push()` into `state.todos`\n- Immutability makes a copy, alters it, returns the copy\n- Allows us to compare state over time\n- Try it out! Woah!\n- `2.js`\n\n---\n### Adding items\n\n```javascript\nconst view = (state, prev, send) =\u003e {\n  return html`\n    \u003cdiv\u003e\n      \u003cform onsubmit=${(e) =\u003e {\n        const input = e.target.children[0]\n        send('addTodo', { title: input.value })\n        input.value = ''\n        e.preventDefault()\n      }}\u003e\n        \u003cinput type=\"text\" placeholder=\"New item\" id=\"title\"\u003e\n      \u003c/form\u003e\n      \u003cul\u003e\n        ${state.todos.map((todo) =\u003e html`\u003cli\u003e${todo.title}\u003c/li\u003e`)}\n      \u003c/ul\u003e\n    \u003c/div\u003e`\n}\n```\n\nYikes...\n\n---\n### Adding items\n\n```javascript\nconst view = (state, prev, send) =\u003e {\n  return html`\n    \u003cdiv\u003e\n      \u003cform onsubmit=${onSubmit}\u003e\n        \u003cinput type=\"text\" placeholder=\"New item\" id=\"title\"\u003e\n      \u003c/form\u003e\n      \u003cul\u003e\n        ${state.todos.map((todo) =\u003e html`\u003cli\u003e${todo.title}\u003c/li\u003e`)}\n      \u003c/ul\u003e\n    \u003c/div\u003e`\n\n  function onSubmit (e) {\n    const input = e.target.children[0]\n    send('addTodo', { title: input.value })\n    input.value = ''\n    e.preventDefault()\n  }\n}\n```\n\nAh, that's better.\n\n???\n\n- Notice it doesn't reset the text in your input\n- `3.js`\n\n---\n### Completion status\n\n```bash\nnpm install xtend\n```\n\n```javascript\nconst extend = require('xtend')\nconst choo = require('choo')\nconst html = require('choo/html')\nconst app = choo()\n```\n\n---\n### Completion status\n\n```javascript\napp.model({\n  state: {\n    todos: []\n  },\n  reducers: {\n    addTodo: (data, state) =\u003e {\n      const todo = extend(data, {\n        completed: false\n      })\n      const newTodos = state.todos.slice()\n      newTodos.push(todo)\n      return { todos: newTodos }\n    }\n  }\n})\n```\n\n???\n\n- Now new items will be stored as `{ title: 'Our title', complete: false }`\n\n---\n### Completion status\n\n```javascript\nconst view = (state, prev, send) =\u003e {\n  return html`\n    \u003cdiv\u003e\n      \u003cform onsubmit=${onSubmit}\u003e\n        \u003cinput type=\"text\" placeholder=\"New item\" id=\"title\"\u003e\n      \u003c/form\u003e\n      \u003cul\u003e\n        ${state.todos.map((todo) =\u003e html`\n          \u003cli\u003e\n            \u003cinput type=\"checkbox\" ${todo.completed ? 'checked' : ''} /\u003e\n            ${todo.title}\n          \u003c/li\u003e`)}\n      \u003c/ul\u003e\n    \u003c/div\u003e`\n\n  function onSubmit (e) {\n    . . .\n}\n```\n\n???\n\n- You'll notice, though, that adding new items resets the checkboxes\n  because checking them doesn't do anything\n- `4.js`\n\n---\n### Completion status\n\n```javascript\nconst view = (state, prev, send) =\u003e {\n  return html`\n    \u003cdiv\u003e\n      \u003cform onsubmit=${onSubmit}\u003e\n        \u003cinput type=\"text\" placeholder=\"New item\" id=\"title\"\u003e\n      \u003c/form\u003e\n      \u003cul\u003e\n        ${state.todos.map((todo, index) =\u003e html`\n          \u003cli\u003e\n            \u003cinput type=\"checkbox\" ${todo.completed ? 'checked' : ''} onchange=${(e) =\u003e {\n              const updates = { completed: e.target.checked }\n              send('updateTodo', { index: index, updates: updates })\n            }} /\u003e\n            ${todo.title}\n          \u003c/li\u003e`)}\n      \u003c/ul\u003e\n    \u003c/div\u003e`\n\n  function onSubmit (e) {\n    . . .\n}\n```\n\n???\n\n- State stores todos as an array\n- To update, we need to know index\n- Here we pass `index` and `updates` to reducer\n\n---\n### Completion status\n\n```javascript\napp.model({\n  state: {\n    todos: []\n  },\n  reducers: {\n    addTodo: (data, state) =\u003e {\n      // ...\n    },\n    updateTodo: (data, state) =\u003e {\n      const newTodos = state.todos.slice()\n      const oldItem = newTodos[data.index]\n      const newItem = extend(oldItem, data.updates)\n      newTodos[data.index] = newItem\n      return { todos: newTodos }\n    }\n  }\n})\n```\n\n???\n\n- Here we create the reducer to update the state,\n  making a copy and returning it\n- Now your app maintains completed state, but refreshing\n  will lose all items\n- `5.js`\n\n---\n### Effects\n\n- Effects are similar to reducers except instead of modifying\n  the state they cause side effects by interacting servers,\n  databases, DOM APIs, etc. Often they'll call a reducer when\n  they're done to update the state.\n\n#### Example\n\n```javascript\n{\n  state: {\n    users: []\n  },\n  reducers: {\n\treceiveUsers: (data, state) =\u003e {\n    \treturn { users: data }\n    }\n  },\n  effects: {\n    fetchUsers: (data, state, send, done) =\u003e {\n      http('api.com/users', (err, response, body) =\u003e {\n        send('receiveUsers', body.users, done)\n      })\n    }\n  }\n}\n```\n\n---\n### Effects\n\n```javascript\n// localStorage wrapper\nconst store = {\n  getAll: (storeName, cb) =\u003e {\n    try {\n      cb(JSON.parse(window.localStorage[storeName]))\n    } catch (e) {\n      cb([])\n    }\n  },\n  add: (storeName, item, cb) =\u003e {\n    store.getAll(storeName, (items) =\u003e {\n      items.push(item)\n      window.localStorage[storeName] = JSON.stringify(items)\n      cb()\n    })\n  },\n  . . . \n```\n\nhttps://yoshuawuyts.gitbooks.io/choo/content/02_your_first_app.html\n\n---\n### Effects\n\n```javascript\napp.model({\n  state: {\n    todos: []\n  },\n  reducers: {\n    receiveTodos: (data, state) =\u003e {\n      return { todos: data }\n    }\n    // ...\n  },\n  effects: {\n    getTodos: (data, state, send, done) =\u003e {\n      store.getAll('todos', (todos) =\u003e {\n        send('receiveTodos', todos, done)\n      })\n    }\n  }\n})\n```\n\n???\n\n- We'll use a method from the snippet called `getAll`\n- Once it completes, we'll use `send()` to pass the data to a reducer\n- Note third param, `done`, which allows effects to be chained together\n\n---\n### Effects\n\n```javascript\nconst view = (state, prev, send) =\u003e {\n  return html`\n    \u003cdiv onload=${() =\u003e send('getTodos')}\u003e\n      \u003cform onsubmit=${onSubmit}\u003e\n        \u003cinput type=\"text\" placeholder=\"New item\" id=\"title\"\u003e\n      \u003c/form\u003e\n      \u003cul\u003e\n        // ...\n      \u003c/ul\u003e\n    \u003c/div\u003e`\n\n  function onSubmit (e) {\n    // ...\n}\n```\n\n```javascript\nlocalStorage.todos = '[{\"title\": \"Test\", \"completed\": false}]'\n```\n\n???\n\n- Now we'll trigger `getTodos` when our view is rendered\n- `6.js`\n\n---\n### Effects\n\n```javascript\napp.model({\n  state: {\n    todos: []\n  },\n  reducers: {\n    receiveTodos: (data, state) =\u003e {...},\n    receiveNewTodo: (data, state) =\u003e {\n      const newTodos = state.todos.slice()\n      newTodos.push(data)\n      return { todos: newTodos }\n    }\n  },\n  effects: {\n    getTodos: (data, state, send, done) =\u003e {...},\n    addTodo: (data, state, send, done) =\u003e {\n      const todo = extend(data, {\n        completed: false\n      })\n\n      store.add('todos', todo, () =\u003e {\n        send('receiveNewTodo', todo, done)\n      })\n    }\n  }\n})\n```\n\n???\n\n- We want `addTodo` to interact w/localStorage as well, so we'll\n  replace it with an effect and add a reducer to receive its data\n- Similar to before: we split functionality and added a side effect\n\n---\n### Effects\n\n```javascript\napp.model({\n  state: {\n    todos: []\n  },\n  reducers: {\n    receiveTodos: (data, state) =\u003e {...},\n    receiveNewTodo: (data, state) =\u003e {...},\n    replaceTodo: (data, state) =\u003e {\n      const newTodos = state.todos.slice()\n      newTodos[data.index] = data.todo\n      return { todos: newTodos }\n    }\n  },\n  effects: {\n    getTodos: (data, state, send, done) =\u003e {...},\n    addTodo: (data, state, send, done) =\u003e {...},\n    updateTodo: (data, state, send, done) =\u003e {\n      const oldTodo = state.todos[data.index]\n      const newTodo = extend(oldTodo, data.updates)\n\n      store.replace('todos', data.index, newTodo, () =\u003e {\n        send('replaceTodo', { index: data.index, todo: newTodo }, done)\n      })\n    }\n  }\n})\n```\n\n???\n\n- Let's do the same for `updateTodo`\n- When you call `send` it looks for reducers _and_ effects by that name,\n  so our view should already be wired up.\n- You should not be able to add items, mark them complete, and refresh\n- `7.js`\n\n---\n### This presentation\n\ntimwis.com/choo-workshop\n\nOh, and we're hiring!\n\n* Data Engineer\n* Front-end / WordPress Developer\n* Product Manager for beta.phila.gov\n\nEmail me: tim.wisniewski@phila.gov\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimwis%2Fchoo-workshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftimwis%2Fchoo-workshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimwis%2Fchoo-workshop/lists"}