choo icon indicating copy to clipboard operation
choo copied to clipboard

Best pattern to fetch data based on a path

Open zapaiamarce opened this issue 7 years ago • 16 comments

Expected behavior

var html = require('choo/html')
var choo = require('choo')

var app = choo()
app.use(productStore)
app.route('/product/:id', mainView)
app.mount('body')

function mainView (state, emit) {

  return html`  <body onload=${pullData}>
      <h1>product is ${state.product.name}</h1>
    </body>
  `
  function pullData(){
    emitter.emit('fetchProduct');
  }
}

function productStore (state, emitter) {
  emitter.on('fetchProduct', function () {
    fetch('/product/'+state.params.id,function (product) {
       state.product = product;
       emitter.emit('render')
    })
  })
}

I was wondering if this is a good pattern in order to fetch some data when access some URL.

zapaiamarce avatar Mar 28 '17 19:03 zapaiamarce

@qzapaia I'm wondering this too. Though I have some issue with this approach.

The onload hook seems only get triggered when refreshing the page. But if it was triggered by emit('pushState', '/page/userB') from /page/userA (using same component) and the dom has already "loaded" before, the onload will not be triggered.

I have tried to emit the fetchProduct event inside the mainView function, but the problem is that it will run into an infinite loop if emit('render') is get called in store handler and mainView function will be called all the time.

I have worked with React for a while, and normally we could simply use componentDidMount and that event will be triggered when switch from page to page(or component being mount to view). Not sure if onload is the best solution here though.

fraserxu avatar Mar 28 '17 22:03 fraserxu

@qzapaia yeah I think that's quite reasonable

yoshuawuyts avatar Mar 29 '17 08:03 yoshuawuyts

Guys, I think this corresponds to my choo-routehandler framework. Bear in mind it's written for Choo v4, as I haven't got around to looking at v5 yet. However, the core premise of the framework is to facilitate the loading of data required by individual routes, if you see what I mean. So when a route is switched to, it may load data asynchronously if it defines a loadData hook.

I also found on-load inadequate for this sort of thing, so I implemented MutationObserver in choo-routehandler myself, to observe when the rendered route changes. It works well AFAICT.

aknuds1 avatar Mar 29 '17 11:03 aknuds1

@fraserxu ahh yeah so I think I figured out what's happening in your case: you're recreating the tag on each render, so the onload handler is called on each render. You probably want to apply some form of caching through https://github.com/yoshuawuyts/choo#caching-dom-elements which means a lil bit of boilerplate.

But actually I think using a one-off var might be just as easy:

var html = require('choo/html')
var choo = require('choo')

var app = choo()
app.use(productStore)
app.route('/product/:id', mainView)
app.mount('body')

var loaded = false
function mainView (state, emit) {
  if (!loaded) {
    loaded = true
    emitter.emit('fetchProduct');
  }

  return html`
    <body>
      <h1>
        product is ${state.product.name}
      </h1>
    </body>
  `
}

function productStore (state, emitter) {
  emitter.on('fetchProduct', function () {
    fetch('/product/'+state.params.id,function (product) {
       state.product = product;
       emitter.emit('render')
    })
  })
}

yoshuawuyts avatar Mar 29 '17 23:03 yoshuawuyts

Hi @yoshuawuyts thanks for the response. I think I've tried all the solutions here.

The issue with using onload is that is called and only called once for the initial rendering, but it's not what I want as I want to be able to fetch data on router change.

The issue with set a loaded flag it kind of the same that it only runs once, but I want it to run on router change.

The issue to emit('fetchProduct') in mainView() will certainly leads to infinite render and fetch loop.

I've setup a demo repo to show what I mean and hopefully it makes more sense with real code. https://github.com/fraserxu/choo-data-fetching-demo

fraserxu avatar Mar 30 '17 07:03 fraserxu

@fraserxu oh but detecting route changes shouldn't be that hard - e.g. I think something like this should work:

var html = require('choo/html')
var choo = require('choo')

var app = choo()
app.use(productStore)
app.route('/product/:id', mainView)
app.mount('body')

var route = null
function mainView (state, emit) {
  var newRoute = window.location.href
  if (newRoute != route) {
    route = newRoute
    emitter.emit('fetchProduct');
  }

  return html`
    <body>
      <h1>
        product is ${state.product.name}
      </h1>
    </body>
  `
}

function productStore (state, emitter) {
  emitter.on('fetchProduct', function () {
    fetch('/product/'+state.params.id,function (product) {
       state.product = product;
       emitter.emit('render')
    })
  })
}

yoshuawuyts avatar Mar 30 '17 09:03 yoshuawuyts

It does require keeping var route in sync with the current route. I use a helper method for that

const syncRoute = R.curry(
  (
    onChange: (state: StateType, emit: Function) => void,
    viewFunc: ViewFunctionType
  ) => (state, emit) => {
    ifBrowser(() => {
      const newRoute = window.location.href;

      if (state.routing.current !== newRoute) {
        emit(SET_ROUTE, newRoute);
        if (onChange && typeof onChange === "function") onChange(state, emit);
      }
    });
    return viewFunc(state, emit);
  }
);

app.route("*", R.pipe(withLayout, syncRoute(fetchAll)(Main)));

The naming and routing store can probably be improved but that's what I came up with just now. If that's not ideal, someone let me know :O

cideM avatar Jun 19 '17 09:06 cideM

Just throwing my two cents in here :) Feel free to tell me if it's crazy, as either I'm doing it wrong or I may have found a bug with similar problems to #479/#549.

Coming from react land, the event-emitter pattern seems very close to redux actions, so for loading data I would like to use a pattern like so:

function todoStore(state, emitter) {
  const todos = {todo: null}
  state.todos = todos
  
  emitter.on('navigate', ()=> {
    if (someCondition) {
      emitter.emit('todos.fetchTodo', state.params.id)
    }
  })

  emitter.on('todos.fetchTodo', id => {
    loadTodo(id).then(todo => emitter.emit('todos.fetchTodo.done', todo))
  })

  emitter.on('todos.fetchTodo.done', todo => {
    todos.todo = todo
    emitter.emit('render')
  })
}

By introducing a parameter for todos.fetchTodo event, we can decouple that action from current state and fire it off arbitrarily. From there the listener for fetchTodo.done could cache data or do whatever.

The issue I'm having is that the navigate event is always one behind the current page. In other words, for the first page nav, the handler doesn't fire; then the second nav, what should have fired first then fires. Having trouble figuring that out.

jonjaques avatar Aug 30 '17 16:08 jonjaques

Is this resolved in a more elegant way? Only thing keeping me from adopting Choo

JoeLanglois avatar Nov 23 '17 21:11 JoeLanglois

This is a very common issue ! Do you really use Choo in your real project ? @yoshuawuyts ~~Forgive my rudeness, but the routing system in Choo really sucks !~~ I'm very rude, and the routing system in Choo really sucks !

intellecat avatar Nov 28 '17 14:11 intellecat

@chuck911 Why ask for forgiveness when you're consciously rude? It's not that hard to give constructive feedback. "I see a lot of missing functionality in the routing system in Choo. It would be great if I were able to do X. I could try to create a pull request if someone would be willing to mentor/help me?"

marlun avatar Nov 28 '17 15:11 marlun

@chuck911 - You're being very hurtful by saying an open source library that has taken 100s of hours of people's free time sucks. What's more is you haven't suggested anything constructive that solves the issue at hand.

Please take a moment before posting comments that purposefully upset people who are passionate contributors to open source.

josephluck avatar Nov 28 '17 15:11 josephluck

@josephluck @marlun Thank you very much, you are very kind. I was trying to call the author's attention, your replies do help. This issue was posted 8 months ago, the author didn't seem to regard this issue as a real problem. Being nice and polite do nothing here, sometimes someone has to become bad person to tell the truth. It's ok to thumb-down me. I hope this promising framework could be better. @josephluck @marlun Do you gentlemen use this framework usually?

intellecat avatar Nov 29 '17 05:11 intellecat

I've submitted a PR that might make this easier. Probably not quite ready for merge yet, but could be a starting point...

brechtcs avatar Nov 29 '17 18:11 brechtcs

@chuck911 - I respectfully disagree, there's already a few ways that the question in this issue can be addressed, and many people have been kind enough to share their experiences in this thread. Demanding the attention of the author in the way you have is pretty disrespectful.

josephluck avatar Dec 04 '17 09:12 josephluck

Let's keep this conversation positive. One of the reasons that choo is so great is that it keeps the core minimal by allowing others to come up with the userland solutions that work best for them.

I just published nanofetch which makes it easy to write components that fetch remote data. You may find it useful in solving the above issue with slightly less boilerplate.

s3ththompson avatar Jan 12 '18 18:01 s3ththompson