-
Notifications
You must be signed in to change notification settings - Fork 53
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
Comments
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? |
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! |
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? |
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 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. |
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. |
@cowboyd I will update my example to use async await. |
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.
Thanks!
The text was updated successfully, but these errors were encountered: