Exploring React Hooks in SPFx
The journey to learn SharePoint Framework, TypeScript and React has been quite long and will be continuing for quite some time I think.
Some of the courses and resources I have been using include;
I have found that to be effective I really have to put in the time to learn JavaScript before TypeScript and then React. Without those skills under my belt, I found SPFx quite a challenge.
While trying to craft my first SPFx solutions, I realised that using classes, props and state seemed quite inelegant. Trying to work out how to structure the components to do work and get external data seemed a bit unnatural for me, probably because I am more used to C#.
Then I discovered React Hooks. These seem awesome and greatly simplify the code and structure of the solution in my opinion. It removes the need to use classes and we need to worry less about lifecycle methods. Managing state seems to become simple and reusable across the solution.
A disclaimer first; at the time of writing, SharePoint is not yet using React 16.8 which is required for using Hooks. This post from Sarah Otto explains how you can start using hooks as of SPFx 1.8 by changing the version of TypeScript and including React in your bundle.
I set myself a goal of writing a simple proof of concept web part with SPFx, React and Hooks.
The premise of the solution is as follows;
I have a requirement for audience targeted navigation depending on the organisation the user belongs to. For example, if I share a SharePoint site with multiple Office 365 tenants using guest accounts, I want the user to see a list which is tailored to their organisation.
To start with, I create a SharePoint list to host the data that will be used by the web part.
SharePoint list with navigation links
The list uses managed metadata for the Entity column, which did make the data transformation a bit more difficult. The Navigation Group is a heading under which to group the navigation nodes. To simulate the Entity that the user comes from, I used a Choice Group from Office UI Fabric with a list of companies. To display the links, I used the Nav with custom group header from Office UI Fabric.
So the following two screen shots show the output from the web part:
Output when Company A is selected
Output when Company B is selected
First, I’ll explain the project structure;
Source files for the project
For the web part itself, I left that as a class, but this is the only class in the project. It just embeds the top level function component called NavList. All other components are in the FunctionComponent folder.
import * as React from 'react';
import { NavList } from './FunctionComponents/NavList';
import styles from './GroupNavigation.module.scss';
import { IGroupNavigationProps } from './IGroupNavigationProps';
export default class GroupNavigation extends React.Component<IGroupNavigationProps, {}> {
public render(): React.ReactElement<IGroupNavigationProps> {
return (
<div className={styles.groupNavigation}>
<NavList />
</div>
);
}
}
The NavState component is the main custom hook that I am using to manage all state for the web part. I could have used the useState hook directly in the other components if I had wanted, but I preferred to extract it out so it could be used anywhere if I chose and so it could contain the code to call out to SharePoint.
I create state items with ‘useState’ for everything I need, including a flag to indicate if the data is loading, the Entity selected from the Choice Group and the list of Navigation items. I perform the methods to retrieve the data from SharePoint and manipulate/transform the data into the shape that the Nav component needs for display. Yes, I know there are probably more efficient ways to do the transformation…
Then I have the ‘useEffect’ which will run when the component loads, and is also bound to the selectedEntity state. So whenever the selectedEntity changes the loadNavItems method will be called.
import { find } from '@microsoft/sp-lodash-subset';
import { sp } from "@pnp/sp";
import * as React from 'react';
import ISPNavItem, { IGroupLink, INavGroup } from './INavItem';
export default function useNavState(initialEntity: string) {
//Flag to say if the data is loading
const [isLoading, setLoading] = React.useState(false);
//set a default entity
const [selectedEntity, setSelectedEntity] = React.useState(initialEntity);
//init state to empty array for nav items
const [navItemsGroup, setNavItemsGroup] = React.useState<INavGroup[]>([]);
let groupItems: INavGroup[] = [];
let groupItem: INavGroup = null;
let currentGroup: string = "";
//Load the data and filter out items for only the selectede entity
const loadNavItems = () => {
setLoading(true);
sp.web.lists.getByTitle("NavigationList").items.select("Id", "Title", "Entity", "NavigationGroup", "LinkURL", "Target").orderBy("NavigationGroup").get().then((items: ISPNavItem[]) => {
let filteredItems = items.filter(item => {
let includeFlag: boolean = false;
if (find(item.Entity, { Label: selectedEntity })) includeFlag = true;
if (includeFlag) return item;
});
//convert the sharepoint items to the format needed by the office ui fabric Nav control
if (filteredItems.length > 0) {
for (let i = 0; i < filteredItems.length; i++) {
if (filteredItems[i].NavigationGroup !== currentGroup) {
if (currentGroup !== "") {
groupItems.push(groupItem);
}
currentGroup = filteredItems[i].NavigationGroup;
groupItem = { name: filteredItems[i].NavigationGroup, links: [] };
}
let currentLink: IGroupLink = { name: filteredItems[i].Title, url: filteredItems[i].LinkURL, key: filteredItems[i].Id.toString(), target: filteredItems[i].Target };
groupItem.links.push(currentLink);
if (i === filteredItems.length - 1) {
groupItems.push(groupItem);
}
}
}
//Populate the navItems from the SharePoint list
setNavItemsGroup(groupItems);
setLoading(false);
});
};
//When the entity changes, reload the nav items
React.useEffect(() => {
loadNavItems();
}, [selectedEntity]);
return { navItemsGroup, isLoading, selectedEntity, setSelectedEntity };
}
The NavList component isn’t a class and only has three components in it. The Entity Selector, the Spinner from Office UI Fabric and the Nav component, also from Office UI Fabric. Notice that it does import useNavState. The magic happens in the line where I get the state fields and a function to update the state from the useNavState custom hook;
//Get the state data from the custom hook
const {
navItemsGroup,
isLoading,
selectedEntity,
setSelectedEntity
} = useNavState(initialEntity);
import { INavLinkGroup, Nav } from "office-ui-fabric-react/lib/Nav";
import { Spinner, SpinnerSize } from "office-ui-fabric-react/lib/Spinner";
import * as React from "react";
import styles from "../GroupNavigation.module.scss";
import { EntitySelector } from "./EntitySelector";
import useNavState from "./NavState";
const initialEntity = "companya.com";
//This is the main component which will lis the navigation
export const NavList = () => {
//Get the state data from the custom hook
const {
navItemsGroup,
isLoading,
selectedEntity,
setSelectedEntity
} = useNavState(initialEntity);
const _onRenderGroupHeader = (group: INavLinkGroup): JSX.Element => {
return <h3>{group.name}</h3>;
};
return (
<div className={styles.groupNavigation}>
<div className={styles.row}>
<EntitySelector
entity={selectedEntity}
updateEntity={setSelectedEntity}
/>
</div>
<div>{isLoading ? <Spinner size={SpinnerSize.large} /> : ""}</div>
<div>
<Nav
onRenderGroupHeader={_onRenderGroupHeader}
groups={navItemsGroup}
/>
</div>
</div>
);
};
Then last but not least, I have the Entity Selector component. I pass the updateEntity reference in props which actually runs the setSelectedEntity method in the NavState component.
import * as React from 'react';
import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/ChoiceGroup';
export const EntitySelector = (props) => {
const _onChange = (ev: React.FormEvent<HTMLInputElement>, option: any): void => {
props.updateEntity(option.key);
};
return (
<div>
<ChoiceGroup
className="defaultChoiceGroup"
defaultSelectedKey="companya.com"
options={[
{
key: 'companya.com',
text: 'Company A',
'data-automation-id': 'companya'
} as IChoiceGroupOption,
{
key: 'companyb.com',
text: 'Company B'
},
{
key: 'companyc.com',
text: 'Company C'
},
{
key: 'companyd.com',
text: 'Company D'
}
]}
onChange={_onChange}
label="Pick a company to display the navigation for that entity"
required={true}
/>
</div>
);
};
So in summary, all of my code to query SharePoint and the state is extracted into one component. The other components are relatively clean and free from code.
I’ve dropped the project into a gitub repo at https://github.com/ozippy/GroupNav if you would like to look at it.
I’m looking forward to SharePoint Online supporting React 16.8 so that we can make use of hooks natively in SPFx.