5. State

The next thing we should look at is the state.

We have defined it previously as "A JavaScript object containing all the state exposed by your package". For example:

/packages/my-awesome-theme/src/index.js
export default {
  state: {
    theme: {
      menu: [
        ["Home", "/"],
        ["About", "/about"],
      ],
      featuredImage: {
        showOnList: true,
        showOnPost: false,
      },
      isMenuOpen: false,
    },
  },
};

As you can see here, this theme needs some settings like the menu or settings to define if it should show featured images or not, and then some state that is useful while the app is running, like isMenuOpen.

You can access the state in the client console with:

> frontity.state

State is a proxy, so you can see the original object clicking on [[Target]] :

Why not separate settings and state?

First, here at Frontity we think the less concepts the better. Second, imagine a notifications package wants to add an item to the menu only when the browser actually supports notifications. That's super easy to do by just using the state:

/packages/my-notifications-package/src/index.js
export default {
  actions: {
    notifications: {
      init: ({ state }) => {
        // Only run this in the browser:
        if (state.frontity.platform === "client") {
          // Only add item to the menu if browser support notifications:
          if ("Notification" in window) {
            state.theme.menu.push(["Notifications", "/notification-settings"]);
          }
        }
      }
    }
  }

As you can see, packages can access the state exposed by other packages.

Finally, what if you decide that the app should be run with the menu open by default? Then you'd only have to set isMenuOpen to true in your frontity.settings.js file. Yes, I know, that makes no sense, but I hope it gives you a sense of how flexible this pattern is.

Another good example of state is tiny-router. It exposes three props:

/packages/tiny-router/src/index.js
export default {
  state: {
    router: {
      link: "/",
      autoFetch: true,
    },
  },
};

Here link represents the current URL of your app and it changes when you use the action actions.router.set("/other-url")in your theme.

If we were to create an analytics package, we could use state.router.link when sending pageviews:

/packages/my-analytics-package/src/index.js
export default {
  actions: {
    analytics: {
      sendPageView: ({ state }) => {
        ga('send', {
          hitType: 'pageview',
          page: state.router.link
        });
      }
    }
  }

Finally, tiny-router exposes a third prop called autoFetch. This is a setting and, by default, is true. If it's active, it fetches the data you need each time you navigate to a new route using: actions.router.set(link).

Here the most common scenario is that you will use your frontity.settings.js file to set autoFetch to false when you want to control the fetching yourself:

frontity.settings.js
export default {
  packages: [
    ...,
    {
      name: "@frontity/tiny-router",
      state: {
        router: {
          autoFetch: false,
        },
      },
    },
  ],
};

These are the most important things you need to know about the Frontity state:

1. State should be serializable

Only objects, arrays and primitives (strings, numbers...) are allowed in the state because it must be serializable. No circular dependencies are allowed either. The best way to think about it is: it's a JSON.

Actually, it is converted to a JSON when it's sent to the client. We'll talk later about how server-side Rendering works, but it is something like this:

First, this is what Frontity does in the server:

  1. It gets the settings of the current site from frontity.settings.js.

  2. It merges the state exposed by each package with the state from frontity.settings.js.

  3. It gives each package the opportunity to populate state with an async beforeSSR action. SSR stands for server-side Rendering. This is usually used to fetch content from the WP REST API.

  4. It renders React using that initial state.

  5. It sends both the HTML generated by React and the initial state to the client.

The client browser paints the HTML received from the server. Then, this is what Frontity does once the JavaScript is run:

  1. It loads the state in the client using the initial state received from the server. This guarantees that when we render React again we will be in the very same place where we left on the server.

  2. It renders React again. It should produce the very same HTML we've sent from the server.

  3. It gives each package the opportunity to run code with an afterCSR action. CSR stands for client-side Rendering.

2. All the state is merged together

As we've seen in the previous point, the states from frontity.settings.js and your packages are merged together.

Let's imagine we have this setting file:

frontity.settings.js
export default {
  state: {
    frontity: {
      url: "https://my-site.com",
    },
  },
  packages: [
    "@frontity/wp-source",
    {
      name: "@frontity/my-awesome-theme",
      state: {
        theme: {
          featuredImage: {
            showOnList: true,
          },
        },
      },
    },
    {
      name: "@frontity/tiny-router",
      state: {
        router: {
          autoFetch: false,
        },
      },
    },
  ],
};

First, the states from my-awesome-theme, tiny-router and wp-source get merged:

state: {
  theme: {
    isMenuOpen: false,
    featuredImage: {
      showOnList: false,
      showOnPost: false
    }
  },
  router: {
    link: "/",
    autoFetch: true,
  },
  source: {
    data: {},
    post: {},
    ... // source contains more objects for categories, tags, pages...
  }
}

Then, the state from frontity.settings.js file gets merged:

state: {
  frontity: {
    url: "https://my-site.com", // <- this was added in frontity.settings.js
  },
  theme: {
    isMenuOpen: false,
    featuredImage: {
      showOnList: true, // <- this was modified in frontity.settings.js
      showOnPost: false
    }
  },
  router: {
    link: "/",
    autoFetch: false, // <- this was modified in frontity.settings.js
  },
  source: {
    data: {},
    post: {},
    ...
  }
}

Then Frontity executes beforeSSR to give each package the opportunity to modify the state. For example, the theme could use it to fetch content from the REST API:

/packages/my-awesome-theme/src/index.js
actions: {
  theme: {
    beforeSSR: async ({ state, actions }) => {
      await actions.source.fetch(state.router.link);
    };
  }
}

This populates source with some data. For example, if the URL is /my-post:

state: {
  ...,
  source: {
    data: {
      "/my-post": {
        type: "post",
        id: 123,
        isPost: true
      }
    },
    post: {
      123: {
        id: 60,
        date: "2016-11-25T18:31:11",
        title: "..."
        content: "..."
        ...
      }
    },
    ...
  }
}

Now everything is ready for the React render in the server!

3. State should be minimal

There are two reasons for this:

  1. The initial state is sent to the client, so the smaller the better.

  2. It's easier to cause out-of-sync bugs when the state exists in two different places.

For that reason, Frontity supports derived state.

Remember I told you that state must be serializable and cannot contain functions? Well, that's still technically true, but you can include derived state functions. Let's take a look at an example:

state: {
  share: {
    data: {
      "/my-first-post": {
        "facebook": 15,
        "twitter": 12,
      },
      "/my-second-post": {
        "facebook": 25,
        "twitter": 32,
      }
    },
    totalCount: 84
    ...
  }
}

Here we have a totalCount field that represents the sum of all the shares we have in our posts. It looks great, but what happens if we update the shares of our second post?

state: {
  share: {
    data: {
      "/my-first-post": {
        "facebook": 15,
        "twitter": 12,
      },
      "/my-second-post": {
        "facebook": 43,
        "twitter": 64,
      }
    },
    totalCount: 84 // <- now totalCount is out of sync
    ...
  }
}

Wouldn't it be much easier if totalCount could be calculated reactively each time their dependencies change? That's precisely what derived state is for:

state: {
  share: {
    data: {
      "/my-first-post": {
        "facebook": 15,
        "twitter": 12,
      },
      "/my-second-post": {
        "facebook": 43,
        "twitter": 64,
      }
    },
    totalCount: ({ state }) => {
      let totalCount = 0;
      const shareData = Object.values(state.share.data);
      for (let i = 0; i < shareData.length; i +=1) {
        totalCount += shareData[i].facebook;
        totalCount += shareData[i].twitter;
      }
      return totalCount;
    }
    ...
  }
}

That's it! Now when you use state.share.totalCount in React everything will be updated without having to do anything additional on your end.

You can also use derived state with additional custom parameters. Such a function works like a "getter" for a piece of state:

state: {
  share: {
    data: {
      "/my-first-post": {
        "facebook": 15,
        "twitter": 12,
      },
      "/my-second-post": {
        "facebook": 43,
        "twitter": 64,
      }
    },
    totalCountByRoute: ({ state }) => route => {
      let totalCount = 0;
      totalCount += state.share.data[route].facebook;
      totalCount += state.share.data[route].twitter;
      return totalCount;
    }
    ...
  }
}

And then consumed like this: state.share.totalCountByRoute("/my-first-post"), so you should be able to create derived state for pretty much anything.

Additionally, Frontity gives you access to both state as well as [libraries]('./libraries) in your derived state:

state: {
  share: {
    data: {
      "/my-first-post": {
        "facebook": 15,
        "twitter": 12,
      },
      ...
    },
    // `html2react.processors` comes from the @frontity/html2react package.
    processorsCount: ({ state, libraries }) => {
      return libraries.html2react.processors.length;
    };
  }
}

These derived state functions are stripped out from the initial state we send to the client but don't worry, they are reinstantiated later in the client by Frontity to ensure everything is back to normal :)

If you still have any questions about State in Frontity, please check out the community forum, which is packed full of answers and solutions to all sorts of Frontity questions. If you don't find what you're looking for, feel free to start a new post.

Last updated