Skip to content

christianalfoni/react-states

Repository files navigation

react-states

Explicit states for predictable user experiences

Install

npm install react-states

Description

This video is the initial introduction of the concept:

react-states concept

After exploring this concept at CodeSandbox.io we discovered one flaw in the concept. Using transition effects led to a lot of indirection in the code, which made it very difficult to reason about application flows. The indirection happens because your logic is split between the reducer, which is often a single file, and the component handling the effects of those transitions.

To fix this issue react-states is now co locating state with commands. That means your reducer does not only describe the transitions from one state to another, but can also describe a command to execute as part of that transition.

You can now define your state, transitions and commands as a pure reducer hook. The actual execution of the commands is implemented with the usage of the hook. This is great for separation of concerns and testability.

API

createTransitions

import { createTransitions } from "react-states";

type State =
  | {
      state: "NOT_LOADED";
    }
  | {
      state: "LOADING";
    }
  | {
      state: "LOADED";
      data: string;
    }
  | {
      state: "ERROR";
    };

type Action = {
  type: "FETCH";
};

type Cmd = {
  cmd: "FETCH_DATA";
};

export const useData = createTransitions<State, Action, Cmd>((transition) => ({
  NOT_LOADED: {
    FETCH: () =>
      transition(
        {
          state: "LOADING",
        },
        {
          cmd: "FETCH_DATA",
        }
      ),
  },
  LOADING: {
    FETCH_SUCCESS: ({ data }) =>
      transition({
        state: "LOADED",
        data,
      }),
    FETCH_ERROR: ({ error }) =>
      transition({
        state: "ERROR",
        error,
      }),
  },
  LOADED: {},
  ERROR: {},
}));

The transition function is used to ensure type safety. It is not strictly necessary, but TypeScript does not have exact return types. That means you only get errors on lacking properties. The transition function ensures exact types on your state and commands.

match

Transform state into values and UI.

Exhaustive match

import { match } from "react-states";
import { useData } from "./useData";

const DataComponent = () => {
  const [data, dispatch] = useData(
    {
      FETCH_DATA: async () => {
        const newData = await Promise.resolve("Some data");

        dispatch({
          type: "FETCH_SUCCESS",
          data: newData,
        });
      },
    },
    {
      state: "NOT_LOADED",
    }
  );

  return match(data, {
    NOT_LOADED: () => (
      <button onClick={() => dispatch({ type: "LOAD" })}>Load data</button>
    ),
    LOADING: () => "Loading...",
    LOADED: ({ data }) => <div>Data: {data}</div>,
    ERROR: ({ error }) => <div style={{ color: "red" }}>{error}</div>,
  });
};

Partial match

import { match } from "react-states";
import { useData } from "./useData";

const DataComponent = () => {
  const [data, dispatch] = useData(
    {
      FETCH_DATA: async () => {
        const newData = await Promise.resolve("Some data");

        dispatch({
          type: "FETCH_SUCCESS",
          data: newData,
        });
      },
    },
    {
      state: "NOT_LOADED",
    }
  );

  const dataWithDefault = match(
    data,
    {
      LOADED: ({ data }) => data,
    },
    (otherStates) => "No data yet"
  );

  return <div>Data: {dataWithDefault}</div>;
};

Match by key

import { match } from "react-states";
import { useData } from "./useData";

const DataComponent = () => {
  const [data, dispatch] = useData(
    {
      FETCH_DATA: async () => {
        const newData = await Promise.resolve("Some data");

        dispatch({
          type: "FETCH_SUCCESS",
          data: newData,
        });
      },
    },
    {
      state: "NOT_LOADED",
    }
  );

  const dataWithDefault = match(data, "data")?.data ?? "No data yet";

  return <div>Data: {dataWithDefault}</div>;
};

Debugging

import { debugging } from "react-states";

debugging.active = Boolean(import.meta.DEV);

You could also implement custom behaviour like a keyboard shortcut, localStorage etc.

About

Explicit states for predictable user experiences

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published