How to Use the useReducer Hook for Managing Complex State in SPFx Projects: Step-by-Step Guide
State management is essential for creating dynamic and interactive components in SharePoint Framework (SPFx) solutions. While useState
is a popular choice for managing state in React-based SPFx web parts, useReducer
is better when the state logic becomes more complex. This article demonstrates the advantages of useReducer
in SPFx solutions using a counter example, highlighting its power and flexibility.
SPFx React useReducer Hook Sample
Why State Management Matters in SPFx
Imagine you’re building a web part that fetches data from multiple SharePoint lists. Managing the loading states, errors, and fetched data can quickly become messy, especially as your logic expands. Without a centralized approach, it’s easy to lose track of updates, causing bugs and inconsistencies. This is where useReducer
shines—it centralizes state transitions and simplifies managing complex workflows.
What is useReducer
Hook?
The useReducer
hook is a React hook that lets you manage state with a reducer function. This function decides how the state should change in response to specific actions. This method is particularly helpful in SPFx solutions where state transitions are complex or need a centralized approach.
For more details on useReducer
, check out the official React documentation.
Syntax:
const [state, dispatch] = useReducer(reducer, initialState);
reducer
: A function that takes the current state and an action, and returns the new state.initialState
: The initial value of the state.dispatch
: A function to send actions to the reducer.
Comparing Approaches: useState vs. useReducer
To understand the benefits of useReducer
, let’s build a simple counter web part using SPFx. We’ll compare this approach to a useState
implementation to highlight why useReducer
is a better choice for certain scenarios.
These benefits make useReducer
a powerful tool for managing state in applications where state logic is complex or needs to be shared across components.
Using Multiple useState
Hooks
When using useState
for complex state logic, you might end up with something like the following:
import React, { useState } from 'react';
const CounterWithUseState: React.FC = () => {
const [count, setCount] = useState(0);
const [isEven, setIsEven] = useState(true);
// Function to increment the count
const increment = () => {
const newCount = count + 1; // Increment the count by 1
setCount(newCount); // Update the `count` state
setIsEven(newCount % 2 === 0); // Update the `isEven` state based on the new count
};
// Function to decrement the count
const decrement = () => {
const newCount = count - 1; // Decrement the count by 1
setCount(newCount); // Update the `count` state
setIsEven(newCount % 2 === 0); // Update the `isEven` state based on the new count
};
// Function to reset the count and parity
const reset = () => {
setCount(0); // Reset the count to 0
setIsEven(true); // Reset `isEven` to true since 0 is even
};
return (
<div>
{/* Display the current count */}
<h1>Count: {count}</h1>
{/* Display whether the count is even or odd */}
<p>{isEven ? 'Even' : 'Odd'}</p>
{/* Button to increment the count */}
<button onClick={increment}>Increment</button>
{/* Button to decrement the count */}
<button onClick={decrement}>Decrement</button>
{/* Button to reset the count */}
<button onClick={reset}>Reset</button>
</div>
);
};
export default CounterWithUseState;
Challenges with the useState Approach:
Scattered Updates:
Using separate hooks for related state (e.g.,count
andisEven
) requires multiple coordinated updates, leading to potential errors.Scalability Challenges:
As you add new actions (like “double”) or more state variables, the logic becomes more complex and error-prone.Manual Synchronization:
Maintaining state consistency (updatingisEven
every timecount
changes) can easily lead to mistakes when adding more related states.Distributed Logic:
New functionalities require spreading update logic across multiple setter calls, reducing code clarity.
Using the useReducer Approach
By centralizing state updates in one reducer function, you gain clarity and scalability. Consider the following example which manages a counter’s value and parity, including an additional “double” action.
File → counterReducer.ts
// reducers/counterReducer.ts
// Define the structure of the state
export interface CounterState {
count: number;
isEven: boolean;
}
// Define the structure of the actions
export interface CounterAction {
type: 'INCREMENT' | 'DECREMENT' | 'RESET' | 'DOUBLE';
}
// Define the initial state
export const initialState: CounterState = {
count: 0,
isEven: true,
};
// Reducer function to manage state transitions
export const counterReducer = (state: CounterState, action: CounterAction): CounterState => {
switch (action.type) {
case 'INCREMENT': {
const newCount = state.count + 1;
return {
count: newCount,
isEven: newCount % 2 === 0,
};
}
case 'DECREMENT': {
const newCount = state.count - 1;
return {
count: newCount,
isEven: newCount % 2 === 0,
};
}
case 'RESET': {
return initialState;
}
case 'DOUBLE': {
const newCount = state.count * 2;
return {
count: newCount,
isEven: newCount % 2 === 0,
};
}
default:
return state; // Return unchanged state for unrecognized actions
}
};
File →CounterWithUseReducer.tsx
import React, { useReducer } from 'react';
import { counterReducer, initialState } from '../reducers/counterReducer';
const CounterWithUseReducer: React.FC = () => {
// Initialize the state and dispatch function using useReducer
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div>
<h1>Count: {state.count}</h1>
<p>{state.isEven ? 'Even' : 'Odd'}</p>
{/* Button to increment the count */}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
{/* Button to decrement the count */}
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
{/* Button to reset the count */}
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
{/* Button to double the count */}
<button onClick={() => dispatch({ type: 'DOUBLE' })}>Double</button>
</div>
);
};
export default CounterWithUseReducer;
Benefits of the useReducer
Approach
Centralized State Updates
With a reducer, all logic for updatingcount
andisEven
is encapsulated in one place. This makes it easier to reason about how the state changes on each action.Easier to Add New Actions
To add a new action, you simply extend the action type and handle it in the reducer. There’s no need to sprinkle additionalsetState
calls in different places.Improved Readability
Having a single reducer function that describes all possible state changes keeps your component simpler and more predictable.Scales Better for Complex State
As your application grows, a reducer-based approach often becomes more maintainable than using multipleuseState
hooks for closely related pieces of state.
Handling Data Fetching in SPFx with useState vs. useReducer
For growing SPFx projects, useReducer
provides a structured approach to state transitions, keeping components clean and logic straightforward. Below is an example using useState
to fetch and display SharePoint list items.
Example: Using useState
for Fetching Items
Below is an example of using useState
to fetch and display SharePoint list items:
import * as React from 'react';
import styles from './ReactUseReducerHookBasic.module.scss';
import { IReactUseReducerHookBasicProps } from './IReactUseReducerHookBasicProps';
import { initialState, listReducer } from '../reducers/listReducer';
import { spfi, SPFx } from '@pnp/sp';
import '@pnp/sp/webs';
import '@pnp/sp/lists';
import '@pnp/sp/items';
const ReactUseReducerHookBasic: React.FC<IReactUseReducerHookBasicProps> = ({ listName }) => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchItems = async () => {
setLoading(true);
setError(null);
try {
const fetchedItems = await sp.web.lists.getByTitle(listName).items();
setItems(fetchedItems);
} catch (err) {
setError('Failed to fetch items');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchItems();
}, []);
return (
<div>
{loading && <p>Loading...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
<ul>
{items.map((item) => (
<li key={item.Id}>{item.Title}</li>
))}
</ul>
</div>
);
};
export default ReactUseReducerHookBasic;
Challenges with useState
:
Managing multiple, related states (e.g.,
loading
,error
,items
) can become tedious and error-prone.State transitions are scattered across multiple setter calls, making logic harder to follow.
Adding new state-related actions (e.g., pagination or retry logic) increases complexity.
Example: Using useReducer for Fetching Items
import * as React from 'react';
import styles from './ReactUseReducerHookBasic.module.scss';
import { IReactUseReducerHookBasicProps } from './IReactUseReducerHookBasicProps';
import { initialState, listReducer } from '../reducers/listReducer';
import { spfi, SPFx } from '@pnp/sp';
import '@pnp/sp/webs';
import '@pnp/sp/lists';
import '@pnp/sp/items';
// Define action types
const FETCH_START = 'FETCH_START';
const FETCH_SUCCESS = 'FETCH_SUCCESS';
const FETCH_ERROR = 'FETCH_ERROR';
// Define the initial state
const initialState = {
items: [],
loading: false,
error: null,
};
// Define the reducer
const listReducer = (state, action) => {
switch (action.type) {
case FETCH_START:
return { ...state, loading: true, error: null };
case FETCH_SUCCESS:
return { ...state, loading: false, items: action.payload };
case FETCH_ERROR:
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};
const ReactUseReducerHookBasic: React.FC<IReactUseReducerHookBasicProps> = ({ listName }) => {
const [state, dispatch] = useReducer(listReducer, initialState);
const fetchItems = async () => {
dispatch({ type: FETCH_START });
try {
const fetchedItems = await sp.web.lists.getByTitle(listName).items();
dispatch({ type: FETCH_SUCCESS, payload: fetchedItems });
} catch (err) {
dispatch({ type: FETCH_ERROR, payload: 'Failed to fetch items' });
}
};
useEffect(() => {
fetchItems();
}, []);
return (
<div>
{state.loading && <p>Loading...</p>}
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
<ul>
{state.items.map((item) => (
<li key={item.Id}>{item.Title}</li>
))}
</ul>
</div>
);
};
export default ReactUseReducerHookBasic;
Below is an example using useReducer
with PnP.js to fetch items in an SPFx context. All state transitions are handled in one place (the reducer), making it easier to modify and debug.
Step-by-Step Guide to Implementing useReducer
with PnPjs and Creating the Folder Structure
This guide will walk you through the process of implementing a useReducer
-based state management system and organizing your project with a clean, scalable folder structure.
Step 1: Set Up SPFx Web Part
Open a Terminal or Command Prompt Navigate to the folder where you want to create your project.
Run the Yeoman SharePoint Generator
yo @microsoft/sharepoint
Answer the Prompts:
Solution Name: (e.g.,
ReactUseReducerHookBasic
).Baseline Packages: Select SharePoint Online only (latest).
Folder Location: Select Use the current folder.
Component Type: Choose WebPart.
WebPart Name: (e.g.,
ReactUseReducerHookBasic
).Framework: Choose React.
Navigate to the Project Folder:
cd ReactUseReducerHookBasic
Install PnP.js Libraries:
npm install @pnp/sp @pnp/logging
Start the Local Development Server:
gulp serve
This will open the SharePoint Workbench in your browser, where you can preview your web part.
Step 2: Implement the Reducer Logic
Folder Structure
Create the following folders and files: Organize your project to maintain clean separation of concerns:
ReactUseReducerHookBasic/
├── src/
│ ├── webparts/
│ │ └── ReactUseReducerHookBasic/
│ │ ├── components/ # UI Components
│ │ │ ├── ReactUseReducerHookBasic.tsx # Main React Component
│ │ │ └── ReactUseReducerHookBasic.module.scss # Component Styles
│ │ ├── state/ # State management logic
│ │ │ ├── actions/ # Action types and creators
│ │ │ │ └── listActionsTypes.ts # Action types for list fetching
│ │ │ ├── reducers/ # Reducer logic and initial states
│ │ │ │ ├── listReducer.ts # Reducer for managing list state
│ │ │ │ ├── initialStates.ts # Initial state for reducers
│ │ │ └── index.ts # Re-export reducers and actions
│ │ ├── ReactUseReducerHookBasicWebPart.ts # WebPart entry file
│ │ └── index.ts # WebPart index file
Define Action Types
Create
src/webparts/ReactUseReducerHookBasic /state/actions/listActionsTypes.ts
to define action types and their structure:// src/webparts/ReactUseReducerHookBasic/state/actions/listActionsTypes.ts // Action types for list state management export const FETCH_START = 'FETCH_START'; export const FETCH_SUCCESS = 'FETCH_SUCCESS'; export const FETCH_ERROR = 'FETCH_ERROR'; // Action type definitions export type ListAction = | { type: typeof FETCH_START } | { type: typeof FETCH_SUCCESS; payload: any[] } | { type: typeof FETCH_ERROR; payload: string };
Create the Reducer
In
src/state/reducers/listReducer.ts
, implement the reducer logic:// src/webparts/ReactUseReducerHookBasic/state/reducers/listReducer.ts import { ListAction, FETCH_START, FETCH_SUCCESS, FETCH_ERROR } from '../actions/listActionsTypes'; export interface ListState { items: any[]; loading: boolean; error: string | null; } export const initialState: ListState = { items: [], loading: false, error: null, }; export const listReducer = (state: ListState, action: ListAction): ListState => { switch (action.type) { case FETCH_START: return { ...state, loading: true, error: null }; case FETCH_SUCCESS: return { ...state, loading: false, items: action.payload }; case FETCH_ERROR: return { ...state, loading: false, error: action.payload }; default: return state; } };
Re-export State Files
In
src/state/index.ts
, re-export all reducers and actions for better imports:// src/webparts/ReactUseReducerHookBasic/state/inedx.ts export * from './actions/listActionsTypes'; export * from './reducers/listReducer';
Step 3: Use the Reducer in the Component
In
src/webparts/ReactUseReducerHookBasic/components/ReactUseReducerHookBasic.tsx
, integrate the reducer and implement the fetching logic:// src/webparts/ReactUseReducerHookBasic/components/ReactUseReducerHookBasic.tsx import * as React from 'react'; import styles from './ReactUseReducerHookBasic.module.scss'; import { IReactUseReducerHookBasicProps } from './IReactUseReducerHookBasicProps'; import { initialState, listReducer } from '../reducers/listReducer'; import { spfi, SPFx } from '@pnp/sp'; import '@pnp/sp/webs'; import '@pnp/sp/lists'; import '@pnp/sp/items'; const ReactUseReducerHookBasic: React.FC<IReactUseReducerHookBasicProps> = ({ listName }) => { // Initialize our state and dispatch from the reducer const [state, dispatch] = useReducer(listReducer, initialState); /** * Asynchronous function to fetch items from the specified SharePoint list. * Dispatches actions to manage loading, success, and error states. */ const fetchItems = async () => { // Indicate that a fetch request is starting dispatch({ type: 'FETCH_START' }); try { // Fetch items from the SharePoint list using PnP.js const items = await sp.web.lists.getByTitle(listName).items(); // On success, dispatch the fetched items to be stored in state dispatch({ type: 'FETCH_SUCCESS', payload: items }); } catch (err) { // On error, store an error message dispatch({ type: 'FETCH_ERROR', payload: 'Failed to fetch list items' }); } }; /** * Use useEffect to fetch items on initial render (component mount). * This ensures that as soon as the component loads, data retrieval begins. */ useEffect(() => { fetchItems(); }, []); return ( <div> {/* Show a loading indicator while items are being fetched */} {state.loading && <p>Loading...</p>} {/* Display any error messages in red */} {state.error && <p style={{ color: 'red' }}>{state.error}</p>} {/* Render the fetched items as a list */} <ul> {state.items.map((item) => ( <li key={item.Id}>{item.Title}</li> ))} </ul> {/* If there's an error, provide a retry button */} {state.error && <button onClick={fetchItems}>Retry</button>} </div> ); }; export default ReactUseReducerHookBasic;
How It Works
Reducer & Initial State
- The component imports
listReducer
andinitialState
from yourlistReducer
file. These define how your state transitions happen (e.g.,FETCH_START
,FETCH_SUCCESS
,FETCH_ERROR
) and what the initial data, loading, and error values should be.
- The component imports
Dispatching Actions
Before making the fetch call, the component dispatches
{ type: 'FETCH_START' }
to setloading: true
.On success, it dispatches
{ type: 'FETCH_SUCCESS', payload: items }
to store the items and setloading: false
.On error, it dispatches
{ type: 'FETCH_ERROR', payload: 'Error message' }
, which setsloading: false
and updates theerror
field.
useEffect
- The component calls
fetchItems()
once after it mounts, kicking off the data load immediately.
- The component calls
SPFx-Specific Best Practices and Folder Structure
When building SPFx web parts with PnP.js and React, adopting a structured folder approach helps keep your project scalable. Here’s a recommended structure:
src/
├── components/ # UI Components
│ ├── ReactUseReducerHookBasicExample.tsx # The component for fetching and displaying items
│ ├── .... # other default files
├── state/ # State management logic
│ ├── actions/ # Action types and creators
│ │ ├── listActionsTypes.ts # Action types for list fetching
│ ├── reducers/ # Reducer logic and initial states
│ │ ├── listReducer.ts # Reducer for managing list state
│ │ ├── initialStates.ts # Initial state for reducers
│ └── index.ts # Re-export reducers and actions
├── services/ # API or utility services
│ ├── spService.ts # SPFx or PnP.js services for SharePoint API
│ └── index.ts
├── hooks/ # Custom hooks
│ ├── useFetchListItems # Custom hook for fetching SharePoint list items
│ └── index.ts # Re-export all hooks
├── utils/ # General utility functions
│ ├── logger.ts # Logging utility
│ ├── helpers.ts # Helper functions
│ └── index.ts
When to Use useReducer
Hook in SPFx Solutions
In SPFx projects using PnP.js, deciding between useState
and useReducer
often comes down to the complexity of your data-fetching logic:
useState
Approach
Use
useState
if your code is relatively simple and you only need a few pieces of state.Great for simple scenarios or small components where you have only a few states (
loading
,error
,data
).Less setup complexity, since you can directly call
setLoading(true)
, thensetItems(...)
, etc.Becomes harder to scale if you introduce more states or transitions (e.g., pagination, multiple fetch calls).
useReducer
Approach
Use
useReducer
when you need more structure, have multiple transitions, or want a centralized place to manage complex state logic.Centralizes state transitions in a single reducer function, which improves maintainability.
State transitions are complex: For example, managing multiple form fields or implementing workflows.
State updates depend on previous state: Such as toggling components or navigating multi-step processes.
Shared logic: If multiple components need to follow the same state management logic.
Scalability is a priority: For web parts with features that will grow over time.
Scales better for complex logic (e.g., multiple fetch scenarios, merging data from several lists, advanced error handling).
Clearer action semantics: dispatch typed actions—such as
FETCH_START
,FETCH_SUCCESS
,FETCH_ERROR
—making the code more self-documenting.
Both methods are valid. Pick the one that best fits your web part’s complexity and the user experience you want to deliver.
Helpful Resources and Getting Started Guides
Extracting State Logic into a Reducer: earn how to refactor complex state management in React by consolidating state update logic into a
reducer
function.React useReducer Documentation: Official React Documentation provides an in-depth explanation of the
useReducer
hook, including examples and best practices.Getting Started with SPFx: Step-by-step guide to set up your development environment for SharePoint Framework.
Using React with SPFx: A guide that explains how to build SPFx web parts using React.
SPFx GitHub Repository: The official GitHub repo for SPFx documentation and code samples.
SPFx Community Samples: Explore a wide range of community-contributed SPFx samples for inspiration.