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