Frontend Development 24 min read

Deep Dive into Umi's useModel: Usage, Optimization, and Source Code Analysis

This article explores Umi's useModel lightweight state management solution, covering basic usage, performance optimizations with selectors, custom hooks, Babel plugin automation, and an in‑depth source code analysis, while also discussing its drawbacks and practical considerations for frontend projects.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Deep Dive into Umi's useModel: Usage, Optimization, and Source Code Analysis

Introduction

The article introduces Umi's useModel as a lightweight state‑management solution for React projects, comparing it with other libraries like Zustand and Pinia, and sets the stage for a detailed exploration of its usage, optimization techniques, and internal implementation.

Basic Usage and Optimization

Following the official Umi documentation, useModel is used by creating a TypeScript file that exports a custom hook; the hook's state becomes globally accessible. The basic example shows a userInfo model and several components consuming it.

// src/models/userInfo.ts
import { useState } from "react";

const userInfoModel = () => {
  const [name, setName] = useState("abc");
  const [age, setAge] = useState(20);

  return {
    name,
    age,
    setName,
    setAge,
  };
};

export default userInfoModel;
// src/pages/index.tsx
import { useModel } from "umi";

const Child1 = () => {
  const { setName } = useModel("userInfo");
  return (
child1 component:
setName((pre) => pre + "s")}>update name
);
};

const Child2 = () => {
  const { setAge } = useModel("userInfo");
  return (
child2 component:
setAge((pre) => pre + 1)}>update age
);
};

const Child3 = () => {
  const { name } = useModel("userInfo");
  return (
child3 component:
{name}
);
};

const Child4 = () => {
  const { age } = useModel("userInfo");
  return (
child4 component:
{age}
);
};

export default function HomePage() {
  return (
    <>
);
}

Without a selector, any state change triggers re‑render of all components that use the model, which can be inefficient.

Encapsulating a Selector Utility

A generic useSelector function is introduced to simplify selector creation, reducing repetitive code.

function useSelector<S extends object, P extends keyof S>(path: P[]): (state: S) => Pick<S, P> {
  return (state: S) => {
    return path.reduce(
      (prev, key) => {
        if (prev[key]) return prev;
        prev[key] = state[key];
        return prev;
      },
      {} as Pick<S, P>
    );
  };
}

Usage in a component demonstrates that only the selected fields cause re‑render.

const Child3 = () => {
  const { name } = useModel("userInfo", useSelector(["name"]));
  console.log("child3 render");
  return (
child3 component:
{name}
);
};

Creating a Performance‑Oriented Wrapper

A usePerformanceModel wrapper is built to replace direct useModel calls, preserving type safety.

function usePerformanceModel(modelName: any, keys: any[]) {
  return useModel(modelName, (state) => {
    return keys.reduce((prev, key) => {
      if (prev[key]) return prev;
      prev[key] = state[key];
      return prev;
    }, {});
  });
}
const Child3 = () => {
  const { name } = usePerformanceModel("userInfo", ["name"]);
  console.log("child3 render");
  return (
child3 component:
{name}
);
};

Type‑level improvements are later added using Parameters and ReturnType to keep full type inference.

import { useModel } from "umi";

type ModelNames = Parameters<typeof useModel>[0];
type ModelState<N extends ModelNames> = ReturnType<typeof useModel<N>>;

function usePerformanceModel<N extends ModelNames, K extends keyof ModelState<N>>(modelName: N, keys: K[]) {
  return useModel(modelName, (state) => {
    return keys.reduce(
      (prev, key) => {
        if (prev[key]) return prev;
        prev[key] = state[key];
        return prev;
      },
      {} as Pick<ModelState<N>, K>
    );
  });
}

Automating Selector Generation with Babel

The article proposes a Babel plugin that automatically injects a selector based on the destructured variables in the surrounding component.

// plugins/transformModel.js
module.exports = function transformModel() {
  return {
    visitor: {
      CallExpression(path) {
        if (path?.node?.callee?.name === "useModel") {
          const declarations = path?.parentPath?.parentPath?.node?.declarations;
          console.log(
            "keys:",
            declarations?.map((item) => item?.init?.property?.name)
          );
        }
      },
    },
  };
};

The plugin is added to the Umi configuration:

// .umirc.ts
import { defineConfig } from "umi";

export default defineConfig({
  routes: [
    { path: "/", component: "index" },
    { path: "/docs", component: "docs" },
  ],
  npmClient: "pnpm",
  plugins: ["@umijs/plugins/dist/model"],
  extraBabelPlugins: ["./plugins/transformModel"],
  model: {},
});

The final version of the plugin parses the generated selector code and injects it as the second argument of useModel when only one argument is present.

module.exports = function transformModel({ parse }) {
  return {
    visitor: {
      CallExpression(path) {
        if (path?.node?.callee?.name === "useModel" && path?.node?.arguments?.length === 1) {
          const declarations = path?.parentPath?.parentPath?.node?.declarations;
          const keys = declarations?.map((item) => item?.init?.property?.name).filter(Boolean);
          if (keys && keys.length) {
            const selectorCodeStr = `(state) => ({ ${keys.map((key) => `${key}: state.${key}`)} })`;
            const selectorAST = parse(selectorCodeStr);
            path?.node?.arguments?.push(selectorAST?.program?.body?.[0]?.expression);
          }
        }
      },
    },
  };
};

Source Code Analysis of useModel

The article walks through the generation of the model registry, the runtime provider, and the executor component that ties model hooks to the React component tree.

Key points include:

Models are collected by a plugin and exported as a namespace‑to‑model map.

A Provider wraps the application, storing model hooks in a dispatcher.

The Executor component runs each model hook, registers update callbacks, and triggers re‑renders when the model state changes.

The useModel hook retrieves the current model state from the dispatcher, optionally applies a selector, and uses deep‑equal comparison to avoid unnecessary updates.

Dispatcher and Executor Mechanics

The dispatcher implements a publish‑subscribe pattern, storing callbacks per namespace. The executor renders the model hook, captures its return value, and on each render calls onUpdate to store the new state and invoke the registered callbacks.

useModel Hook Logic

The simplified useModel implementation extracts the model state from the dispatcher, registers a handler for updates, and uses a selector (if provided) to compare only the needed slice of state. When the selected slice changes, setState is called to re‑render the consuming component.

Drawbacks and Practical Considerations

The article lists several limitations of useModel :

Models execute even when no component consumes them, which can trigger unwanted side‑effects such as network requests.

The automatically injected Provider sits at the top of the React tree, making its placement opaque and potentially conflicting with other providers (e.g., React Router).

Selectors rely on fast-deep-equal , which does not fully support ES6 collections like Map or Set , leading to stale renders for those types.

Because of these issues, the author recommends using useModel for small projects while considering alternative state‑management solutions for larger codebases.

Conclusion

Overall, useModel offers a concise, zero‑boilerplate state‑management experience in Umi, but developers should be aware of its hidden execution model, provider hierarchy, and selector limitations when deciding whether to adopt it for production projects.

Optimizationstate managementReactBabelUmiuseModel
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.