Use Models

Using Models in Components

Using as Global State

useModel can be used to obtain the State, Actions, and other information of the Model. When the State of the Model is modified by Actions, any other components that use the Model will automatically re-render.

In the counter example in Quick Start, we have demonstrated the use of useModel and will not repeat it here.

useModel supports passing multiple Models, and the State and Actions of multiple Models will be merged and returned as the result. For example:

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]);
// Or
const [state, actions] = useModel(fooModel, barModel);

state and actions value are:

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

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

useModel also supports selector operations on State and Actions to filter or rename State and Actions. For example:

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
);

We use stateSelector to rename the states with the same name in fooModel and barModel. We use actionsSelector to filter out the Actions of barModel.

If only actionsSelector needs to be set, you can set stateSelector to undefined as a placeholder. For example:

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

Using as Static State

useStaticModel can be used to obtain the Model and use the state of the Model as a static state. This ensures that the State accessed by the component is always the latest value, but the change of the Model's State does not cause the current component to re-render.

INFO

The usage of useStaticModel is exactly the same as useModel.

Consider the following scenario: there is an Input component responsible for user input, and another Search component responsible for executing a search operation after the user input information is entered and the search button is clicked. We do not want the state changes during the user input process to cause Search to re-render. In this case, useStaticModel can be used:

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

function Search() {
  // should not be destructured
  const [state] = useStaticModel(searchModel);

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

Do not destructure the state returned by useStaticModel. For example, changing it to the following code: const [{input}] = useStaticModel(searchModel); will always get the initial value of Input.

useStaticModel is also suitable for use with animation libraries such as react-three-fiber, because binding fast-changing states in animation component UI can easily cause performance issues. In this case, you can choose to use useStaticModel, which only subscribes to the State but does not cause the view component to re-render. Here is a simplified example:

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

  useFrame(() => {
    state.value;
    actions.setValue(1);
    state.value;
  });
}

Using React's refs can also achieve similar effects. In fact, useStaticModel also uses refs internally. However, using useStaticModel directly helps decouple the state management logic from the component and converge it into the Model layer.

The complete sample code can be found here.

Using as Local State

useLocalModel can be used to obtain the Model and use the state of the Model as local state. At this time, the change of the Model State only causes the current component to re-render, but does not cause other components that use the Model to re-render. The effect is similar to managing state through useState in React, but it can decouple the state management logic from the component and converge it into the Model layer.

INFO

The usage of useLocalModel is exactly the same as useModel.

For example, we modify the code of the counter application and add a counter component LocalCounter with local state:

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>
  );
}

Click the add button of Counter and LocalCounter respectively, and the states of the two do not affect each other:

local-model

The complete sample code can be found here.

Using outside of components

In actual business scenarios, sometimes we need to use Model outside of React components, such as accessing State and executing Actions in utility functions. At this time, we need to use the Store. The Store is a low-level concept that users generally cannot touch. It is responsible for storing and managing the entire application's state. Reduck's Store is based on Redux's Store implementation and adds Reduck-specific APIs, such as use.

First, call useStore in the component to obtain the store object used by the current application and mount it to a variable outside the component:

let store; // Reference to `store` object outside of the component
function setStore(s) {
  store = s;
}
function getStore() {
  return store;
}

function Counter() {
  const [state] = useModel(countModel);
  const store = useStore();
  // Avoid unnecessary duplicate settings through `useMemo`
  useMemo(() => {
    setStore(store);
  }, [store]);

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

You can obtain the Model object through store.use, and the usage of store.use is the same as useModel. Taking the counter application as an example, we perform an increment operation on the counter value every 1 second outside the component tree:

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

The complete sample code can be found here.

INFO

If the Store object is manually created through createStore, there is no need to obtain it through useStore in the component, and it can be used directly.

Additional Information

For detailed API definitions related to this section, please refer to here.