SPFx Series: Understanding Props & State

When you start building SharePoint Framework components, one of the first things that can trip you up is understanding the difference between props and state. I see developers mixing these up all the time, so let’s clear this up once and for all.

The main difference between props and state is simple: props are configuration values that come from outside your component, while state is internal data that your component manages itself. Think of props as the settings someone gives you, and state as the notes you keep for yourself.

Props: Your Component’s Configuration

Props (short for properties) define how your component should behave or appear. They’re the knobs and switches that users or developers can adjust to customize your web part or extension. When someone adds your web part to a page and opens the property pane, they’re modifying props.

Let’s look at a typical example:

TSX
export interface IHelloWorldWebPartProps {
title: string;
showTitle: boolean;
description: string;
maxItems: number;
}

In this example, we’ve defined four props:

  • title: The heading that will be displayed
  • showTitle: A toggle to hide or show the title
  • description: Some additional text to display
  • maxItems: How many items to show in a list

These props are defined in your web part manifest and automatically generate fields in the property pane. When a user changes the title from “Hello World” to “Welcome”, they’re modifying a prop. Your component receives this new value and re-renders accordingly.

Props Are Immutable

Here’s something important: your component should never modify its own props directly. Props flow in one direction only – from parent to child, from configuration to component. If you try to change a prop value inside your component, you’re doing it wrong.

TypeScript
// DON'T DO THIS
public render(): void {
this.properties.title = "New Title"; // Wrong!
// Your render code...
}

The property pane handles prop updates for you. When someone changes a value in the property pane, SPFx automatically calls your component’s render method with the new prop values.

Defining Props in the Property Pane

Here’s where it gets practical. You define your props interface, but how do you actually create those fields in the property pane? This happens in your web part’s getPropertyPaneConfiguration() method. See also the other blog post SPFx Series: Web part properties.

Let’s build on our earlier example, the IHelloWorldWebPartProps shown above. Now we need to create the actual property pane controls for these props:

TypeScript
import {
PropertyPaneTextField,
PropertyPaneToggle,
PropertyPaneSlider
} from '@microsoft/sp-property-pane';
export default class HelloWorldWebPart extends BaseClientSideWebPart<IHelloWorldWebPartProps> {
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: "Configure your web part settings"
},
groups: [
{
groupName: "Display Settings",
groupFields: [
PropertyPaneTextField('title', {
label: 'Title',
description: 'The main heading for your web part'
}),
PropertyPaneToggle('showTitle', {
label: 'Show title',
onText: 'Visible',
offText: 'Hidden'
}),
PropertyPaneTextField('description', {
label: 'Description',
multiline: true,
rows: 3
}),
PropertyPaneSlider('maxItems', {
label: 'Maximum items to show',
min: 1,
max: 50,
step: 1,
showValue: true
})
]
}
]
}
]
};
}
}

Notice how the first parameter of each property pane control matches the property name in your interface? That’s the connection. When you write PropertyPaneTextField('title', {…}), you’re telling SPFx “this text field controls the title prop”.
The magic happens automatically. When someone types “My News Feed” in that text field, SPFx updates this.properties.title and calls your render method. You don’t write any code to handle that – it just works.

Default Values

You might wonder where default values come from. You set those in your web part manifest (HelloWorldWebPart.manifest.json):

JSON
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "abc123...",
"alias": "HelloWorldWebPart",
"componentType": "WebPart",
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Other" },
"title": { "default": "Hello World" },
"description": { "default": "Shows a hello world message" },
"officeFabricIconFontName": "Page",
"properties": {
"title": "My News Feed",
"showTitle": true,
"description": "Latest news from our organization",
"maxItems": 10
}
}]
}

The values in the properties section are your defaults. When someone adds your web part to a page for the first time, these are the values they’ll see.

State: Your Component’s Memory

State is completely different. State represents data that changes over time and is managed internally by your component. It’s the information your component needs to keep track of while it’s running.

Here’s a simple state interface:

TypeScript
export interface IHelloWorldWebPartState {
selectedOption: string;
isLoading: boolean;
items: any[];
errorMessage: string;
}

Notice how these values are different from props:

  • selectedOption: Changes when the user picks something from a dropdown
  • isLoading: Tracks whether data is currently being fetched
  • items: Stores the data retrieved from an API
  • errorMessage: Holds any error text that needs to be displayed

These aren’t configuration values that come from the property pane. They’re dynamic values that change based on what’s happening in your component right now.

Managing State in SPFx

In React-based SPFx components (which is what you’ll be building most of the time), you manage state using React’s state management. Here’s how you typically set it up in your React component:

TypeScript
export default class HelloWorld extends React.Component<IHelloWorldProps, IHelloWorldState> {
constructor(props: IHelloWorldProps) {
super(props);
// Initialize your state here
this.state = {
selectedOption: '',
isLoading: false,
items: [],
errorMessage: ''
};
}
public componentDidMount(): void {
// Load data when component mounts
this.loadItems();
}
private async loadItems(): Promise<void> {
// Set loading state
this.setState({ isLoading: true, errorMessage: '' });
try {
const items = await this.fetchItems();
this.setState({ items: items, isLoading: false });
} catch (error) {
this.setState({
errorMessage: 'Failed to load items',
isLoading: false
});
}
}
private handleDropdownChange = (option: string): void => {
// Update state when user makes a selection
this.setState({ selectedOption: option });
}
public render(): React.ReactElement<IHelloWorldProps> {
// Use both props and state in rendering
const { title, showTitle } = this.props;
const { selectedOption, isLoading, items, errorMessage } = this.state;
return (
<div>
{showTitle && <h2>{title}</h2>}
{isLoading && <Spinner label="Loading..." />}
{errorMessage && <MessageBar messageBarType={MessageBarType.error}>{errorMessage}</MessageBar>}
{!isLoading && items.map(item => (
<div key={item.id}>{item.title}</div>
))}
<Dropdown
selectedKey={selectedOption}
onChange={this.handleDropdownChange}
options={this.getDropdownOptions()}
/>
</div>
);
}
}

Notice that the state is used in your component file, not your webpart file!

Every time you call this.setState(), React automatically re-renders your component. This is powerful but also something you need to be careful with. If you update state too frequently or unnecessarily, you can cause performance problems.