selbekk

Handling Data Fetching with State Machines

Handling Data Fetching with State Machines

July 25, 2018
5 min read

Fetching data just got a bit easier

I get jealous whenever I see people post their passion projects on Twitter. ✨ They typically have all of the fancy tech, with GraphQL, or super-interactive animations or just jaw-dropping performance. In my day-to-day job, I don’t handle any of those concerns. I fetch data.

The mundane reality of real-life apps

When I fetch data in one of my container components (let’s say, a user profile page), I know what to do. I’ve done it a ton of times. I scaffold out a component, complete with some state variables that typically are named data, error and loading. I load some data when it mounts, and complete the boilerplate with some conditional rendering to render different things for different states.

They all kind of look like this:

class SomePage extends React.Component {
  state = {
    isPending: false,
    error: null,
    result: null,
  };
  async componentDidMount() {
    this.setState({ isPending: true, error: null, result: null });
    try {
      const res = await fetch('/api/some-data');
      const data = await res.json();
      this.setState({ error: null, result: data });
    } catch (e) {
      this.setState({ error: e, result: null });
    }
    this.setState({ isPending: false });
  }
  render() {
    // Renders the appropriate thing based on all of that state
  }
}

There’s a ton of state, and it looks exactly the same. Every. Single. Time. Every single time I write this, you could bet your ass I forget to reset SOME part of that state for some particular case.

Say hello to state machines

Let’s make our lives a bit easier by creating a state machine! 💥

Each of my data-fetching components can be in one out of four states — it can be idle (before it has fetched its data), pending, successful or erroneous. It can go from idle to pending, and from pending to either successful or erroneous. Once there, you could reset the state back to idle if you want.

This starts to sound a lot like a state machine. I have to admit that my knowledge on theoretical state machines is somewhat limited, but this article by Mark Shead helped me out a ton! I suggest you spend a few minutes skimming through it if you’re like me.

So let’s start constructing our state machine. A simple outline would look like this:

class ApiStateMachine {
  state = {
    current: 'idle',
  };
  next(input) {
    let nextState;
    switch (this.state.current) {
      case 'idle': nextState = 'pending'; break;
      case 'pending': nextState = input ? 'success' : 'error'; break;
      default: nextState = 'idle';
    }
    this.state.current = nextState;
}

As you can tell, there’s not a lot going on in here. You create an instance of this ApiStateMachine thingamajig, and call the next() -method whenever you want to make the state machine change states. It takes an optional input parameter, which can indicate whether something went well or not.

The main point is that your state can be in one and only one state — it can’t be pending with an error, or idling with a result. You’ll never run the risk of forgetting any part of the state again!

Can I use this in React?

Yeah sure, why not? To make this a bit more React-y, we can make it into a higher order component:

const withApiState = TargetComponent => class extends React.Component {
  state = {
    current: 'idle',
  };
  next = (input) => {
    let nextState;
    switch (this.state.current) {
      case 'idle': nextState = 'pending'; break;
      case 'pending': nextState = input ? 'success' : 'error'; break;
      default: nextState = 'idle';
    }
    this.setState({ current: nextState });
  }
  render() {
    return (
      <TargetComponent 
        {...this.props} 
        apiState={{ ...this.state, next: this.next }} 
      />
    );
}

This HOC accepts a component, and returns a new component that keeps all of this state for you, and provides both the state and a way to go to the “next” stage as props.

This way, we can rewrite most of our data-fetching pages like this:

class SomePage extends React.Component {
  async componentDidMount() {
    const { apiState } = this.props;
    apiState.next();
    try {
      const res = await fetch('/api/some-data');
      const data = await res.json();
      apiState.next(true);
    } catch (e) {
      apiState.next(false);
    }
  }
  render() {
    // Renders the appropriate thing based on props!
  }
}

const SomeBetterPage = withApiState(SomePage);

Improving the API

Now this is starting to look pretty neat! The only thing I’m not very happy about is this .next() method. It isn’t very easy to understand how it works. Let’s fix that.

Instead of the non-descript .next() method, we can expose .pending() , .success() , .error() and even an .idle() method! Although not implemented above, our HOC can potentially make sure to warn us if we call them out of order too — which keeps the state machiny features alive for us.

Bottom line is, we still hide all of the complexity of state handling in a reusable HOC. Here’s the final no-frills version of the API state machine HOC:

const withApiState = TargetComponent =>
  class extends React.Component {
    state = {
      current: "idle"
    };

    apiState = {
      pending: () => this.setState({ current: "pending" }),
      success: () => this.setState({ current: "success" }),
      error: () => this.setState({ current: "error" }),
      idle: () => this.setState({ current: "idle" }),
      isPending: () => this.state.current === "pending",
      isSuccess: () => this.state.current === "success",
      isError: () => this.state.current === "error",
      isIdle: () => this.state.current === "idle"
    };

    render() {
      return <TargetComponent {...this.props} apiState={this.apiState} />;
    }
  };

Not technically a state machine, but how a real-life implementation could be.

This is how you’d use it:

class SomePage extends React.Component {
  async componentDidMount() {
    const { apiState } = this.props;
    apiState.pending();
    try {
      const res = await fetch('/api/some-data');
      const data = await res.json();
      apiState.success();
    } catch (e) {
      apiState.error();
    }
  }
  render() {
    // Renders the appropriate thing based on props!
  }
}

const SomeBetterPage = withApiState(SomePage);

Reads pretty nicely, doesn’t it? I think so too.

Here’s a simple example app where you can play with the result:

Can we do more?

There are of course tons of more cool stuff we could do to improve this naïve implementation, including adding types, dev-only warnings and perhaps a slightly more refined API. You could make it handle error messages for you, or even the actual data fetching as well if you’d so please.

The cool thing about HOCs — and React in general — is how well composition works. So instead of writing a component that “does everything”, you can write several smaller utilities that compose together to do what you need them to do in any given situation.

Here’s an example where I’ve implemented an HOC withData that handles the actual data fetching and state of fetching stuff from an async source. Now, our actual page component can be stateless, and the data fetching logic is shareable!

Here, the withData HOC uses the withApiState HOC to handle the general use case, while still making withApiState available for reuse on its own.

Summary

Separating out reusable logic like API data fetching states can save you a lot of troubles down the line. If you create a HOC to handle all that jazz for you, you can solve it once, and reuse it everywhere :)

Hope you got inspired to create your own API state handler, or just copy mine. Please let me know what you think in the responses, and please give me a few claps if you think this was worth your time. Thank you!

All rights reserved © 2024