DEV Community

Robert
Robert

Posted on

React Single File Components with XState

After reading React Single File Components Are Here by @swyx and trying out RedwoodJS's Cells I noticed these SFC's look a lot of state machines!

Shawn's first example (reduced for brevity):

export const QUERY = gql`
  query {
    posts {
      id
      title
      body
      createdAt
    }
  }
`;

export default function MyComponent() {
  const { loading, error, data: posts } = useQuery(QUERY);
  if (error) return <div>Error loading posts: {error.message}</div>;
  if (loading) return <div>Loading...</div>;
  if (!posts.length) return <div>No posts yet!</div>;

  return posts.map(post => (
    <article>
      <h2>{post.title}</h2>
      <div>{post.body}</div>
    </article>
  ));
}

Looks a lot like something you would do using xstate:

export const fetchMachine = Machine(...);

export default function MyComponent() {
  const [state, send] = useMachine(fetchMachine);
  const { error, posts } = state.context;

  if (state.matches("error"))
    return <div>Error loading posts: {error.message}</div>;

  if (state.matches("loading")) return <div>Loading...</div>;

  if (!posts.length) return <div>No posts yet!</div>;

  return posts.map(post => (
    <article>
      <h2>{post.title}</h2>
      <div>{post.body}</div>
    </article>
  ));
}

Redwood Cells

Here's what Redwood Cells look like:

export const QUERY = gql`
  query {
    posts {
      id
      title
      body
      createdAt
    }
  }
`;

export const Loading = () => <div>Loading...</div>;

export const Empty = () => <div>No posts yet!</div>;

export const Failure = ({ error }) => <div>Error loading posts: {error.message}</div>;

export const Success = ({ posts }) => {
  return posts.map(post => (
    <article>
      <h2>{post.title}</h2>
      <div>{post.body}</div>
    </article>
  ));
};

Behind the scenes, Redwood executes the exported GraphQL query and renders the component that matches the state of the request automatically.

What if we could write the UI for our state machine in such a way? For every state you could export a matching component. Let's say our state machine from the previous example looks something like this:

State Machine for Fetch Requests

A basic machine that fetches an arbitrary request, saves the data on success and saves the error on failure. It could have the exact same SFC implementation as a Redwood Cell!

But what about a different state machine? Let's try this one:

State Machine for a Flower

It's a very basic machine for a flower that starts as a seedling, grows when you water it, wilts when you don't water it, and dies when you don't water it when it's wilting. Check it out in the sandbox below!

How would the code for this machine UI look in a SFC format? I'd say kind of like this:

export const Seedling = () => ๐ŸŒฑ

export const Grown = () => ๐ŸŒน

export const Wilting = () => ๐Ÿฅ€

export const Dead = () => ๐Ÿ’€

Cool, right? Now this example is pretty basic, but it makes the UI very readable. Another potential benefits to defining state UI this way could be that if you pass your machine's context to the SFC you can be certain of some piece of context being available inside a state component, eliminating the need for null checks. Kind of like with TypeStates

E.g. in the fetch example:

export const Success = ({ context }) => {
  // Here I am certain context.data is available. No need for
  // if (context.data) return </Stuff >
  // or even
  // state.matches('success') return <Stuff />
  // The latter is abstracted away in our tooling
}

Ok, but why?

Is this useful? I'm not sure! It has the benefits of single file components, it could make your state machine-driven UI more readable, it might prevent even more bugs than using state machines in general, but doing this would probably also have some downsides.

What if I want some piece of UI to be shared between multiple states? The plant example has 3 states that allow you to water (seedling, grown, wilting) and 1 state that doesn't (dead). Now what? Do I duplicate the watering UI 3 times? Maybe that piece of UI should live outside of my state component? Or perhaps I think about re-designing my machine. I could change my machine to look like this:

State Machine for Plant

It now has 2 main states, alive and dead. You can only water it when it's alive. This would change my file to export 2 components: Alive and Dead. Where Alive needs some kind of way to display the sub machine. Perhaps a second file with Seedling, Grown and Wilting, I don't know, I didn't think this far yet!

To conclude; these are all just thoughts, and since you've made it this far, I'd love to hear yours as well! Do you think defining your UI this way could be useful, or it's just limiting?

Top comments (0)