Skip to content

Instantly share code, notes, and snippets.

@yoshuawuyts
Created November 1, 2016 23:21
Show Gist options
  • Save yoshuawuyts/b5e5b3e7aacbee85a3e61b8a626709ab to your computer and use it in GitHub Desktop.
Save yoshuawuyts/b5e5b3e7aacbee85a3e61b8a626709ab to your computer and use it in GitHub Desktop.

uppy

The last word on uploading files.

"Uppy is a sleek, modular file uploader that integrates seemlessly with any framework. It's fast, easy to use and let's you worry about more important problems than building a file uploader.

Readme

  • capabilities is mentioned before explained
  • capabilities could also be confused with the crypto equivalent - different meaning tho it's cool probably
  • capabilities also seem enumerable - enumerating them is probably worth it
  • addFile() mentions plugin types - shoulda been mentioned sooner
  • consider using hemingway
  • or run this:
#!/bin/zsh

where alex >/dev/null
[ $? -eq 0 ] || npm i -g alex

where write-good >/dev/null
[ $? -eq 0 ] || npm i -g write-good

where aspell >/dev/null
[ $? -eq 0 ] || brew install aspell

printf '[aspell] running\n'
aspell check "$1"
printf '[write-good] running\n'
write-good "$1"
printf '[alex] running\n'
alex "$1"

Issues

  • file size cuts rely on dead code elimination rather than specific imports
  • terminology isn't always clear
  • mash up of different API types (event emitter, external state, prototypes)
  • not sure how to create new plugins

Core

  • import with / is not great for file sizes; not necessarily an issue with rollup but that's not for everyone
  • new can be unwieldy - use if (!(this insteanceof Foo))
  • Core is an odd name; the thing being used is the uppy file transfer framework; all instances of Core in code will look odd in the light of a real application I reckon
  • The core setup discourages third party integrations; they'd be imported differenty which leaves others to feel a bit lackluster
  • feels use of prototypes is leaking into API design - it's an implementation detail, really

Plugins

  • found it unintuitive they plugins were called by uppy rather than initialized by the user
  • found it unintuitive args are passed later
  • feels technical implementation is leaking into plugin design
  • using closures is fine and given it's not in the hot path it's fast enough
  • if there's only a single mandatory argument, it makes sense to make it the first arg passed
    • breaks the current single opts arg passing paradigm
    • makes for a more intuitive API
    • again: not hot path so it's all cool

proposal

state [patch] -> uppy -> state [complete]
const Progress = require('uppy/progress')
const dnd = require('uppy/drag-and-drop')
const tus10 = require('uppy/tus10')
const uppy = require('uppy')

const upload = uppy()
upload.use(dnd('#drop-target'))

upload.use(function (state, cb) {
  const newState = changeState
  cb(null, newState)
})

upload.use(Progress({ appendChild: 'body' }))

app.model({
  namespace: 'uppy',
  reducers: {
    update: function(state, data) {
      return data
    },
    addFile: function (state, data) {
      return state.files.push(data)
    }
  }
  state: {}
})

function (state, prev, send) {
  const opts = { thumbnails: false }
  const el = upload(opts, (err, state) => {
    if (err) throw err
    send('update', state)
  })
  document.body.appendChild(el)
}

el.set({ my: 'state' })
  • so .use() would detect the .plugin value on any given value passed in and use that as a plugin - allows for using a result as both a plugin and DOM render target without needing to introduce more keywords. - Just pass it in; similar to how pull-stream does it - lil bit of polymorphism is cool

API

We're not Java - heavy taxonomy is not cool. Uppy should be allowed to modify state in whichever way it wants. When all updates are done, callback is called with new state which can be used by any other framework if desired.

Default state:

{ files: [] }

Internal:

{ files: [ { file: File, thumbnail: Image }] }
// or
{ files: [], thumbnails: { filename: Image } }
  • getState - remove and make part of callback
  • setState - rename to set(), partial setting of vars is cool
  • updateAll() - if state is flushed on each set, this is not needed
  • updateMeta() - make files part of state
  • addFile() - just push to files array OR perhaps we do need a special file adding API - it could make sense; matter of taste and what a file looks like internally
  • capabilities - not needed; just read out state
  • log - not needed; hooks that tie into lifecycle events might be more interesting - any more debuggint than that feels off
  • .on(), .emit(), .emitter - not needed; use state
uppy.set({ files: [ File ] })

Plugins

  • Drag and Drop & Progressbar are inconsistent
    • drag and drop adds functionality to a selector
    • progress injects an element into the selector
@arturi
Copy link

arturi commented Nov 13, 2016

First of all, thanks for you suggestions on everything! We are going to be gradually implementing most of them. These are questions I still have, I understand some of them might be answered in your proposal, this means I didn’t understand that part, sorry for that.

1. Do you think we should separate logic components/plugins from views? Now they are coupled together — when you update state (logic), re-render is called on all plugins. If we do that, we can in theory re-use logic in all environments that support JS, while suppling separate React/React Native/JSX/Bel/Angular/Whatever UI component.

So, for example, for Google Drive plugin this would mean you can do something like this:

const uppy = Uppy()
const gdrive = GoogleDrive()
const myView = require('/myview.js')
uppy.use(gdrive)

const el = myView({
  files: gdrive.getFiles,
  onSelect: uppy.addFile
})

So gdrive only provides logic gdrive.getFiles(), gdrive.getFileInfo(fileID). Maybe this is close to your proposal? But then how do we re-render myView automatically? And all setup becomes complicated for the user, like “combine this logic with this view that accepts the right props”.

2.

Uppy should be allowed to modify state in whichever way it wants.

This works when I need to add a file, or show a message, for example, in some plugin:

function DragDrop (state, cb) {
  const newState = {
    informer: 'File has been selected!',
    files: newFileArray
  }
  cb(null, newState)
}

What about when a user clicks “upload” button in some view, say Dashboard plugin, and I need to start the upload, which is handled by Uploader plugin? That’s what we had events for — similar to reducers in Choo: click --> uppy.emit('core:start-upload'), which is same as send('start-upload') in Choo. Example: uploader plugin reacts to remove-file event, which happens when file is removed from state, and cancels the upload. How would this work if plugins can only modify state, should they set a flag like newState = {beginUpload: true}?

In short, plugin to plugin communication. You do have model and send in your proposal, but I don’t get what is that function that returns el — uppy itself? But we might want to mount to multiple places, like we do know, and like you show with upload.use(Progress({ appendChild: 'body' })).

so .use() would detect the .plugin value on any given value passed in and use that as a plugin

Don’t get that part. In your proposed example plugins look like this: function (state, cb) {}. Where would the .plugin go? Or do you mean we keep current structure where plugins are objects that have methods and properties?

3. Props vs send/dispatch — components can be re-used easier when they only get props, right?

If I understand correctly, the philosophy is sort of: Choo is for apps/smart widgets, so we have (state, prev, send), when Bel is for simple elements, so just pass whatever they need: (onSelect, onRemove, onDone, currentProgress).

4. State can sometimes get updated like 10 times per second, and I only need to show progress every second. Where do I debounce this: in uploader plugin that updates state with progress, or in view that displays progress?

5. In Choo you use Request Animation Frame. Should we do that too? What if we just wrap yo.update() in a window.requestAnimationFrame()? Not sure I understand why raf and nanoraf modules exist (I did look inside, but still confused), node support?

6. We might feel like yo-yo is not working for us, because:

  • maybe we want JSX
  • to avoid issues like “extra network request with img src=
  • be able to convert template strings to document.createElement while using babel and ES6 (currently I feel like yo-yoify can’t handle that).

In that case, what do you think we could switch to?

So far I’ve found Preact’s virtual dom and rendering works like I expect, but I did not use any of the component magic there, just virtual dom and rendering, here it is, preact+JSX and preact+hyperx: http://www.webpackbin.com/NJymHWsJf. The virtual-dom module is not good with SVG — didn’t play nice with our views last I tried. Any other good virtual-dom modules I overlooked?

In short: the current standard, React, is good for apps, but too big and complex for a lib. And yo-yo is small and cool, and I like it, but not battle-tested enough, so we are running into issues that have been solved in React and various virtual-dom.

@hedgerh
Copy link

hedgerh commented Nov 15, 2016

@arturi, I can answer this question:

  1. State can sometimes get updated like 10 times per second, and I only need to show progress every second. Where do I debounce this: in uploader plugin that updates state with progress, or in view that displays progress?

The view should only be taking state and outputting something based on that state. If you only need to update progress once per second, you want to debounce where you update the progress state to once per second.

@yoshuawuyts
Copy link
Author

  1. Do you think we should separate logic components/plugins from views? Now
    they are coupled together — when you update state (logic), re-render is
    called on all plugins. If we do that, we can in theory re-use logic in all
    environments that support JS, while suppling separate React/React
    Native/JSX/Bel/Angular/Whatever UI component.

Yeah I think that sounds good - from what you're saying that leads to a less
coupled architecture which is a good thing.

In the example you posted, the contents of the returned el can be updated by
uppy without any trouble, causing the DOM to be updated in return. You're
right in that it does complicate the views a little, but I feel that's the
nature of how unidirectional updates work; if you provide default props the
user can mount into their global state things become easier tho.


How would this work if plugins can only modify state, should they set a flag
like newState = {beginUpload: true}?

Yup, I feel using proper delimited names this would work well; in essence it's
the same idea as a central event bus, but using a different (more convenient)
API


But we might want to mount to multiple places

I think that mounting in several places means you've got different elements
that need to be mounted. If that's the case then those elements are most likely
generated by different plugins, which could each return their own DOM element.


so .use() would detect the .plugin value on any given value passed in and use
that as a plugin

Don’t get that part. In your proposed example plugins look
like this: function (state, cb) {}. Where would the .plugin go? Or do you
mean we keep current structure where plugins are objects that have methods
and properties?

const uppy = require('uppy')
const html = require('bel')

const el = myCoolPlugin()

const upload = uppy()
upload.use(el) // reads out the .plugin value, yay!

document.body.appendChild(el)

function myCoolPlugin () {
  const el = html`<main>DOM element goes here</main>`
  el.plugin = function (my, params) {
    // this is a property on our dom element that can be picked up by uppy.
    // This makes it so it can both be rendered on the DOM as passe directly
    // into Uppy
  }
  return el
}

  1. Yup, agree to all you said there

Where do I debounce this: in uploader plugin that updates state with
progress, or in view that displays progress?

Good question. I don't have an answer from experience there, but instinctively
I'd say both.


  1. In Choo you use Request Animation Frame. Should we do that too? What if we
    just wrap yo.update() in a window.requestAnimationFrame()? Not sure I
    understand why raf and nanoraf modules exist (I did look inside, but still
    confused), node support?

nanoraf exists to make it so re-renders don't happen if new state wasn't
flushed down. Actually refreshing at 60fps causes most CPUs to spin so you
don't wanna do that; instead only re-render when new stuff happens, capped at
60fps is like a good way of doing things. That's what nanoraf does for you. the
raf package is a polyfill for browsers that don't support it.


We might feel like yo-yo is not working for us

Yeah, yo-yo is a low-level library - it might not be for everyone. I'm not sure
what I'd recommend for building libraries if not yo-yo; using JSX won't
necessarily help with things like this. Have you taken a look at
cache-element at all? - It's a bit young still, but I think it should help
squash some of the duplicate fetch issues at least.


I hope this provides a sufficient answer to your questions! ✨

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment