This article shares the concept and implementation of the selector unit in frontend applications within the Clean Architecture.

Repository with example:
https://github.com/harunou/react-tanstack-react-query-clean-architecture

Selector is a unit that derives values or data structures from the state without modifying it. It implements read-only queries against the state and contains application-specific logic. Selectors do not do any side effects.

The selector unit primarily depends on the entity store for data access. Some selectors may also depend on gateways when additional data is needed, such as fetch states or cached entity data. These gateway dependencies are possible when gateways are implemented using libraries like TanStack React Query or RTK Query.

Selector Implementation

The selector implements interface provided by a consumer. The interface could be just a data type which selector should return or a more complex one, used globally across the application.

The unit has two possible implementation types: inline and extracted. In practice, the unit evolves in the following way:

--------------------      ----------------------
| inline  selector | ---> | extracted selector |
--------------------      ----------------------

Any selector implementation starts from a simple inline function in a consumer.

All development context is focused on the selection logic only.

Inline Selector Implementation

Let's look at a basic example, where we have already view and presenter implemented.

interface OrderProps {
  orderId: string;
}

interface Presenter {
  itemIds: string[];
}

const usePresenter = (params: { orderId: string }): Presenter => {
  return {
    itemIds: ['itemId1', 'itemId2'],
  };
};

export const Order: FC<OrderProps> = (props) => {
  const presenter = usePresenter(props);

  return (
    <div style={{ padding: "5px" }}>
      {presenter.itemIds.map((itemId) => (
        <OrderItem key={itemId} itemId={itemId} />
      ))}
    div>
  );
};

The selector we need should retrieve items of a specific order, filter them based on a filter value, and return an array. The resulting array should be an array of strings, because the presenter indicates that the property itemIds requires a type of string[]. The data type of the output is already defined, so our main concern is to determine where and how to select the data.

Observing the codebase we found that filter values are stored in the store and for the order entity there is already an implemented selector. In this case the required selector implementation can be done following way.

interface OrderProps {
  orderId: string;
}

interface Presenter {
  itemIds: string[];
}

const usePresenter = (params: { orderId: string }): Presenter => {
  const visibleItemsIds = useOrdersPresentationStore((state) => state.itemsFilterById);
  const order = useOrderByIdSelector(props.orderId);
  return {
    // inline selector attached to the itemIds property
    itemIds:
      order?.itemEntities
        .filter((itemId) => visibleItemsIds.includes(itemId))
        .map((itemEntity) => itemEntity.id) ?? [],
  };
};

export const Order: FC<OrderProps> = (props) => {
  const presenter = usePresenter(props);

  return (
    <div style={{ padding: "5px" }}>
      {presenter.itemIds.map((itemId) => (
        <OrderItem key={itemId} itemId={itemId} />
      ))}
    div>
  );
};

Extracted Selector Implementation

Final step is to observe codebase for the need of selector extraction and reuse. The extraction happens if any other consumer unit already has same logic implemented or the selector became more complex. In this case inline selector evolves to extracted selector.

// extracted selector
export const useVisibleItemIdsSelector = (params: { orderId: string }): string[] => {
  const visibleItemsIds = useOrdersPresentationStore((state) => state.itemsFilterById);
  const order = useOrderByIdSelector(props.orderId);
  return (
    order?.itemEntities
      .filter((itemId) => visibleItemsIds.includes(itemId))
      .map((itemEntity) => itemEntity.id) ?? []
  );
};

interface OrderProps {
  orderId: string;
}

interface Presenter {
  itemIds: string[];
}

const usePresenter = (params: { orderId: string }): Presenter => {
  const visibleItemIds = useVisibleItemIdsSelector(props.orderId);
  return {
    itemIds: visibleItemIds,
  };
};

export const Order: FC<OrderProps> = (props) => {
  const presenter = usePresenter(props);

  return (
    <div style={{ padding: "5px" }}>
      {presenter.itemIds.map((itemId) => (
        <OrderItem key={itemId} itemId={itemId} />
      ))}
    div>
  );
};

Another approach to extracting selector is to split inline selector into extracted selector and presenter mapper.

// extracted selector
export const useVisibleItemsSelector = (params: { orderId: string }): ItemEntity[] => {
  const visibleItemsIds = useOrdersPresentationStore((state) => state.itemsFilterById);
  const order = useOrderByIdSelector(props.orderId);
  return order?.itemEntities.filter((itemId) => visibleItemsIds.includes(itemId));
};

interface OrderProps {
  orderId: string;
}

interface Presenter {
  itemIds: string[];
}

const usePresenter = (params: { orderId: string }): Presenter => {
  const visibleItemEntities = useVisibleItemsSelector(props.orderId);
  return {
    itemIds: visibleItemEntities.map(toItemEntityId);
  };
};

// presenter mapper
const toItemEntityId = (item: ItemEntity): string => item.id

export const Order: FC<OrderProps> = (props) => {
  const presenter = usePresenter(props);

  return (
    <div style={{ padding: "5px" }}>
      {presenter.itemIds.map((itemId) => (
        <OrderItem key={itemId} itemId={itemId} />
      ))}
    div>
  );
};

The approach of selector extraction should reflect current selector usage needs. Naming of extracted selector is suggested to be based on the data it extracts followed by the suffix Selector.

At this stage the selector unit implementation is considered complete.

Q&A

How to test the selector?

Selector units can be tested in integration with other units they depend on, in isolation by mocking dependencies or by extracting core logic.

Example of integration test can be found in useTotalItemsQuantitySelector.spec.tsx.

Let's look at the example with core logic extraction.

The selector has inline select (core) logic.

// extracted selector
export const useVisibleItemIdsSelector = (params: { orderId: string }): string[] => {
  const visibleItemsIds = useOrdersPresentationStore((state) => state.itemsFilterById);
  const order = useOrderByIdSelector(props.orderId);
  return (
    // select (core) logic
    order?.itemEntities
      .filter((itemId) => visibleItemsIds.includes(itemId))
      .map((itemEntity) => itemEntity.id) ?? []
  );
};

Once the select logic is extracted, the selector will look like this.

// extracted selector
export const useVisibleItemIdsSelector = (params: { orderId: string }): string[] => {
  const visibleItemsIds = useOrdersPresentationStore((state) => state.itemsFilterById);
  const order = useOrderByIdSelector(props.orderId);
  return select(order, visibleItemsIds);
};

// extracted select (core) logic
export const select = (order: OrderEntity | undefined, visibleItemsIds: string[]): string[] => {
  return (
    order?.itemEntities
      .filter((itemId) => visibleItemsIds.includes(itemId))
      .map((itemEntity) => itemEntity.id) ?? []
  );
};

The select is a pure function, which can be pretty easily tested with just mock data passed as arguments.

Where to place it?

Selectors are suggested to be placed in a selectors directory.

What is the selector interface and do I need a specific one?

The minimal requirement is that a consumer specifies the type for the data it needs, and the selector returns that specific data type. In more complex cases, an application might define a global selector interface that both consumers and selectors should use.

The example below demonstrates a simple selector. The return type of the selector is determined by the type annotation of the itemsIds property in the presenter interface.

interface Presenter {
  // Presenter interface defines type of `itemsIds`
  itemIds: string[];
}

// The selector is implemented to return the type requested by the presenter
const useVisibleItemIdsSelector = (params: { orderId: string }): string[] => {
  const visibleItemsIds = useOrdersPresentationStore((state) => state.itemsFilterById);
  const order = useOrderByIdSelector(props.orderId);
  return (
    order?.itemEntities
      .filter((itemId) => visibleItemsIds.includes(itemId))
      .map((itemEntity) => itemEntity.id) ?? []
  );
};

// Now the selector is used to the presenter implementation
const usePresenter = (params: { orderId: string }): Presenter => {
  const itemIds = useVisibleItemIdsSelector(props.orderId);
  return {
    itemIds
  };
};

Next example shows the same selector implementation, but with the use of global Selector interface definition withing an application. This approach is not necessary, but it can be useful in some cases, e.g., dependency injection, providing options to select operation in specific contexts.

// Global selector type
type Selector<R, T extends unknown[] = []> = {
  select: (...params: T) => R;
};

interface Presenter {
  // Presenter interface defines type of `itemsIds`
  itemIds: string[];
}

// Selector implementation, which requires injection of `visibleItemIdsSelector`
// and `orderByIdSelector`
const useVisibleItemIdsByOrderIdSelector = (params: {
  visibleItemIdsSelector: Selector<string[]>;
  orderbyIdSelector: Selector<OrderEntity>;
}): Selector<string[]> => {
  return {
    select: () => {
      const visibleItemsIds = params.visibleItemIdsSelector.select();
      const order = params.orderByIdSelector.select();
      return (
        order?.itemEntities
          .filter((itemId) => visibleItemsIds.includes(itemId))
          .map((itemEntity) => itemEntity.id) ?? []
      );
    },
  };
};

// Presenter implementation, which requires injection of `visibleItemIdsSelector`
const usePresenter = (params: {
  visibleItemIdsByOrderIdSelector: Selector<string[]>;
}): Presenter => {
  return {
    itemIds: params.visibleItemIdsByOrderIdSelector.select(),
  };
};

Conclusion

Selector is essential unit in Clean Architecture for frontend applications. It provides a consistent and maintainable way to derive data from the application state. By using selectors, developers can keep the state clean and effectively separate application logic from enterprise logic.