Add results of fetch to global state

I’m finding myself reusing a hook to fetch the same data a lot, which looks like this:

import { useEffect, useState } from 'react';
import to from 'await-to-js';
import { useConnect } from 'frontity';

//

/**
 *
 * @param {number} id The ID of the post you want to fetch
 * @param {number} category The ID of the category you want to fetch
 * @returns A list of posts
 */

const useFetchPosts = (id, category) => {
  const [post, setPost] = useState(null);
  const { libraries } = useConnect();

  useEffect(() => {
    async function getPostById() {
      const { api } = libraries.source;
      let err;
      let res;
      let resData;

      [err, res] = await to(
        api.get({
          endpoint: id ? `posts/${id}` : 'posts',
          params: { _embed: true, categories: category },
        })
      );
      if (!res) console.error(err);

      [err, resData] = await to(res.json());
      if (!resData) console.error(err);

      setPost(resData);
    }

    getPostById();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return post;
};

export default useFetchPosts;

Then at component level I find myself using this a lot:

const allSamples = useFetchPosts(null, 12); // '12' is the id of a page containing data (samples) I need

I’m thinking at this point it makes more sense to add it to the global state, in my index file of my theme package (with roots etc) but I’m not sure how to do this.

Can anyone advise how to take the above hook and fetch the data I need in state rather than in each component? That way I can just access it directly from state

Hi @modg

I think source.libraries.state.populate() could be what you need.

Thanks @mburridge — where would you call this from?

Hi @modg

You would call this once you have the data returned from the get call, so I guess you could call it either in your useFetchPosts component, or in the component that receives the post returned by useFetchPosts.

Here’s another possible way that you could approach this.

I liked the sound of the second approach as this feels like a ā€˜proper’ way of handling this, but I’m hitting a few bumps in the road:

I have this:

const samplesHandler = {
  name: 'allSamples',
  priority: 10,
  pattern: '/samples/:slug',
  func: async ({ link, state, libraries, force }) => {
    console.info(link);

    // 1. get sample
    const response = await libraries.source.api.get({
      endpoint: 'posts',
      params: { _embed: true, categories: 12 },
    });

    // 2. add sample to state
    const [allSamples] = await libraries.source.populate({
      response,
      state,
      force,
    });

    // 3. add link to data
    Object.assign(state.source.data[link], {
      id: allSamples.id,
      type: allSamples.type,
      isPostType: true,
      isSample: true,
    });
  },
};

export default samplesHandler;

and I added this to handlers in my components/index.js file… but if I console.log(state) from one of my pages I’m not seeing the data I’d expect to see.

Any ideas what steps I might be missing here?

Change the pattern to "@samples/:slug", otherwise wp-source handles it as a regular URL and tries to get page which doesn’t exist.

And also check frontity-examples/index.js at dcb60f4076e81dd0d69bae592ce3ade137b2840a Ā· frontity-demos/frontity-examples Ā· GitHub and
frontity-examples/index.js at dcb60f4076e81dd0d69bae592ce3ade137b2840a Ā· frontity-demos/frontity-examples Ā· GitHub because you still need to call the handler in one way or another :wink:

Thanks @Johan , apologies for the delay, I only get to work on this a couple of days a week so have to keep jumping in and out.

I changed the pattern to @samples/:slug as you suggested, and I also added this snippet as in your example:

      beforeSSR: async ({ actions }) => {
        await actions.source.fetch(`/category/samples`);
      },

I’m not really sure how all of this connects together though. I’ve got a handler running the function above and it’s set like so:

    source: {
      handlers: [samplesHandler],
    },

the handler should be working as it works as a hook, but I’m still not seeing any data in state (just an empty array from allSamples)

You can see my code here:

Specifically: Marton-Mills/index.js at main Ā· Modular-Everything/Marton-Mills Ā· GitHub
and: Marton-Mills/samplesHandler.js at main Ā· Modular-Everything/Marton-Mills Ā· GitHub

If your handler pattern is @samples/:slug, it means it expects that as the ā€œurlā€ for the content you want to fetch and get.

So

beforeSSR: async ({ actions }) => {
   // this will preload the posts with category 'some-category'
   // and put them in `state.source.data['@samples/some-category']`
   await actions.source.fetch('@samples/some-category');
},

and in your component

const somePosts = state.source.get('@samples/some-category'); // <- The data will exist

Alternatively, if you don’t want to preload all posts at all times:

const SomePost = ({ actions, state }) => {
  useEffect(() => {
    // No need to use `async/await` here
    actions.source.fetch('@samples/some-category);
  }, []);

  // The data will not exist at first, `dataPost.isReady` will be false.
  // But then, it will rerender when `actions.source.fetch` is finished.
  const somePosts = state.source.get('@samples/some-category');

  // This will work just fine.
  return somePosts.isReady ? <Post link="/some-post" /> : <Loading />;
};
3 Likes

Thanks for the precise and helpful explanation, @Johan. You’re a credit to the community!

This helped me get it over the line, although in the end I handled it like this:

const samplesHandler = {
  name: 'allSamples',
  priority: 10,
  pattern: '@samples/:slug',
  func: async ({ link, state, libraries }) => {
    // 1. get sample
    const response = await libraries.source.api.get({
      endpoint: 'posts',
      params: { _embed: true, categories: 12 },
    });

    // 2. parse JSON
    const sampleData = await response.json();

    // 3. add link to data
    const sample = state.source.data[link];
    Object.assign(sample, { items: sampleData });
  },
};

export default samplesHandler;

And then in my components

  const allSamples = state.source.get('@samples/samples');
  if (allSamples.isFetching) return <Loading />;

I have access to all of the returned samples in allSamples.items this way. It works perfectly and the site speed has rocketed having to only fetch this data once.

I’m not sure what’s going to happen when I have a load of samples to grab, but that’s the next bridge to cross…

Thanks again