Introduction: Function Components and Hooks Background

In the past year or so, the React community has started moving towards Function Components and Hooks and away from Classes and High Order Components.  The SharePoint community has also started to move in this direction. I felt that this is a place where I could contribute, by writing a Function Components and Hooks architecture tutorial.

While there have been plenty of walkthroughs and examples of basic hooks, particularly for useState and useEffect, they tend to be somewhat basic, like watching a counter tick up or down.   I’ve found it difficult to get solid pattern examples for things that would be more common to the typical SharePoint Developer.  I imagine most web parts are doing some kind of API call. Most are probably doing far more than one and also need to track the state of these calls.

So I set out to build something that could showcase the building blocks of React Hooks. I also wanted to grapple with the context of building Hooks with TypeScript. Lastly, I wanted to showcase more typical scenarios like working with APIs and the states one normally wants to track within that.  I won’t say that I’ve come up with the perfect pattern. But hopefully, you will take something away from this that will influence your own development patterns.

Setting and Getting State plus Side-Effects…Encapsulate it All!

Most things we build are going to interface with one or many API endpoints throughout its lifecycle.  This implies a number of states that we will want to track.  Oftentimes, you will also have user-driven events that could re-trigger an endpoint call or a brand new one.  In either case, the returned data will likely change the state of one or more things.  And it’s typically desirable to track the state of the call itself as well.  Lastly, there are also events that need to trigger a state change outside of a user-driven event, like the loading of the page.  These are called side-effects.  For state tracking, we can use useState or useReducer to delegate the tracking/changing of the state of particular objects/variables.  For side effects, we can use useEffect to trigger effects when the state of something changes or when the DOM finishes loading.  Lastly, we also want to be able to drive calls to the api from events, not just other state changes.  For this we still can use normal functions as handlers to start an API call and subsequently begin updating our state.  What I hope you see through this article is a holistic approach to encapsulating it all within a single API hook function.  

useState vs useReducer

I wanted to make one note about useState and useReducer and the way I have organized this walkthrough.  Here are the short definitions provided for useState and useReducer from the React docs:

useState: Returns a stateful value, and a function to update it.

useReducer: An alternative to useState.

Note: useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values. It also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

https://reactjs.org/docs/hooks-reference.html

So in an ordinary walkthrough, we would probably start with useState and then show you useReducer. However, this walkthrough will do the opposite. I will first show you one way to set up for a reducer and then for using a couple of states.  You can then make your own decision on which way you want to architect your solutions.  Personally, I gravitate towards the useReducer structure, even though it ends up being a little bit more code.  With that out of the way, let’s get started. 

Setup

The full functionality on display for the Heroes part

In this walkthrough, we are going to create an SPFx Web Part that interacts with 2 SharePoint lists that are linked together through Lookups.  Let’s walk through our imagined scenario. We want to track characters (called Heros) within an imagined game. This game is loosely based on the actual game Soul Hunters. Then we want to allow for reviewing character lineups (called Hero Teams).  So our Hero list will display characteristics of a Hero. The Hero Teams list will contain a team name and then 3 separate lookups back to the Hero list for the Front line, Middle line and Back line heros.

I realize that my use of the term teams could be a little confusing here.  Hopefully it doesn’t throw you off too much.  But keep in mind that I’m laying out an approach.  You could easily replace the parts of this demo to be concerning the actual Microsoft Teams objects and the calls could easily be to the Graph API instead.  You would simply substitute those parts within our encapsulated API hook functions.

I have provided the source for all of this code here.  In the topmost folder of the code, I have provided PnP Provisioning templates for the 2 lists. So you can create these lists within your SharePoint site if you’d like.  You can also change the models to anything you want.  I figured, if I’m doing this walkthrough, I may as well have fun with it.

The Heros list just captures some data concerning characters, including the number of Likes that character has received.  The Hero Teams list tracks what characters are in lineup positions.

Requirements

We want the web part to do the following:

  • Pull the full list of heros from the list into a repeating set of panels – indicate a loading status while pulling
  • Allow for refreshing the pulled list, via a Refresh button, which should redisplay loading
  • Track the status of errors for these pulls
  • Allow for liking a hero via a link click
  • Provide a property in the web part to toggle the display of a View Teams link in the character panels
  • When shown, hovering over the View Teams link will display a Hover Card.  When the hover card loads, pull all of the teams that this character belongs too.
  • Selecting a team in the list displayed in the hover card should display thumbnails of all the heros in that team

All these requirements are centered around actions driven from the user or from starting events within the part. They are also making calls to Sharepoint for further information.  We will meet all of these requirements with Hooks.

Setup

I am not going to go through all of the specifics of setting up a Sharepoint web part.  (Start here if you need that) This will start from the perspective of having already completed that and starting to build out the functionality needed by your components.

Here is the general folder structure for our solution.  Our web part will load the HerosFirst component, which will have several smaller sub-components.  We will have a few core models for Heros and Teams and a third for previewing the thumbnails of heros within any team.  The general naming convention in the React community is to prefix your hooks with “use.”  I went the extra step of classifying them each as Api’s.  The general approach I took was trying to keep these hook api’s centered around the calls needed for the type of object it interacted with.  This ended up splitting into a useHerosApi and a useTeamsApi.  They are using a useReducer pattern and a useState pattern respectively.

 

Models

Here is the code for our strongly-typed models:

//From models/Hero.ts 
export interface IHero { 
  Id: number; 
  Title: string; 
  HeroImage: IHeroImage; 
  FlavorText: string; 
  Element: string; 
  Power1: string; 
  Power2: string; 
  Likes: number; 
  Race: string; 
} 
 
export interface IHeroImage { 
  Url: string; 
}
//From Team.ts 
import { ITeamHeroImages } from "./TeamHeroImages"; 
 
export interface ITeam { 
  Id: number; 
  Title: string; 
  FrontHeroId: number; 
  MiddleHeroId: number;  
  BackHeroId: number; 
  Images: ITeamHeroImages; 
}
//From TeamHeroImages.ts 
import { IHeroImage } from "./Hero"; 
 
export interface ITeamHeroImages { 
  FrontLineHero: ITeamHeroPreview; 
  MidLineHero: ITeamHeroPreview; 
  BackLineHero: ITeamHeroPreview; 
} 
 
export interface ITeamHeroPreview { 
  Id: number; 
  Title: string; 
  HeroImage: IHeroImage; 
}

Hooks APIs

Our useHerosApi is employing a pattern around useReducer.  Let’s get our imports going for state, our models and for making our calls to Sharepoint.

import { useEffect, useReducer, ReducerAction, Dispatch } from "react"; 
import { sp } from "@pnp/sp"; 
import { IHero, IHeroImage } from "../models/Hero"; 
import "@pnp/sp/webs"; 
import "@pnp/sp/lists"; 
import "@pnp/sp/items"; 
import { LogHelper } from "../utilities/LogHelper"; 
import { 
  BaseClientSideWebPart, 
  WebPartContext 
} from "@microsoft/sp-webpart-base"; 
import { Action } from "./action";

Our Api will need a Sharepoint context in order to initialize so let’s specify that in our strongly-typed properties interface.

export interface IHeroApiProps { 
  context: WebPartContext; 
}

Encapsulation is the goal

The goal of our hooks api is threefold:

  • Encapsulate whatever logic is necessary for making calls to our api, in this case Sharepoint
  • Manage the state of whatever objects it wants to expose to a user of the api
  • Provide any handlers for asking the api to take action, where it will then dispatch changes to the state of the objects it exposes

With that understanding in mind and knowing that we are building with TypeScript, we need to build the strongly-typed objects necessary for what this Api is going to expose to a caller.  I chose to build my state hierarchically to represent the overall object that it’s tracking, along with the smaller objects whose state is directly tied to requests that the Api is making that will effect change on that object.  For Heros, this was represented as such:

  • Heros
    • GetRequest
      • IsLoading
      • HasError
      • ErrorMessage
    • LikeRequest
      • HasError
      • ErrorMessage

Here is the code for these strongly typed objects that build ultimately up to a HeroState object.

export interface GetRequest { 
  isLoading: boolean; 
  hasError: boolean; 
  errorMessage: string; 
} 
 
export interface LikeRequest { 
  hasError: boolean; 
  errorMessage: string; 
} 
 
export interface HeroState { 
  heros: IHero[]; 
  getRequest: GetRequest; 
  likeRequest: LikeRequest; 
}

All of this builds up to what our api needs to return: the state. This is a collection of properties to track and the exposed functions that are open to callers.

export interface HeroApi {
  state: HeroState;
  setRefresh: () => void; 
  setLike: (IHero) => void; 
}
***Please note that my lambda functions are getting encoded in these snippets.  So refer to the source when in doubt.

Reducers and Actions

Now let’s set up our reducer. The reducer will take in a state (our HeroState object) and an action. Its goal is to make decisions about what parts of the state object have changed based on the action type passed in. It gets a tad bit tricker though when you have TypeScript involved, particularly because the Action could have different types of payloads incoming for different actions. For instance, in our Heros API, the GET_ALL action will take a payload of a hero list and fully replace the existing list with the new, but the LIKE_HERO will take a payload of a single hero and update just that one.

I felt it best to focus on building strongly-typed actions based on the payload types that come across.

export interface HeroArrayPayloadAction extends Action {
  payload: IHero[];
}
export interface HeroPayloadAction extends Action {
  payload: IHero;
}

You may be asking where Action comes from. Notice in our imports that I also brought that in from action.ts. Here’s the definition (all actions should have a type):

export interface Action {
  type: string;
}

Finally, thanks to the beauty of TypeScript and strong typing, I also made a class to encapsulate the possible actions being worked in the reducer.

export class ActionTypes {
  static readonly GET_ALL: string = "GET_ALL";
  static readonly GET_ALL_LOADING: string = "GET_ALL_LOADING";
  static readonly GET_ALL_ERRORED: string = "GET_ALL_ERRORED";
  static readonly LIKE_HERO: string = "LIKE_HERO";
  static readonly LIKE_HERO_ERRORED: string = "LIKE_HERO_ERRORED";
}

useReducer

Finally, here is the actual setup of our reducer, called heroReducer.

function heroReducer(state: HeroState, action: Action) {
  //First establish the type of the payload
  switch (action.type) {
    case ActionTypes.GET_ALL_LOADING:
      return { ...state, getRequest: { isLoading: true, hasError: false } };
    case ActionTypes.GET_ALL:
      var arrayAction: HeroArrayPayloadAction = action as HeroArrayPayloadAction;
      return {
        ...state,
        heros: arrayAction.payload,
        getRequest: { isLoading: false, hasError: false, errorMessage: "" }
      };
    case ActionTypes.GET_ALL_ERRORED:
      return {
        ...state,
        getRequest: {
          isLoading: false,
          hasError: true,
          errorMessage: "Get Hero Failure"
        }
      };
    case ActionTypes.LIKE_HERO:
      var heroAction: HeroPayloadAction = action as HeroPayloadAction;
      const newHeros = state.heros.map(h => {
        //Only replace the record that was LIKED
        return h.Id === heroAction.payload.Id ? heroAction.payload : h;
      });
      return {
        ...state,
        heros: newHeros,
        likeRequest: { hasError: false, errorMessage: "" }
      };
    case ActionTypes.LIKE_HERO_ERRORED:
      return {
        ...state,
        likeRequest: { hasError: true, errorMessage: "Error liking this hero" }
      };
    default:
      throw new Error();
  }
}

At its core, it expects the new object state to have already been passed via a dispatch invocation, which you will see in the implementation of the api. It evaluates the type of action in the switch statement and replaces the appropriate properties of the state. One last thing to highlight is the downcasting of the action to the particular payload types that should correspond to the type of action that was passed. This seems sound to me since the API is completely encapsulating all of the dispatch logic and hiding that from consumers of the api.

The HerosAPI

Finally, let’s look at useHerosApi in pieces

export function useHerosApi(props: IHeroApiProps): HeroApi {
  const [heroState, heroDispatch] = useReducer(heroReducer, {
    heros: [],
    getRequest: { isLoading: false, hasError: false, errorMessage: "" },
    likeRequest: { hasError: false, errorMessage: "" }
  });

  sp.setup({
    spfxContext: props.context
  });

Our api expects to be initialized with the Sharepoint context passed in the properties, which we use to call sp.setup. Our first step is to initialize our reducer, with useReducer. We have passed in our heroReducer and our starting HeroState. What we have gotten back is a reference to our heroState and reference to our dispatch function, which we have called heroDispatch. Also note that the return type of useHerosApi is HeroApi, which we defined above.

Dispatching as We Go

Our api takes on the responsibility of managing calls to whatever api’s are necessary to get the job done – in this case, its all Sharepoint. Here are our 2 api calls, for getting all Heros and for liking a Hero.

async function getHerosAsync() {
    try {
      //Dispatch the LOADING action
      heroDispatch({ type: ActionTypes.GET_ALL_LOADING });

      const allItems: IHero[] = await sp.web.lists
        .getByTitle("Heros")
        .items.getAll();
      console.log(allItems);

      //Dispatch the GET_ALL action
      heroDispatch({
        type: ActionTypes.GET_ALL,
        payload: allItems
      } as HeroArrayPayloadAction);
    } catch (error) {
      console.error(error);
      LogHelper.logError("useHerosApi", "getHerosAsync", error);
      heroDispatch({ type: ActionTypes.GET_ALL_ERRORED });
    }
  }

async function likeHerosAsync(hero) {
    //Only trigger a call if a hero is passed in (this way nothing will happen on load)
    try {
      //Increase the Likes property by 1
      hero.Likes += 1;

      //Update the Likes column on the ListItem in SP
      const i = await sp.web.lists
        .getByTitle("Heros")
        .items.getById(hero.Id)
        .update({
          Likes: hero.Likes
        });
      console.log(i);

      //Dispatch the LIKE_HERO action
      heroDispatch({
        type: ActionTypes.LIKE_HERO,
        payload: hero
      } as HeroPayloadAction);
    } catch (error) {
      console.error(error);
      LogHelper.logError("useHerosApi", "likeHerosAsync", error);
      heroDispatch({ type: ActionTypes.LIKE_HERO_ERRORED });
    }
  }

Note that these functions are making use of our heroDispatch function throughout the various steps of each function. Calls to this function will ingest an Action. The action should always have a type, which we’ve defined in our ActionTypes static class. Some actions will also need a payload to be of value. This is where we are specifying our payload action types. For instance, when we update the Like column in the list, we are passing in that updated Hero as the payload cast as a HeroPayloadAction.

const i = await sp.web.lists
        .getByTitle("Heros")
        .items.getById(hero.Id)
        .update({
          Likes: hero.Likes
        });
      console.log(i);

      //Dispatch the LIKE_HERO action
      heroDispatch({
        type: ActionTypes.LIKE_HERO,
        payload: hero
      } as HeroPayloadAction);

useEffect

Seeing as the getHerosAsync function is something we need to load immediately, we are going to invoke a useEffect. The idea of useEffect is to use it for triggering side effects as things change. When defining, you are passing in a function of what needs to run when the effect is triggered. You can optionally pass in a set of parameters. These are state objects that you want to monitor for changes, which would then trigger the side effect. We only want this effect to trigger when the DOM finishes loading, so we supply an empty array.

 useEffect(() => {
    getHerosAsync();

    return () => {
      console.log("cleanup");
    };
  }, []);

Return our HerosApi

Let’s wrap up our useHerosApi by providing consumers with handlers to functions that they can call when something happens in the UI. We already specified these functions in our HeroApi object. Here we are actually defining what should happen when invoked. Once that’s set up we are ready to return our full HeroApi object from the function.

  const setRefresh = () => {
    getHerosAsync();
  };

  const setLike = (hero: IHero) => {
    likeHerosAsync(hero);
  };

  return {
    state: {
      heros: heroState.heros,
      getRequest: {
        isLoading: heroState.getRequest.isLoading,
        hasError: heroState.getRequest.hasError,
        errorMessage: heroState.getRequest.errorMessage
      },
      likeRequest: heroState.likeRequest
    },
    setRefresh,
    setLike
  };
}

Whew! That was a lot, but as you have probably noticed, we’ve encapsulated most of the power into this api object as a set of hooks. This should greatly simplify the work that our components have to take on.

HerosFirst Component

The primary goal of this writeup is to focus on how to structure our hooks, using either reducers or states, in light of using TypeScript. Thus I feel it’s necessary to examine all of the parts of the api function that we created. When I review the components however, I’m only going to focus on the parts that are using the hooks and not the entire code. By all means, have at it with the full code base here.

Our top-most component is called HerosFirst. This component imports useHerosApi function and the HeroApi object.

import { useHerosApi, HeroApi, HeroState } from "../../../hooks/useHerosApi";

Then we instantiate the api.

const HerosFirst: React.FunctionComponent<IHerosFirstProps> = props => {
  //const heroState: HeroState = useHerosApi({ context: props.context });
  const api: HeroApi = useHerosApi({ context: props.context });

Note that our object must be typed as a FunctionComponent. Hooks can only work within function components.

Let’s look at the following snippet of code in the return JSX of the component:

{api.state.heros.map((h: IHero) => (
        <div className={styles.heroPanel}>
          <div
            className={styles.heroImage}
            style={{
              backgroundImage: `url(${h.HeroImage.Url})`
            }}
          ></div>

We are looping through the Heros found on in the state of the api. As changes occur to that object, this will react.

Sub-components and Hooks

Let’s look at 2 of our sub-components: Loading and Like.

Our Loading component takes in 2 properties: isLoading and setRefresh. This means that our component is built to be very simplistic. It expects to be passed properties to react to and handlers to invoke if it has an action to perform. Here’s how the component makes use of these properties in the JSX:

export const Loading: React.FunctionComponent<IRefreshProps> = props => {
  return (
    <div className={styles.loadingBar}>
      {props.isLoading === true ? <span>Loading...</span> : <span>Done!</span>}
         
      <a className={styles.refresh} onClick={() => props.setRefresh()}>
        Refresh
      </a>
    </div>
  );
};

Similarly, our Like component takes in a hero property and a setLike function. Here’s how it makes use of these hooks in the JSX:

export const Like: React.FunctionComponent<ILikeProps> = props => {
  return (
    <div className={styles.likeBar}>
      <span>
        {props.hero.Likes} Like{props.hero.Likes === 1 ? "" : "s"}
      </span>
      <a onClick={() => props.setLike(props.hero)} className={styles.like}>
        Like
      </a>
    </div>
  );
};

Finally, let’s take a quick look at how our HerosFirst component is simply passing down the state properties that it has received from the api as well as the handlers to make requests to the api.

<Loading
        isLoading={api.state.getRequest.isLoading}
        setRefresh={api.setRefresh}
      />
<Like hero={h} setLike={api.setLike} />

Ultimately this is what our HerosFirst component looked like, but more importantly how it functioned easily, thanks to our reducer-based Heros api function.

Same approach, different functions

Now let’s take a look at a different way to approach hooks, wherein we use a collection of useState calls. We will use our Teams API as the sample. So, in this case, we will be concerned with http calls to Sharepoint surrounding our Teams list and it’s related data. Ultimately, we are still building an api as hooks-based function, whose responsibilities remain the same as our Heros API:

  • Encapsulate whatever logic is necessary for making calls to our api, in this case Sharepoint
  • Manage the state of whatever objects it wants to expose to a user of the api
  • Provide any handlers for asking the api to take action, where it will then dispatch changes to the state of the objects it exposes

Let’s dive into the code for the useTeamsApi. Our top section should look somewhat familiar, as we get our imports straightened out and establish our needed properties (SP context) and define the strongly-typed objects that need to be returned from our api function:

import { useEffect, useState } from "react";
import { sp } from "@pnp/sp";
import { IHero, IHeroImage } from "../models/Hero";
import { ITeam } from "../models/Team";
import { ITeamHeroImages } from "../models/TeamHeroImages";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import { LogHelper } from "../utilities/LogHelper";
import {
  BaseClientSideWebPart,
  WebPartContext
} from "@microsoft/sp-webpart-base";

export interface ITeamsApiProps {
  context: WebPartContext;
}

export interface ITeamsRequest {
  teams: ITeam[];
  hero: IHero;
  isLoading: boolean;
  isError: boolean;
}

export interface TeamsState {
  request: ITeamsRequest;
  setHeroTeams: (IHero) => void; 
  expandTeam: (ITeam) => void;
}

The TeamsAPI

Since we don’t have to work with a reducer that is managing dispatched actions, there’s less code involved. Instead you will need to track and set the appropriate states from your functions, rather than dispatching an action to be centrally managed.

export function useTeamsApi(props: ITeamsApiProps): TeamsState {
  const [teams, setTeams] = useState<ITeam[]>([]);
  const [hero, setHeroContext] = useState<IHero>(undefined);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [isError, setIsError] = useState<boolean>(false);

  sp.setup({
    spfxContext: props.context
  });

Our useTeamsApi brings in our props and returns the TeamsState object, which contains requested information (teams array, highlighted hero, and loading and error states) along with handlers for retrieving teams for a specified hero and for getting some details about a team.

We begin by making a series of useState calls. The useState function takes in a default value and returns 2 items: the object whose state you are tracking, and the dispatched function to call in order to change the state of that object. Note that the type is inferred generically, thanks to TypeScript, as indicated in the <> brackets in this line:

const [hero, setHeroContext] = useState<IHero>(undefined);

Setting State as We Go

Let’s look at our functions for interacting with Sharepoint.

  /*
  Gets a filtered list of teams, based on whether or not the incoming hero is in any part of a lineup
  */
  async function getTeamsAsync(hero: IHero) {
    if (hero !== null && hero !== undefined) {
      try {
        setIsError(false);
        setIsLoading(true);

        const allItems: ITeam[] = await sp.web.lists
          .getByTitle("Hero Teams")
          .items.filter(
            `FrontHeroId eq ${hero.Id} or MiddleHeroId eq ${hero.Id} or BackHeroId eq ${hero.Id}`
          )
          .get();
        console.log(allItems);
        setTeams(allItems);

        setHeroContext(undefined); //Finally, reset our hero object so subsequent calls can be made
      } catch (error) {
        console.error(error);
        LogHelper.logError("useTeamsApi", "getTeamsAsync", error);
        setIsError(true);
      }
      setIsLoading(false);
    } else {
      console.log("Fired a blank (get teams)");
    }
  }

  /*
  Fills out the Team Images portion of the team, with an additional call back to the Heros list for the lineup id's of the incoming team
  */
  async function getHeroImagesForTeamAsync(team: ITeam) {
    if (team !== null && team !== undefined) {
      try {
        setIsError(false);
        setIsLoading(true);

        const teamImages: IHero[] = await sp.web.lists
          .getByTitle("Heros")
          .items.filter(
            `Id eq ${team.FrontHeroId} or Id eq ${team.MiddleHeroId} or Id eq ${team.BackHeroId}`
          )
          .select("Id", "Title", "HeroImage")
          .get();
        console.log(teamImages);
        const newTeams: ITeam[] = teams.map(t => {
          if (t.Id === team.Id) {
            const expandedTeam = {
              ...t,
              Images: {
                FrontLineHero: null,
                MidLineHero: null,
                BackLineHero: null
              }
            };
            teamImages.forEach(ti => {
              switch (ti.Id) {
                case expandedTeam.FrontHeroId:
                  console.log(`Found Front Line Hero ${ti.Title}`);
                  expandedTeam.Images.FrontLineHero = {
                    Id: ti.Id,
                    Title: ti.Title,
                    HeroImage: ti.HeroImage
                  };
                  break;
                case expandedTeam.MiddleHeroId:
                  console.log(`Found Mid Line Hero ${ti.Title}`);
                  expandedTeam.Images.MidLineHero = {
                    Id: ti.Id,
                    Title: ti.Title,
                    HeroImage: ti.HeroImage
                  };
                  break;
                case expandedTeam.BackHeroId:
                  console.log(`Found Back Line Hero ${ti.Title}`);
                  expandedTeam.Images.BackLineHero = {
                    Id: ti.Id,
                    Title: ti.Title,
                    HeroImage: ti.HeroImage
                  };
                  break;
                default:
                  break;
              }
            });
            return expandedTeam;
          } else {
            return t;
          }
        });
        setTeams(newTeams);

        setHeroContext(undefined); //Finally, reset our hero object so subsequent calls can be made
      } catch (error) {
        console.error(error);
        LogHelper.logError("useTeamsApi", "getHeroImagesForTeamAsync", error);
        setIsError(true);
      }
      setIsLoading(false);
    } else {
      console.log("Fired a blank (get team images)");
    }
  }

Similar to our useReducer methodology, these functions are encapsulating the bulk of the work and making state changes as the logic progresses. But rather than make dispatch calls, they are making specific calls to our useState functions that we established early, like in this example when a successful call is made to retrieve any teams where a particular hero might exist in any of the 3 lineup columns from our Teams list.

const allItems: ITeam[] = await sp.web.lists
          .getByTitle("Hero Teams")
          .items.filter(
            `Front_x0020_HeroId eq ${hero.Id} or Middle_x0020_HeroId eq ${hero.Id} or Back_x0020_HeroId eq ${hero.Id}`
          )
          .get();
        console.log(allItems);
        setTeams(allItems);

        setHeroContext(undefined); //Finally, reset our hero object so subsequent calls can be made
      }

Return our TeamsState

Let’s close up our teams api with our functions we want to expose to callers and our final TeamsState return object.

const setHeroTeams = (hero: IHero) => {
    getTeamsAsync(hero);
  };

  const expandTeam = (team: ITeam) => {
    getHeroImagesForTeamAsync(team);
  };

  return {
    request: { teams, hero, isLoading, isError },
    setHeroTeams,
    expandTeam
  };
}

Incorporating Teams

Returning to our HerosFirst component, let’s import our useTeamsApi hook.

import { useTeamsApi, TeamsState } from "../../../hooks/useTeamsApi";

Props and Function Components

Let’s take a quick detour to show that properties from the web part can still come through just fine to our function components. I have defined a toggle switch to allow the editor of a page to turn on/off the viewing of the teams functionality within the web part, as defined in the web part code.

export interface IHerosFirstWebPartProps {
  showTeams: true;
}

Our component expects 2 properties to be passed to it from the web part, the Sharepoint context and the showTeams setting.  One more side note: I thought about including useContext in this tutorial but I think we bit off enough already.  Also, remember that we probably shouldn’t be passing the full WebPartContext down but rather the ServiceScope.

export interface IHerosFirstProps {
  context: WebPartContext;
  showTeams: boolean;
}

The showTeams property is handled just fine, despite the fact that the component is a function component.

const element: React.ReactElement<IHerosFirstProps> = React.createElement(
      HerosFirst,
      {
        context: this.context,
        showTeams: this.properties.showTeams
      }
    );

We evaluate this showTeams property before deciding to render out our TeamsPopup component. Take note of the properties being set, which are all coming from either our useHerosApi state or our useTeamsApi state. Easy peasy!

                {props.showTeams ? (
                  <TeamsPopup
                    hero={h}
                    teams={teamsState.request.teams}
                    setHeroTeams={teamsState.setHeroTeams}
                    expandTeam={teamsState.expandTeam}
                  />
                ) : null}

Here’s that in action:

Teams Popup sub-component

Finally, let’s look at what the TeamsPopup sub-component does. The only specialness to it is the fact that it leverages the HoverCard, providing by the Fabric-UI framework. There are events provided by the HoverCard that we use to know when to invoke our callbacks to setHeroTeams. Additionally, our team link onClick handler invokes the expandTeam callback to load a series of tiny thumbnails for team members.

export interface ITeamsPopupProps {
  hero: IHero;
  teams: ITeam[];
  setHeroTeams: (IHero) => void; 
  expandTeam: (ITeam) => void;
}

export const TeamsPopup: React.FunctionComponent<ITeamsPopupProps> = props => {
  const expandingCardProps: IExpandingCardProps = {
    onRenderCompactCard: _onRenderCompactCard,
    onRenderExpandedCard: _onRenderExpandedCard,
    renderData: props
  };
  return (
    <HoverCard
      expandingCardProps={expandingCardProps}
      instantOpenOnClick={true}
      onCardVisible={() => props.setHeroTeams(props.hero)}
      className={styles.teamsHover}
    >
      <span className={styles.teamsLabel}>View Teams</span>
    </HoverCard>
  );

  function _onRenderCompactCard(props: ITeamsPopupProps): JSX.Element {
    return (
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          height: 100,
          fontWeight: "bold",
          fontSize: 12,
          color: "orange"
        }}
      >
        <h3>{props.hero.Title}</h3>
      </div>
    );
  }

  function _onRenderExpandedCard(props: ITeamsPopupProps): JSX.Element {
    return (
      <div
        style={{
          padding: "16px 24px",
          fontWeight: "bold",
          fontSize: 12,
          color: "orange"
        }}
      >
        {props.teams.map((t: ITeam) => (
          <div>
            <p
              onClick={() => props.expandTeam(t)}
              style={{ textDecoration: "underline", cursor: "pointer" }}
            >
              {t.Title}
            </p>
            {teamImagesToShow(t.Images)}
          </div>
        ))}
      </div>
    );
  }

  function teamImagesToShow(images: ITeamHeroImages): JSX.Element {
    if (images !== null && images !== undefined) {
      return (
        <div>
          <img
            style={{ height: 30, width: 30 }}
            src={images.FrontLineHero.HeroImage.Url}
          />
          <img
            style={{ height: 30, width: 30 }}
            src={images.MidLineHero.HeroImage.Url}
          />
          <img
            style={{ height: 30, width: 30 }}
            src={images.BackLineHero.HeroImage.Url}
          />
        </div>
      );
    }
  }
};

Here is the finished outcome for the needed Teams functionality.

Conclusion

Hopefully, this article has helped you feel more prepared to dive into hooks and to be thinking of the way you want to structure your solution. I’ve presented an encapsulated approach, one which used a reducer and one which used a series of useState calls. Personally, I prefer the reducer method, despite the fact that it means more TypeScript code to keep you type safe. But I will let you decide and refactor as you see fit. I hope this was helpful. Please feel free to provide feedback or ask questions about the code.

Links

GitHub Repo: https://github.com/mhomol/Heros-spfx-react-hooks

Learn More About Our Application Development Offerings

Related Content