How to fetch Menu from wordpress?

Hi, I have exposed my site menus and now i want to fetch the menu in my front end.
My menu is exposed at http://localhost:8080/wp-json/wp/v2/menu/primary

1 Like

Hey @aeyzazkhan, welcome to the community :smile:

What plugin are you using to expose the menus?

I am not using any plugin but exposing using custom code in theme functions.php

You can create a custom handler to fetch the content of that endpoint and assign that data to a name to access it later, for example "primaryMenu".

Something like:

libraries.source.handlers.push({
  name: "primaryMenu",
  priority: 10,
  // This pattern is the name you can later use in "actions.source.fetch"
  // to fetch the content or "state.source.get" to get the data.
  pattern: "primaryMenu", 
  // This is the function triggered when you use:
  // actions.source.fetch("primaryMenu");
  func: async ({ state, libraries }) => {
    // Fetch endpoint.
    const response = await libraries.source.api.get({
      endpoint: "menu/primary"
    });

    // Extract data from response object.
    const data = await response.json();

    // Assign data to be consumed later.
    // This is the data returned when you use:
    // state.source.get("primaryMenu");
    Object.assign(state.source.data["primaryMenu"], {
      data,
      isMenu: true,
    });
  }
});

You should add that handler in both beforeSSR and beforeCSR:

const before = ({ libraries }) => {
  libraries.source.handlers.push(menuHandler);
};

export default () => ({
  state: { ... },
  actions: {
    beforeSSR: before,
    beforeCSR: before,
  },
});

Finally, fetch it. If you are going to need it on each route, you can fetch it in the before action:

const before = async ({ libraries, actions }) => {
  libraries.source.handlers.push(menuHandler);
  // This will wait until the primaryMenu data is fetched.
  await actions.source.fetch("primaryMenu");
};

That will ensure that the primaryMenu data is available before the React rendering.

You can consume it like this in your React components:

const Menu = ({ state }) => {
  // Use "state.source.get" to access the data:
  const { data } = state.source.get("primaryMenu");

  return (
    <div>{... use `data` somehow ...}</div>
  );
}

export default connect(Menu);
1 Like

By the way, we will add support for WP menus once this plugin is merged with the core:

2 Likes

Hi @luisherranz thanks for your awesome project! I’m just checking this out and playing with the mars and 2019 themes to see if Frontity would fit for my client’s needs. The first thing my client would ask for is if they can change the menu so I’m trying to figure out where to implement this code inside the theme structure to make this possible. I’m using the menu plugin you’ve linked (https://github.com/WP-API/wp-api-menus-widgets-endpoints).

Regards from Tallinn,
Kris

These are our plans for wp-source:

  • Phase 0 (finished): custom handlers support
  • Phase 1 (finished): default handlers for common WP urls (home, categories, tags, posts, pages)
  • Phase 2 (EDIT: finished now!): handlers for other URLs (custom post types, custom taxonomies) and settings for common WP configurations (subdirectory, page as homepage…). These things need to be configured in the frontity.settings.js file.
  • Phase 3 (EDIT: started design phase): handlers for non-URL data, like menus, comments, attachments, widgets…

Our idea for the design of phase 3 is that non-URL data work with the same APIs than the URL data. It just doesn’t start with “/”.

For example, comments will be something like:

// Fetch comments for post 123:
actions.source.fetch("comments/123")
// Get data for those comments
const data = state.source.get("comments/123")
// Iterate over the comments
data.items.map(item => {
  const comment = state.source.comments[item.id]
  // Do stuff
})

Similar for menus, with something like:

// Fetch all menus
actions.source.fetch("menus")
// Fetch specific menu
actions.source.fetch("menus/my-top-menu")

We will open a RFC (request for comments) here in the forum to gather feedback before the final implementation.

In the meantime, you can create your own custom handler to get your menus using any name you want linked to an API endpoint as explained here in this topic. And if you have any question let us know and we’ll help you out :slight_smile:

3 Likes

Thanks for the in-depth explanation! :ok_hand:

1 Like

@luisherranz, I am very interested in this method and tried myself to implement menu on mars theme. But it’s not working. When fetching, it returns null object. I think it is due to the api endpoint.

Can you please let me know how to set endpoint to get menu? For instance, I am going to get all menus and REST API entrypoint is: http://localhost/wp-json/wp/v2/menus/. Then how should I set the endpoint.

I hope to get response from you asap.

Regards,

@luisherranz

My case is that I have bilingual site (czech, english). Default language is czech I have a frontpage handler ( “/” ) and other handlers ("/:slug"). These pages read from state.lang = “cs” and fetch everything according to this setting.

I bumped into a trouble when user goes for routes “/en/” or “/en/:slug”. Where this time handlers change state.lang property to “en”. But my navigation (menu) stays prefetched in czech language.

How can I fetch the menu from WITHIN a component (navigation.js), which is loaded from index.js > header.js ?

This is what I have now: https://ibb.co/cLv37hZ
This is what I would like to have, but it does not work that way:
https://ibb.co/tPPFGwz

@jin.yin1229 What PHP plugin are you using?

@aeyzazkhan, @kris, @afatoga, @jin.yin1229 we have started the design of handlers for non-URL and one of the things we want to add is support for a REST API menu plugin:

Could you please take a look and share with us your impressions?

1 Like

could someone tell me how to implement this?

It really depends on the plugin you use.

I’ll make an example for the one that is supposed to be the one WP will use, but it’d be similar for others.

Btw, we are going to work in a Frontity package for this but it doesn’t mean it’ll be like this example, it is an implementation you could try meanwhile.

  1. First you have to install this plugin (or the one you want) to show the menus on the REST API
    https://github.com/WP-API/menus-endpoints
  2. You have to create a handler to fetch this data pointing to the new endpoints.

For doing so you have to create it at the index.js file of your theme, or create a new package exclusively for this. At this file you should fetch the data and populate the state as you want, something similar to this:

const menuHandler = {
  name: "menus",
  priority: 10,
  pattern: "menus/:id",
  func: async ({ route, params, state, libraries }) => {
    const { api } = libraries.source;
    const { id } = params;

    // 1. fetch the data you want from the endpoint page
    const response = await api.get({
      endpoint: "menu-items",
      params: {
        menus: id,
        per_page: 100 // To make sure we get all elements
      }
    });

    // 2. get an array with each item in json format
    const items = await response.json();

    // 3. add data to source
    const currentPageData = state.source.data[route];

    Object.assign(currentPageData, {
      id,
      items,
      isMenu: true
    });
  }
};

const marsTheme = {
  name: "@frontity/mars-theme",
  roots: { ... },
  state: { ... },
  actions: { ... },
  libraries: {
    ...
    source: {
      handlers: [menuHandler]
    }
  }
};

export default marsTheme;

This way, we have already created our handler. In order to access the data we first have to fetch it using actions.source.fetch("menus/:id"), the state would be populated and we can access it with state.source.get("menus/:id").

This implementation allows you to select by id the menu you want to fetch. For example, if you want to get the data from menu 54, you have to use actions.source.fetch("menus/54") and state.source.get("menus/54").

  1. If you want to fetch a menu for all pages, you can fetch it from your theme too, adding it to actions. This way, the data will be accessible from the beginning. If you want to do so, you have to include it also at index.js file. It should be something like this:
const menuHandler = {
  name: "menus",
  priority: 10,
  pattern: "menus/:id",
  func: async ({ route, params, state, libraries }) => {
    const { api } = libraries.source;
    const { id } = params;

    // 1. fetch the data you want from the endpoint page
    const response = await api.get({
      endpoint: "menu-items",
      params: {
        menus: id,
        per_page: 100 // To make sure you fetch all the elements
      }
    });

    // 2. get an array with each item in json format
    const items = await response.json();

    // 3. add data to source
    const currentPageData = state.source.data[route];

    Object.assign(currentPageData, {
      id,
      items,
      isMenu: true
    });
  }
};

const marsTheme = {
  name: "@frontity/mars-theme",
  roots: { ... },
  state: { ... },
  actions: { 
    ...
    theme: {
      beforeSSR: ({ actions }) => async () => {
        await actions.source.fetch("menus/54");
      }
    }
  },
  libraries: {
    ...
    source: {
      handlers: [menuHandler]
    }
  }
};

export default marsTheme;
  1. Once you have fetched your menu and it’s available in the state, you just have to consume its data as you want. For example, you could adapt the <Nav /> component of mars-theme:
const Nav = ({ state, libraries }) => {
  const { items } = state.source.get("menus/54");

  return (
    <Container>
      {items.map(item => {
        const name = item.title.rendered;
        const link = libraries.source.normalize(item.url);
        return (
          <Item key={name} isSelected={state.router.link === link}>
            <Link link={link}>{name}</Link>
          </Item>
        );
      })}
    </Container>
  );
};

export default connect(Nav);

With all of these, you should have your menu working fetching the data from WordPress. I hope you find this useful and please, let us know if you have any questions :slightly_smiling_face:

5 Likes

The new Nav block will include support to import existing menus. That means the Gutenberg team needs to add the /menus endpoint to the core, so things started to get in motion.

Jonny Harris (spacedmonkey) has taken over the task and he is actively working on the plugin to make it ready. As soon as the final API is frozen and ready to be merged, we will add support for it.

Figured I’d post here instead of creating another thread. I’m using this plugin to expose my menu endpoints and it’s working well but I’ve run into bit of a performance snag while trying to load multiple menus.

On every page of this site we need to retrieve 4 seperate menus (primary/top nav and 3 footer menus), our menu structure looks like this:

{
    "primary": [{
        "name": "Company",
        "items": [{...}]
    },{
        "name": "Contact"
    }],
    "footer_left": [{
        "name": "About"
    },{
        "name": "Contact"
    }],
    "footer_middle": [{
        "name": "Lorem ipsum"
    },{
        "name": "Blah"
    }],
    "footer_right": [{
        "name": "FAQ"
    },{
        "name": "Blah"
    }]
}

I noticed the plugin we’re using can retrieve all menu item’s irrespective of a menu which is fine, but the issue with this approach is; it doesn’t return the menu it actually belongs to so I can no longer determine what menu item’s should appear where.

Currently we’re performing 4 seperate requests to the WP API in order to populate Frontity and render menus, which for each request will obviously add a delay to page load.

Doing 3 seperate requests doesn’t seem like the correct way to do this so I had 2 questions:

  • Is a menu even the right approach for this?
  • Can I retrieve all the menu items (and their children) with a single request with knowledge of what menu they belong to?

Even running both Frontity and WP locally on the same machine, the additional requests add a noticeable load time to the page (although probably more related to my machine).

This will probably be better once some form of caching is in place and the site is live but I wanted to make sure I’m not doing something stupid and putting a bandaid over it.

Hi @chris!

I don’t know if having three menus for this is the best option but I guess it’s a good approach. As far as I know, if you are using the plugin you mentioned, you could add the query _embed=true in your menu endpoint and it will add the menu each item corresponds to, so you can get all the items in just one fetch.

The fetch should be something like https://mysite.com/wp-json/wp/v2/menu-items?_embed=true and you can find the id, name, slug of the menu it belongs to at the _embedded field.

I think that with this you can just make one fetch instead of four and your performance will improve. Note that the API is restricted to 100 items, so if you have more menu-items you may need to make more than 1 fetch.

Please, let us know if this helps! We’d love to know more about what you are building with Frontity :relaxed:

@SantosGuillamot is there a way to get the three menus and its items without fetching all the items?

Maybe something like https://mysite.com/wp-json/wp/v2/menu-items?_embed=true&include=1,5,12 to retrieve menus with IDs 1, 5 and 12.

If that’s not possible maybe we can try to add it before it gets merged with the core.

Apart from that, @chris, make sure you’re doing your fetching in parallel with something like Promise.all.

Thanks @SantosGuillamot I didn’t know about the _embed flag. It looks like that’ll may solve my issue, I’ll have a play with it.

Interested to hear what you think these should be if not menus.

@luisherranz Thanks for the reply. I agree it would be ideal if I could just get a subset of the menu-items like that.

I can’t reveal too much about the project until it’s released, but with the amount of traffic expected to flow through the site we’re trying to optimise things like this as much as possible.

Edit:
Looks like you can retrieve specific menus using:
http://example.com/wp-json/wp/v2/menu-items?menus=1,2,3,4

Which will actually solve my issues, I don’t know how I didn’t find this earlier…

Edit #2:
Spoke too soon :slightly_frowning_face: Looks like there is an issue doing it this way. If the same menu item belongs to more than 1 menu it won’t pull through both menu-items (i.e. About page belongs in the header and footer menu). In addition, appending _embed=true to the request only shows the 1 menu as being associated with the menu-item.

Perhaps this isn’t meant to work this way. I’ll need to look into this a bit more.

Great! Just let us know once it’s finished :relaxed:.

Nice! It seems to be the correct way to do it then

I’ve done a quick test and in my case the response shows both menu-items, with different ids, belonging to two different menus. And each _embedded field shows a different menu associated.

Maybe it is not showing because if you don’t use the query per_page it only shows 10 items. You could try to use per_page=100 to make sure you get all the items you want.