react-states
TypeScript icon, indicating that this package has built-in type declarations

8.3.0 • Public • Published

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 =
  | {
      status: "NOT_LOADED";
    }
  | {
      status: "LOADING";
    }
  | {
      status: "LOADED";
      data: string;
    }
  | {
      status: "ERROR";
    };

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

type Effect = {
  type: "FETCH_DATA";
};

const useData = createTransitions<State, Action, Effect>((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: {},
}));

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

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

  return <div />;
};

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 effects.

match

Transform state into values and UI.

Exhaustive match

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

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

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

  return match(state, {
    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 [state, dispatch] = useData(
    {
      FETCH_DATA: async () => {
        const newData = await Promise.resolve("Some data");

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

  const dataWithDefault = match(
    state,
    {
      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 [state, dispatch] = useData(
    {
      FETCH_DATA: async () => {
        const newData = await Promise.resolve("Some data");

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

  const dataWithDefault = match(state, "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.

Dependents (1)

Package Sidebar

Install

npm i react-states

Weekly Downloads

605

Version

8.3.0

License

MIT

Unpacked Size

33.7 kB

Total Files

9

Last publish

Collaborators

  • christianalfoni