Skip to content

State Management Selectors

Ben edited this page Feb 18, 2024 · 1 revision

Data Store Selector

Selectors are data retrieval modules under the LobeChat data flow development framework. Their role is to extract data from the store using specific business logic for consumption by components.

Taking src/store/plugin/selectors.ts as an example:

This TypeScript code snippet defines an object named pluginSelectors, which contains a series of selector functions used to retrieve data from the plugin storage state. Selectors are functions that extract and derive data from a Redux store (or similar state management library). This specific example is for managing the state related to the frontend application's plugin system.

Here are some key points to note:

  • enabledSchema: A function that returns an array of ChatCompletionFunctions filtered based on the enabled plugin list enabledPlugins. It appends the plugin identifier as a prefix to the API names to ensure uniqueness and uses the uniqBy function from the Lodash library to remove duplicates.
  • onlinePluginStore: Returns the current online plugin list.
  • pluginList: Returns the list of plugins, including custom plugins and standard plugins.
  • getPluginMetaById: Returns the plugin metadata based on the plugin ID.
  • getDevPluginById: Returns information about the custom plugins in development.
  • getPluginManifestById: Returns the plugin manifest based on the plugin ID.
  • getPluginSettingsById: Returns the plugin settings based on the plugin ID.
  • getPluginManifestLoadingStatus: Returns the loading status of the plugin manifest (loading, success, or error) based on the plugin ID.
  • isCustomPlugin: Checks if the plugin with the given ID is a custom plugin.
  • displayPluginList: Returns a processed plugin list, including author, avatar, creation time, description, homepage URL, identifier, and title.
  • hasPluginUI: Determines if the plugin has UI components based on the plugin ID.

Selectors are highly modular and maintainable. By encapsulating complex state selection logic in separate functions, they make the code more concise and intuitive when accessing state data in other parts of the application. Additionally, by using TypeScript, each function can have clear input and output types, which helps improve code reliability and development efficiency.

Taking the displayPluginList method as an example, its code is as follows:

const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.customPluginList];

const displayPluginList = (s: PluginStoreState) =>
  pluginList(s).map((p) => ({
    author: p.author,
    avatar: p.meta?.avatar,
    createAt: p.createAt,
    desc: pluginHelpers.getPluginDesc(p.meta),
    homepage: p.homepage,
    identifier: p.identifier,
    title: pluginHelpers.getPluginTitle(p.meta),
  }));
  • pluginList method: Used to retrieve the list of all plugins from the plugin state storage PluginStoreState. It creates a new plugin list by combining two arrays: pluginList and customPluginList.
  • displayPluginList method: Calls the pluginList method to retrieve the merged plugin list and transforms the title and desc into text displayed on the UI.

In components, the final consumed data can be directly obtained by importing:

import { usePluginStore } from '@/store/plugin';
import { pluginSelectors } from '@/store/plugin/selectors';

const Render = ({ plugins }) => {
  const list = usePluginStore(pluginSelectors.displayPluginList);

  return <> ... </>;
};

The benefits of implementing this approach are:

  1. Decoupling and reusability: By separating selectors from components, we can reuse these selectors across multiple components without rewriting data retrieval logic. This reduces duplicate code, improves development efficiency, and makes the codebase cleaner and easier to maintain.
  2. Performance optimization: Selectors can be used to compute derived data, avoiding redundant calculations in each component. When the state changes, only the selectors dependent on that part of the state will recalculate, reducing unnecessary rendering and computation.
  3. Ease of testing: Selectors are pure functions, relying only on the passed parameters. This means they can be tested in an isolated environment without the need to simulate the entire store or component tree.
  4. Type safety: As LobeChat uses TypeScript, each selector has explicit input and output type definitions. This provides developers with the advantage of auto-completion and compile-time checks, reducing runtime errors.
  5. Maintainability: Selectors centralize the logic for reading state, making it more intuitive to track state changes and management. If the state structure changes, only the relevant selectors need to be updated, rather than searching and replacing in multiple places throughout the codebase.
  6. Composability: Selectors can be composed with other selectors to create more complex selection logic. This pattern allows developers to build a hierarchy of selectors, making state selection more flexible and powerful.
  7. Simplified component logic: Components do not need to know the structure of the state or how to retrieve and compute the required data. Components only need to call selectors to obtain the data needed for rendering, simplifying and clarifying component logic.

With this design, LobeChat developers can focus more on building the user interface and business logic without worrying about the details of data retrieval and processing. This pattern also provides better adaptability and scalability for potential future changes in state structure.

Clone this wiki locally