6. Namespaces

This "Learning Frontity" guide is intended to be read in order so please start from the first section if you haven't done so already.

Let's talk now about namespaces and how we, as a community, can use them to extend Frontity and create a better tool for everyone.

Using namespaces in package exports

As we've already seen, this could be a typical theme package:

/packages/my-awesome-theme/src/index.js
import Theme from "./components";
โ€‹
export default {
roots: {
theme: Theme
},
state: {
theme: {
menu: [["Home", "/"], ["About", "/about"]],
isMenuOpen: false,
featuredImage: {
showOnList: false,
showOnPost: false
}
}
},
actions: {
theme: {
toggleMenu: ({ state }) => {
state.theme.isMenuOpen = !state.theme.isMenuOpen;
}
}
}
}

One thing you can notice is that roots, state, and actions have a namespace called theme. It seems like it is not adding much value because it is the only namespace. Then, why not write it like this instead?

/packages/my-awesome-theme/src/index.js
import Theme from "./components";
โ€‹
export default {
namespace: "theme",
roots: Theme,
state: {
menu: [["Home", "/"], ["About", "/about"]],
isMenuOpen: false,
featuredImage: {
showOnList: false,
showOnPost: false
}
},
actions: {
toggleMenu: ({ state }) => {
state.theme.isMenuOpen = !state.theme.isMenuOpen;
}
}
}

Several reasons:

1. It's easier to be aware of the final structure

When you access state or actions, it's much easier to see what you need when you write it like this:

state: {
theme: {
isMenuOpen: false,
}
},
actions: {
theme: {
toggleMenu: ({ state }) => {
state.theme.isMenuOpen = !state.theme.isMenuOpen; // <- Easy, right?
}
}
}

and not like this:

state: {
isMenuOpen: false,
},
actions: {
toggleMenu: ({ state }) => {
state.isMenuOpen = !state.isMenuOpen; // <- Be aware, this is wrong!!
}
}

2. It's easier for TypeScript

Even tho TypeScript is optional in Frontity, we make sure it has excellent support in case you want to use it. TypeScript gets really complex when you try to modify the structure of your objects, and in order to make it as simple as possible, it's good to create objects with the same structure that they will be consumed later. So yes, TypeScript just works :)

3. Multiple namespaces per package

Packages can export multiple namespaces and that's good. It makes Frontity more flexible.

For example, imagine we want to create a theme that implements its own share:

/packages/my-awesome-theme-with-share/src/index.js
import Theme from "./components/theme";
import Share from "./components/share";
โ€‹
export default {
roots: {
theme: Theme,
share: Share
},
state: {
theme: {
... // State for the theme
},
share: {
... // State for the share
}
},
actions: {
theme: {
... // Actions for the theme
},
share: {
... // Actions for the share
}
}
}

Making Frontity extensible through namespaces

This is the main reason namespaces exist in Frontity, and a big part of how Frontity itself works.

We use namespaces to create abstractions on top of packages and so they can communicate between each other without really knowing the specific implementation.

It's easier to understand with some examples.

Example: comments

Imagine a Frontity package for WordPress native comments that exports a Comment in its libraries. It is called wp-comments but its namespaces is comments. It may be something like this:

/packages/wp-comments/src/index.js
import Comment from "./components/Comment";
โ€‹
export default {
libraries: {
comments: {
Comment
}
}
};

Now, all the theme packages that want to include a comments section, can take a look and check if there is a comments package installed. If it is, they can include its React component after the Post content.

/packages/my-awesome-theme/src/components/Post.js
const Post = ({ state, actions, libraries }) => {
const data = state.source.get(state.router.link);
const post = state.source.post[data.id];
// Check if libraries.comments exist and if it does get the Comment component.
const Comment = libraries.comments ? libraries.comments.Comment : null;
return (
<Container>
<Title title={post.title.rendered} />
<Content html={post.content.rendered} />
<Comment /> // <- Insert the Comment component in its place.
</Container>
);
};
โ€‹
export default connect(Post);

Final users can use their frontity.settings.js to install and configure wp-comments:

frontity.settings.js
export default {
packages: [
"my-awesome-theme",
"@frontity/tiny-router",
"@frontity/wp-source",
"@frontity/wp-comments" // <- That's it. You have native wp comments now.
]
}

But what if (and now it is when this become interesting) some user doesn't want to use WordPress native comments but Disqus comments?

Then he/she just have to install disqus-comments instead:

frontity.settings.js
export default {
packages: [
"my-awesome-theme",
"@frontity/tiny-router",
"@frontity/wp-source",
"@frontity/disqus-comments" // <- That's it again. You have disqus now.
]
}

The disqus-comments package also exports a Comment component in libraries.comments.Comment so the theme inserts that instead. Actually, the theme has no idea about what specific implementation of comments you have installed.

Example: analytics

Let's see another example: two actions in the analytics namespace. All the packages that want to implement analytics need to have these two actions:

  • actions.analytics.sendPageview: send a pageview to the analytics service.

  • actions.analytics.sendEvent: send an event to the analytics service.

The first one, actions.analytics.sendPageview, is used by packages that implement router, each time actions.router.set is used.

The second one, actions.analytics.sendEvent, is used by the theme when something interesting happens. For example:

Post.js
const Post = ({ actions }) => (
<Post>
<Title />
<Content />
<ShareButtons onClick={actions.theme.openShareModal} />
</Container>
);
โ€‹
export default connect(Post);
/packages/theme/src/index.js
export default {
state: {
theme: {
shareOpen: false
}
},
actions: {
theme: {
openShareModal: ({ state, actions }) => {
state.theme.shareOpen = true;
actions.analytics.sendEvent("share-modal-open");
}
}
}
};

When users open the share modal, a new event is sent to the analytics service installed.