Building an App with SPFx, Reactjs, and TypeScript Part 6: React-Redux

Introduction

In this post, we’re going to look at some react-redux basics. Before we do, I have made some additions to the solution that I haven’t covered because it would’ve just been a repeat of things we’ve already gone over. The changes include some styling around the Group details (where the location, organizers, members appear), the addition of a property in the property pane which is used to point to a new meeting list, and a meetingList component which is used to render the meetings. The meeting list contains an item for each meeting with a lookup to the Event list which we’ll use to render the right meetings for the selected event.

This post was a little challenging to write and assumes you know a little about redux but maybe are having some trouble applying redux to SPFx because of TypeScript. If you are brand new to redux, I suggest you read up on it on the ReduxJS site.

My naming choices are getting messy. Event vs Group vs Meeting. That’s what I get for whipping this together without planning, but we’ll try to cover this up with redux. So what I’m calling an Event is essentially our group (Tri-State Office 365 section) and then below the event are the meetings (March, April, May) for that group.

State Management with React-Redux

Now that you’re caught up, let’s talk some react-redux. State management in this demo isn’t complex and because of that, redux isn’t really necessary here but that’s not going to stop us from adding it for the sake of discussion. In this demo, we have one state object that gets created in our Event Info component and that state is passed down as props down to any other component that needs it. We also have a 2nd state object created in the Members component. It’s created with hard coded values but we’re not going to focus on that state object here. In both components, we’re not changing any state values, and we’re not creating or deleting values from the state. We also aren’t sharing values unless we’re passing them down to a component created inside our component. For example, EventInfo passes a property from its event state down to the MeetingList component.

The state objects in both the Event Info and Members components are not global. They’re passed down to the component that needs them. If we need to share our state data with components that aren’t nested, we would need to repeat CRUD operations in the various components. Redux allows us to centralize that state by creating a central object and functions that will manage the CRUD operations needed.

There are 4 basic concepts in redux that we need to know:

  • Store – holds the application state, allows state to be accessed/updated
  • Reducers – updates your state
  • Actions – defines what we want to do (the action) and the required payload. For example, if we want to add an attendee to a meeting, the action may be named “ADD_ATTENDEE” and the payload may be an object with the attendees name and email.
  • Subscription – a registered listener that tells your component when the state has been updated in the store

We start by installing redux, react-redux, and redux-thunk (we’ll talk about thunk later). Then we’ll create new folders to house our actions and reducers.

npm install --save redux
npm install --save react-redux
npm install --save redux-thunk

As you can see, we have a new Store folder and inside of it are the Actions and Reducers folders.

In the EventHub.tsx, we import createStore from redux which will let us setup a store for our app state. The store also needs a reducer in order to set it up so we’ll create that as well. First, the reducer.

I created a file in the reducer folder named EventInfo.tsx. I like naming my reducers after the component that is expected to use it. Then I’m going to move all of my state logic over from EventInfo to that reducer. Start by importing IEventInfoState like we did in the EventInfo component. Move our initialState object over to the reducer. Then we create our reducer function which takes a state and action and for now, we’ll just return the state. For now, our reducer is ready and we can connect it to our store. We’ll revisit the reducer soon.

import { IEventInfoState } from '../../components/EventInfo/IEventInfoState';

const initialState: IEventInfoState = {
    event: {
        name: '',
        location: '',
        organizers: [],
        numOfMembers: 0,
        meetings: []
    }
};

export const reducer = (state: IEventInfoState = initialState, action) => {
    return state;
};

Next, we go to the EventHub.tsx component since it’s sort of our entry point in that it’s directing what we’re loading. It is in this component where we’ll create our store. (You could also choose to do this in the web part class since that’s what loads the EventHub component). To start, we need to add a few imports.

import { Store, createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { reducer } from '../../store/reducers/EventInfo';
import { IStateProps } from '../../store/IStateProps';

I haven’t mentioned IStateProps yet. In my store folder, I added IStateProps which will define what our global state object will look like. It extends the IEventHubWebPartProps which will allow our state to carry the web part properties around. You’ll also notice a familiar block of code in the group object. It’s almost identical to our “event” object except for it’s name. I mentioned that I was going to clean up some of my naming issues and this where I’m doing it. When we start using our global state, you’ll see me reference group instead of event. Under that, you’ll see a couple of functions that we’ll define later (onIncrementMembers and onInitEvent).

export interface IStateProps extends IEventHubWebPartProps {
    group?: {
        name: string,
        location: string,
        organizers: string[],
        numOfMembers: number,
        meetings: any[],
        
    },
    onIncrementMembers?: () => any;
    onInitEvent?: (props:any) => any;
}

Going back to EventHub, I’ll need to add a public property and a constructor where I’ll instantiate my store object. The createStore function expects a reducer and I’m going to also add thunk as I’ll need it later.

  private store: Store<IStateProps>;

  public constructor(props:IEventHubProps) {
    super(props);

    this.store = createStore(
      reducer, 
      applyMiddleware(thunk)
    );
  }

We now have our imports, and our store. Next, we need to apply our Provider and connect the store. To do that, we’ll wrap everything in our render’s return with Provider and assign our store to the store prop. What this does is it makes our new redux store object available to any component that uses “connect.” It let’s those components use the global state from the store.

public render(): React.ReactElement<IEventHubProps> {
    return (
      <Provider store={this.store}>
        <HashRouter>
          <Chrome>
            <Switch>
                <Route path="/members" component={Members} />
                <Route 
                  path="/" 
                  exact 
                  render={() => (<EventInfo {...this.props} />) } />
                <Route render={() => <h1>Page Not found</h1>} />
            </Switch>
          </Chrome>
        </HashRouter>
      </Provider>
    );
  }

Now we can start taking steps to remove our state management from our components and leverage redux. We’ll return to our EventInfo component.

In order to connect to redux, we need to add a new import. The following gives us a function that will allow us to make that connection and while we’re importing, we’ll add IStateProps here too.

import { connect } from 'react-redux';
import { IStateProps } from '../../store/IStateProps';

Next, right above our export and outside of our component class, we’ll create a new const called mapStateToProps that will hold our state values. I mentioned that I was choosing poor names throughout the web part so I’m going to make a slight naming correction here. We’re going to map our state event to a group property and refer to our events as groups from here on. As you can see, we take the props that were passed to the component, represented by ownProps, and we’re assigning their values back to properties that will be part of our new state object. We are also taking our old state’s event property and mapping it to the new state’s group property. Then we need to pass our mappings as a parameter to a connection function that wraps our export. This will let me quickly clean up that naming issue that I mentioned. I could also go back and just clean it up everywhere but I thought this would be a good way to demonstrate that we’re using different objects now.

Finally, if you look down at the export, you’ll see the connect function that accepts our mapStateToProps object and wraps our EventInfo. This is where we connect our component to redux and how our new state get’s connected.

const mapStateToProps = (state:IEventInfoState, ownProps:IEventHubProps):IStateProps => {
    return {
        description: ownProps.description,
        listName: ownProps.listName,
        listItem: ownProps.listItem,
        meetingListName: ownProps.meetingListName,
        group: {
            ...state.event,  
        }

    };
};


export default connect(mapStateToProps)(EventInfo);

Here’s where TypeScript makes things interesting. If you aren’t using TypeScript, you can start using that group property by using “this.props.group” but because we are, group isn’t found. The reason it’s not found is because in our class declaration line, we are still using the old interface for our props (IEventHubProps) which doesn’t include a group property. IEventHubProps just contains the properties that we captured in the property pane.

class EventInfo extends React.Component<IEventHubProps, IEventInfoState> { ... }

We need to replace IEventHubProps with IStateProps. Remember, it extends IEventHubProps so we still have the web part properties, and it has our old state properties which we’ll map data to. The class should look like this now.

class EventInfo extends React.Component<IStateProps, IEventInfoState> 

Now we can start replacing state references in our EventInfo component with our new prop. For example, “this.state.event.organizers” becomes “this.props.group.organizers”

public render(): React.ReactElement<any> {
        return (
            <div>
            <div className={styles.EventInfo}>
                <img src="https://secure.meetupstatic.com/photos/event/8/c/e/0/600_466836064.jpeg" alt="test" />
                <div className={styles.Details}>
                    <h2>{this.props.group.name}</h2>
                    {/*<h2>{this.state.event.name}</h2>*/}
                    <div>
                        <Icon iconName='MapPin' className='MapPin' />
                        {this.props.group.location}
                        {/*{this.state.event.location}*/}
                    </div>
                    <div>
                        <Icon iconName='PartyLeader' className='PartyLeader' />
                        {'Organizers: ' + this.props.group.organizers.join(', ')}
                        {/*{'Organizers: ' + this.state.event.organizers.join(', ')}*/}
                    </div>
                    <div>
                        <Icon iconName='Group' className='Group' />
                        {this.props.group.numOfMembers + ' members'}
                    </div>
                </div>
                
                </div>
                <hr />
                <MeetingList meetings={this.props.group.meetings} />
                {/*<MeetingList meetings={this.state.event.meetings} />*/}
            </div>
        );
    }

I left the old state properties commented in the code above for reference. If you were to run this, you wouldn’t see any data and that’s because we haven’t set our new state object. We’ve simply defined what it would look like, how we’re going to map the old state to the new state, and connected our component to redux to allow it to use the new mapped object. We need to replace all of the local state logic that we had in our ComponentDidMount function and move it to our global state.

Actions and Reducers

The first thing that I’ll do is add actionTypes.ts to the actions folder. This is just going to store some global constants that we’ll use in our action creators and reducers. These constants are used in multiple places so it’s just good practice to centralize it to avoid inadvertently making a typo and wasting time trying to find it.

export const INCREMENT_MEMBER = 'INCREMENT_MEMBER';
export const DECREMENT_MEMBER = 'DECREMENT_MEMBER';
export const SET_EVENT = 'SET_EVENT';
export const INIT_EVENT = 'INIT_EVENT';

Now back to our reducer that we saw earlier. Our reducer will receive our current state, create a new state object, make the necessary updates, and pass that back to our component. Here’s what the new reducer looks like. A switch statement will decide what to do based on the action type. So when we want to increment the number of members, we take the value from the state that’s passed in, add a 1, assign it to the new state object, and send that back.

export const reducer:Reducer<any> = (state: IEventInfoState = initialState, action) => {
    let newState = Object.assign({}, state);

    switch (action.type){
        case actionTypes.INIT_EVENT, actionTypes.SET_EVENT:
            newState.event = action.payload;
            return newState;
        case actionTypes.INCREMENT_MEMBER:
            newState.event.numOfMembers = state.event.numOfMembers + 1;
            return newState;            
        default: 
            return state;
    }
};

Now we move on to the actions. We have a new eventInfo.ts in our action folder and all of the logic that was in our ComponentDidMount which would get our data from a SharePoint list now lives here with some changes.

We have a new initEvent function that accepts properties of type IEventHubWebPartProps which are the props that we set in the property pane. We’re still in the actions/eventInfo.tsx at this point. We wrap that in a dispatch and at the end, when we have our object fully created, we call a dispatch with another function and our payload.

export const initEvent = (props:IEventHubWebPartProps) => dispatch => {
    let selectedEvent:any = {};

    if (props.listName)
    {
        // store the item id
        const id = Number(props.listItem);

        sp.web.lists.getByTitle(props.listName)
            .items.getById(id).select("Title", "Organizers/Title", "Event_x0020_Location/Address", "Members").expand("Organizers/Title").get().then((item: any) => {
                
                // get the location field's values in JSON format
                const location = JSON.parse(item['Event_x0020_Location']);
                // get the Organizer field's values
                const meetingOrganizers = item['Organizers'];

                
                // set the event state
                selectedEvent = { 
                    event: {
                        name: item.Title,
                        location: location.Address.City + ', ' + location.Address.State,
                        organizers: meetingOrganizers.map(o => o.Title),
                        numOfMembers: item['Members'],
                        meetings: []
                    } 
                };
            }).then(() => {
                const updateEvents = {...selectedEvent.event};

                sp.web.lists.getByTitle(props.meetingListName)
                    .items.filter(`Event_x0020_HubId eq ${id}`).get().then((items: any) => {


                        items.forEach(function (m) {
                            let meetingDate = new Date(m.Start_x0020_Time);
                            
                            updateEvents.meetings.push({
                                date: meetingDate.toDateString(),
                                title: m.Title,
                                description: m.Description
                            });
                        });

                        selectedEvent.event = updateEvents;
                        dispatch(setEvent(selectedEvent.event));
                    });
            });
    }
};

The setEvent function is simply our action with the payload.

export const setEvent = (event) => {
    return {
        type: actionTypes.SET_EVENT,
        payload: event
    }
};  

We now need to call all of this somehow. In order to do that, we need to dispatch our actions to the store. So we once again go back to our EventInfo.tsx component and right under mapStateToProps, we will create mapDispatchToProps and add that to our connection function.

const mapDispatchToProps = (dispatch) => {
    return {
        onIncrementMembers: () => dispatch({type: 'INCREMENT_MEMBER'}),
        onInitEvent: (wpProps) => dispatch(actions.initEvent(wpProps))
    };
};


export default connect(mapStateToProps, mapDispatchToProps)(EventInfo);

The first thing you may notice is that we are defining functions. Those functions are the ones that are part of our IStateProps interface. The idea is that we will be able to use our new props to call our functions. So just like we were able to call this.props.group.organizers, we can also call this.props.onInitEvent().

The first function is a typical action dispatch where you can pass in a type and payload and the reducer takes it from there. The second function is a little more involved. In that function, we call another function that lives with our actions and that creates our object that gets assigned to the state. Once the object is created, that dispatch is called, and our action is given our events. Since we need to call a function first to create the object that is eventually passed to our action, we need to use thunk. There isn’t anything extra that we need to do for it to work. All the setup was done when we created our store in EventHub.tsx. Thunk allows us to pass a function to the dispatch. Without it, you’d see an error telling you that it’s expecting a simple object and not a function.

The wiring up of everything is annoying and time consuming since there are so many steps and you end up going back and forth but at this point, we have 2 final steps. Back in our EventInfo component, we need to update our ComponentDidMount and have it simply call our props’ onInitEvent function which was defined in mapDispatchToProps. The new componentDidMount looks like the following snippet and all of the code that used to be in that function are now in the initEvent in our action.

public componentDidMount(): void {
        this.props.onInitEvent(this.props);
}

Finally, just to wrap it up and show that redux is wired up, I’ll throw in a button on the page which takes the total number of members, adds 1, and displays it on the page. It’s not saving to SharePoint; just adding to the state. If you wanted to save, you could just use PnPJS to update SharePoint with the new value from your state.

<div>
   <button onClick={this.props.onIncrementMembers} >Add Member</button>
</div>

Now I’ll click the button a bunch of times and you’ll see that I went from 344 member to 356.

Conclusion

I know… that was a lot and probably a bit confusing but that’s because of all of the bouncing around that you end up doing to wire everything up. To summarize, we created a Store object that accepts a reducer. We have a global State object that get’s centrally updated. We moved our state logic into our action creator instead of our components so that all the updates happen in a single location. The actions are what send data to our store. We also have a reducer that defines how our state will change. The example that we saw was adding 1 to our member counts. Inside our components, we have a mapStateToProps that defines which state property get’s mapped to which global state property. We also have a mapDispatchToProps where we dispatch our actions to the store. *Phew*. I hope this series was helpful. I know that some of the react-redux setup was tricky for me when I first tried it.