使用 Model

在组件内使用

作为全局状态使用

通过 useModel 可以获取 Model 的 State、Actions 等。当 Model 的 State 通过 Actions 进行修改后,任何其他使用了该 Model 的组件,都会自动重新渲染。

快速上手 的计数器案例中,我们已经演示了 useModel 的使用,不再重复。

useModel 支持传入多个 Model,多个 Model 的 State 和 Actions 会进行合并后作为返回结果。例如:

const fooModel = model('foo').define({
  state: {
    value: 1,
  },
  actions: {
    add(state) {
      state += 1;
    },
  },
});

const barModel = model('bar').define({
  state: {
    title: 'bar',
  },
  actions: {
    set(state, payload) {
      state.title = payload;
    },
  },
});

const [state, actions] = useModel([fooModel, barModel]);
// 或
const [state, actions] = useModel(fooModel, barModel);

stateactions 的值分别为:

state = {
  value: 1,
  title: 'bar',
};

actions = {
  add(state) {
    state += 1;
  },
  set(state, payload) {
    state.title = payload;
  },
};

useModel 还支持对 State 和 Actions 做 selector 操作,实现对 State 和 Actions 的筛选或重命名,例如:

const fooModel = model('foo').define({
  state: {
    value: 1,
  },
  actions: {
    add(state) {
      state += 1;
    },
  },
});

const barModel = model('bar').define({
  state: {
    value: 'bar',
  },
  actions: {
    set(state, payload) {
      state.value = payload;
    },
  },
});

const [state, actions] = useModel(
  [fooModel, barModel],
  (fooState, barState) => ({
    fooValue: fooState.value,
    barValue: barState.value,
  }), // stateSelector
  (fooActions, barActions) => ({ add: fooActions.add }), // actionsSelector
);

我们通过 stateSelector ,把 fooModelbarModel 中重名的状态做了命名修改,通过 actionsSelector ,过滤掉了 barModel 的 Actions。

如果只需要设置 actionsSelector,可以把 stateSelector 设置为 undefined,作为参数占位。例如:

const [state, actions] = useModel(
  [fooModel, barModel],
  undefined,
  (fooActions, barActions) => ({ add: fooActions.add }), // actionsSelector
);

作为静态状态使用

通过 useStaticModel 获取 Model ,将 Model 中的状态作为静态状态使用。可以保证组件每次访问到的 Model 的 State 都是最新值,但是 Model 的 State 的变化,并不会引起当前组件的重新渲染。

INFO

useStaticModel 的使用方式和 useModel 完全一致。

考虑下面一种场景,有一个组件 Input 负责用户输入,另外一个组件 Search 负责根据用户的输入信息,在点击查询按钮后执行查询操作,我们不希望用户输入过程中的状态变化引起 Search 重新渲染,这时候就可以使用 useStaticModel

import { useStaticModel } from '@modern-js/runtime/model';

function Search() {
  // 这里注意不要解构 state
  const [state] = useStaticModel(searchModel);

  return (
    <div>
      <button
        onClick={async () => {
          const result = await mockSearch(state.input);
          console.log(result);
        }}
      >
        Search
      </button>
    </div>
  );
}
注意

不要解构 useStaticModel 返回的 state,例如改成如下写法: const [{input}] = useStaticModel(searchModel); 将始终获取到 Input 的初始值。

useStaticModel 还适合和 react-three-fiber 等动画库一起使用,因为在动画组件 UI 里绑定会快速变化的状态,容易引起性能问题。这种情况就可以选择使用 useStaticModel,它只会订阅状态,但不会引起视图组件的重新渲染。下面是一个简化示例:

function ThreeComponent() {
  const [state, actions] = useStaticModel(modelA);

  useFrame(() => {
    state.value; // 假设初始化为 0
    actions.setValue(1);
    state.value; // 这里会得到1
  });
}

使用 React 的 refs 也可以实现类似效果,其实 useStaticModel 内部也使用到了 refs。不过直接 useStaticModel 有助于将状态的管理逻辑从组件中解耦,统一收敛到 Model 层。

完整的示例代码可以在这里查看。

作为局部状态使用

通过 useLocalModel 获取 Model ,将 Model 中的状态作为局部状态使用。此时 Model State 的变化,只会引起当前组件的重新渲染,但是不会引起其他使用了该 Model 的组件重新渲染。效果和通过 React 的 useState 管理状态类似,但是可以将状态的管理逻辑从组件中解耦,统一收敛到 Model 层。

INFO

useLocalModel 的使用方式和 useModel 完全一致。

例如,我们修改计数器应用的代码,添加一个有局部状态的计数器组件 LocalCounter

import { useLocalModel } from '@modern-js/runtime/model';

function LocalCounter() {
  const [state, actions] = useLocalModel(countModel);

  return (
    <div>
      <div>local counter: {state.value}</div>
      <button onClick={() => actions.add()}>add</button>
    </div>
  );
}

分别点击 CounterLocalCounteradd 按钮,两者的状态互不影响:

local-model

完整的示例代码可以在这里查看。

在组件外使用

在实际业务场景中,有时候我们需要在 React 组件外使用 Model,例如在工具函数中访问 State、执行 Actions 等。这个时候,我们就需要使用 Store。 Store 是一个底层概念,一般情况下用户接触不到,它负责存储和管理整个应用的状态。Reduck 的 Store 基于 Redux 的 Store 实现,增加了 Reduck 特有的 API,如 use

首先,在组件内调用 useStore 获取当前应用使用的 store 对象,并挂载到组件外的变量上:

let store; // 组件外部 `store` 对象的引用
function setStore(s) {
  store = s;
}
function getStore() {
  return store;
}

function Counter() {
  const [state] = useModel(countModel);
  const store = useStore();
  // 通过 useMemo 避免不必要的重复设置
  useMemo(() => {
    setStore(store);
  }, [store]);

  return (
    <div>
      <div>counter: {state.value}</div>
    </div>
  );
}

通过 store.use 可以获取 Model 对象,store.use 的用法同 useModel 相同。以计数器应用为例,我们在组件树外,每 1s 对计数器值 执行自增操作:

setInterval(() => {
  const store = getStore();
  const [, actions] = store.use(countModel);
  actions.add();
}, 1000);

完整的示例代码可以在这里查看。

INFO

如果是通过 createStore 手动创建的 Store 对象,无需通过 useStore 在组件内获取,即可直接使用。

补充信息

本节涉及的 API 的详细定义,请参考这里