Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to handle side effects? (AJAX/async/fetch) #295

Open
kmckee opened this issue Dec 9, 2018 · 6 comments
Open

How to handle side effects? (AJAX/async/fetch) #295

kmckee opened this issue Dec 9, 2018 · 6 comments
Labels

Comments

@kmckee
Copy link

kmckee commented Dec 9, 2018

I've looked all over to try and find some examples of how to handle state that deals with asynchronous transitions but I've come up empty.

Are there any examples floating around of how to handle this? I feel like I'm missing something, but I don't know what it is.

class Root {
    tourUrl = String;
    tour = Tour;
    load(url) {
        // Transition that asynchronously updates state
        // and returns a new state.
        return fetch(url)
            .then(res => res.json())
            .then(tour => {
                return this.tourUrl.set(url)
                           .tour.set(tour);
            });
    }
}

Thanks!

@taras
Copy link
Member

taras commented Dec 9, 2018

Hi @kmckee,

We don't currently have a microstaty way of working with asynchrony. For now, we recommend keeping your types pure - meaning without side effects. To use asynchrony with Microstates, make your API request outside of the microstate and put value into the microstate as necessary.

Once we land references that are currently in microstates/lab, we plan to start working on concurrency mechanism that will make working with asynchrony as fun as microstates are meant to be.

We don't know exactly what that will look like, but it might be a higher order type that will interact with the identity to allow to manage side effects using a structured concurrency library that doesn't exist yet.

Do you like me to give you an example of how we use Microstates + Ajax today?

@kmckee
Copy link
Author

kmckee commented Dec 9, 2018

Hi @taras! Thanks so much for helping me understand. I'm pretty excited about this project and I'm hoping to find ways I can start using it and learning more.

I think an example of how to use microstates with ajax based on where things are today would be a great addition to the documentation, or even if it's just in the issues it would be discoverable.

Thanks again!

@taras
Copy link
Member

taras commented Dec 10, 2018

I made an example CodeSandbox for you with React https://codesandbox.io/s/r1mvjl4qwq

It uses an Async Higher Order Type. It accepts a type and returns a Microstate type for managing async functionality. It gives you a state machine for the async state of fetching the type you provided.

function Async(Type) {
  return class Async {
    content = Type;
    loading = Boolean;
    error = create();

    get isError() {
      return !!this.error.state;
    }

    get isLoading() {
      return this.loading.state;
    }

    get errorMessage() {
      return this.error.state.message;
    }

    start() {
      return this.loading.set(true).content.set(null);
    }

    finish(content) {
      return this.loading.set(false).content.set(content);
    }

    finishWithError(error) {
      return this.error.set(error).loading.set(false);
    }
  };
}

Here is how you'd actually use it,

class State {
  tour = Async(Tour);
}

class App extends React.Component {
  state = {
    $: Store(create(State), $ => this.setState({ $ }))
  };

  // you call this to kickoff the transition  
  fetch = async () => {
    let { $ } = this.state;

    $.tour.start();

    let tour;
    try {
      tour = await fetch();
      // update the microstate
      $.tour.finish(tour); 
    } catch (e) {
      // this will capture the error
      $.tour.finishWithError(e); 
    }

    return tour;
  };

  render() {
    let { $ } = this.state;

    return (
      <div>
        <span>
          {$.tour.isLoading
            ? "Loading..."
            : $.tour.isError
            ? $.tour.errorMessage
            : $.tour.content.name.state}
        </span>
        <button onClick={this.fetch}>Fetch tour</button>
      </div>
    );
  }
}

Does this help?

@cowboyd
Copy link
Member

cowboyd commented Dec 10, 2018

Hey @kmckee. As Taras mentioned, we're working on a 'microstate-y' solution, but even once that lands, the state will always be that: just state. You won't actually have any side-effects inside your microstates, only the reflection of what those side-effects mean to your application.

The way you would do this today is to externalize your effects in a simple function.

Here's how I would (roughly) model it today. First, I'd define the entire life cycle of the request as a state machine:

class TourRequest {
  isIdle = Boolean;
  isPending = Boolean;
  isRejected = Boolean;
  isResolved = Boolean;
  url = String;
  tour = Tour;
  error = Any;

  initialize() {
    return this.isIdle.set(true);
  }

  start(url) {
    return this
      .isIdle.set(false)
      .isPending.set(true)
      .url.set(url)
  }

  resolve(data) {
    return this
      .isPending.set(false)
      .isResolved.set(true)
      .tour.set(data);
  }

  reject(error) {
    return this
      .isPending.set(false)
      .isRejected.set(true)
      .error.set(error);
  }
}

Then, I'd define an async function that will "drive" this state through the side-effects.

async function loadTour(url, state) {
  let pending = state.start(url);
  try {
    let result = await fetch(url);
    return pending.resolve(result);
  } catch (error) {
    return pending.reject(error)
  }
}

Note that state here in this async function is from a microstate Store so that your isPending state will be visible outside of the loadTour function (and not just bound to the local pending variable.)

To do that, you'd probably set this up in a component somewhere.

let root = Store(create(TourRequest), state => doSomethingToRenderState(state))

/// and then somewhere else
loadURL(root);

Of course, once this is working, you can introduce abstractions that let you perform an Ajax request over any type by having a function that returns a request type custom tailored for your return value:

function Ajax(Type) {
  return class Ajax {
    result = Type;
    error = Any;
    isIdle = Boolean;
    // the rest of the request state machine here....
  }
}

create(Ajax(Tour)) //=> a request for `Tour` objects
create(Ajax(User)) //=> a request for a `User`.
create(Ajax([Tour])) //=> request an array of `Tour` objects

So this is a really long winded way of saying that really, you can manage the side-effects any way you want, and the microstate will be your faithful immutable model. From the Microstate's perspective, It's really no-different than the way that state transitions happen normally with mouse events, keyboard events, and other browser events, except that the browser is the one managing the async side-effects instead of the user.

@cowboyd
Copy link
Member

cowboyd commented Dec 10, 2018

Oh no! I had this tab open so that I could answer first thing Monday morning, but I see that @taras gave you a better answer (with working code!) in that time.

@taras
Copy link
Member

taras commented Dec 10, 2018

@cowboyd I will update my example to use async await.

@cowboyd cowboyd added question and removed triage labels Dec 12, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants