Question on getVocabularyTokenTitle action/reducer

My use case is that I have a JSONField/mixedfield schema 13. Content types: Reference – Mastering Plone 6 development — Plone Training 2025 documentation with one of the subfields being a select field with a vocabulary. I noticed that if I have a non-mixedfield select field my component is passed a value that includes both the token and the title. But in the case of mixedfields I am only given the token value.

To get the title for my tokens I am attempting to use the getVocabularyTokenTitle action/reducer as shown below. I will also preface this by saying I am new to Redux so it is possible I have some misconceptions. I used this example 33.3. Volto Actions and component state [voting story] – Mastering Plone 6 development – <span class="section-number">33. </span>Roundtrip [The voting story] frontend, backend, and REST — Plone Training 2025 documentation as a reference.

const ContactPhones = ({ phones }) => {
    // JSONField select items do not come as an Object with title/token so we need to query @vocabulary
    const dispatch = useDispatch(),
        phoneTypeVocabName = 'ims.contacts.phone_types',
        phoneTypes = useSelector((state) => state.vocabularies || {});

    useEffect(() => {
        let tokens = [];
        phones.items.forEach(phone => {
            tokens.push(phone.type);
        })
        dispatch(getVocabularyTokenTitle({ vocabNameOrURL: phoneTypeVocabName, tokens: tokens }));

    }, [dispatch]);
    console.log(phoneTypes)

    const parsedPhones = phones.items.map(ph => {
        return `${phoneTypes?.[phoneTypeVocabName]?.[ph.type] || ph.type}: ${ph.number}`
    })

    return (
        <List id="phones" celled items={parsedPhones} />
    )
}

In the above component I am attempting to use the "tokens" parameter to pass two tokens. This doesn't work, and I'm not entirely sure if it should. The function has a "tokens" parameter but it's not in the docstring. Here's the console log:

If I pass in a single token with the "token" parameter it works. The console log is below. But surely I wouldn't be expected to make a separate XHR for each phone type I've added?

Is there no easy way to get what I want, or am I doing something off?

Inspect the store for vocabularies.
If your vocabulary is present, you have access with useSelect.
If not, post again for details how to make it available.
No custom action/reducer necessary.

It's not present, that is why I am using the getVocabularyTokenTitle action above. I do not have any custom actions/reducers, this is one that comes from volto core. What I was hoping to be in the store is mapping vocab tokens to titles. I am able to get this if I pass in a single token, but not multiple.

In useEffect: getVocabulary(„myvocab“) to make the vocab available in the global store.

After everything is running as expected, check when and how offen the request is done and If this needs more attention than just firing in usEffect of the component to be efficient, not only effective.

I'm sorry, I don't think that answers me question. I was hoping to use getVocabularyTokenTitle I'm just not sure it works the way I want it to. getVocabulary returns the entire vocabulary - this is not ideal because it requires writing a mapping to convert the token to a title. IMO that should be handled by a REST API end point. getVocabularyTokenTitle seems like it is almost there, but not quite.

The trick is to load the store once with the vocabulary and read it for multiple tokens.
The store is global, access it in whatever component, this or the next page.

Comming from Plone Classic it took me days to get the pattern of the global store present.
As this is essential, I was curious how it would look like to enhance the training example 13. Content types: Reference – Mastering Plone 6 development — Plone Training 2025 documentation with a field, constrained by vocabulary.
Here is a draft that works. Hope you can translate it to your needs.


import React from 'react';
import moment from 'moment';
import { Container, Table } from 'semantic-ui-react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { getVocabulary } from '@plone/volto/actions/vocabularies/vocabularies';
import './MixedFieldContent.css';

interface VocabularyTerm {
  value: string;
  title: string;
}

interface HistoryItem {
  historydate?: string;
  historytopic?: string;
  historyversion?: string;
  historyauthor?: string;
  historyproject?: string;
}

interface MyHistoryProps {
  history?: {
    items?: HistoryItem[];
  };
}

interface MixedFieldContentViewProps {
  content: {
    title?: string;
    description?: string;
    history_field?: {
      items?: HistoryItem[];
    };
  };
}

const useProjectLabels = () => {
  const vocabulary = 'rohberg.mixedfield.Projects';
  const dispatch = useDispatch();

  React.useEffect(() => {
    dispatch(getVocabulary({ vocabNameOrURL: vocabulary }));
  }, [dispatch, vocabulary]);

  const terms = useSelector(
    (state: any) =>
      state.vocabularies[vocabulary] && state.vocabularies[vocabulary].items
        ? state.vocabularies[vocabulary].items
        : [],
    shallowEqual,
  );

  const getProjectLabel = (value: string) => {
    if (terms) {
      
      const term = terms.find((term: VocabularyTerm) => term.value === value);
      if (term) {
        return term.label;
      }
    }
    return "";
  };

  return { getProjectLabel };
};

const MyHistory: React.FC<MyHistoryProps> = ({ history }) => {
  const { getProjectLabel } = useProjectLabels();

  return (
    <Table celled className="history_list">
      <Table.Header>
        <Table.Row>
          <Table.HeaderCell>Date</Table.HeaderCell>
          <Table.HeaderCell>What</Table.HeaderCell>
          <Table.HeaderCell>Version</Table.HeaderCell>
          <Table.HeaderCell>Who</Table.HeaderCell>
          <Table.HeaderCell>Project</Table.HeaderCell>
        </Table.Row>
      </Table.Header>

      <Table.Body>
        {history?.items?.map((item, index) => (
          <Table.Row key={index}>
            <Table.Cell>
              {item.historydate && moment(item.historydate).format('L')}
            </Table.Cell>
            <Table.Cell>{item.historytopic}</Table.Cell>
            <Table.Cell>{item.historyversion}</Table.Cell>
            <Table.Cell>{item.historyauthor}</Table.Cell>
            <Table.Cell>
              {item.historyproject ? getProjectLabel(item.historyproject) : ''}
            </Table.Cell>
          </Table.Row>
        ))}
      </Table.Body>
    </Table>
  );
};

const MixedFieldContentView: React.FC<MixedFieldContentViewProps> = ({ content }) => {
  return (
    <Container>
      <h2>Mixed Field Content</h2>
      {content.description && <p>{content.description}</p>}
      
      <h3>History</h3>
      <MyHistory history={content.history_field} />
    </Container>
  );
};

export default MixedFieldContentView;

#datagrid vocabulary

Thank you, that all makes sense. That is roughly what I ended up doing with the getVocabulary action. I'm still curious what the intent of the getVocabularyTokenTitles action is though...

Follow up question, this is really about Redux more than Plone I think. Given my below component, if I have multiple contacts that each use I get an XHR for each use. I only want to call @vocabulary once and have it update state and then just access that state. I think this is the point of dispatch/reducers, but I'm doing something wrong?

export const ContactPhones = ({ phones }) => {
    const dispatch = useDispatch(),
        phoneTypeVocabName = 'ims.contacts.phone_types',
        vocab = useSelector(
            (state) => state.vocabularies?.[phoneTypeVocabName] || {},
        );

    useEffect(() => {
        dispatch(getVocabulary({ vocabNameOrURL: phoneTypeVocabName }));
    }, [dispatch]);
    let phoneMap = {};

    if (vocab?.items?.length) {
        vocab.items.forEach((phone) => {
            phoneMap[phone.value] = phone.label;
        });
    }

    const parsedPhones = phones.items.map((ph) => {
        return `${phoneMap?.[ph.type] || ph.type}: ${ph.number}`;
    });

    return <List id="phones" celled items={parsedPhones} />;
};

It looks like you have one component XY that renders multiple components Contact, each rendering one ContactPhones component. Now you want to dispatch getVocabulary only once per XY component as the call of getVocabulary is the same (same parameters) for all.
Dispatching getVocabulary in useEffect of XY would do this.

1 Like

Another option is to call getVocabulary({ vocabNameOrURL: phoneTypeVocabName }) once per app.

I see, so I had a misconception about dispatches work. I moved the phoneMap building to a custom hook and then called that on the parent component, and passed it down as a prop.