If you’ve ever needed to store user preferences in the WordPress block editor, you’ve probably gone down the familiar path: create a custom REST API endpoint, handle the AJAX calls, manage loading states, deal with optimistic updates… It’s a lot of boilerplate for what should be a simple task.
Enter @wordpress/preferences — a package that’s been hiding in plain sight, powering core editor features like the pre-publish panel preferences, and it’s available for your plugins too.
The Problem with Custom REST Endpoints
Traditionally, storing user preferences in WordPress means:
- Registering a REST API endpoint
- Adding permission callbacks
- Sanitizing and validating input
- Writing the save/update logic
- Creating frontend API calls
- Managing loading and error states
- Implementing optimistic updates (if you’re feeling ambitious)
That’s easily 100+ lines of code just to save a boolean flag.
The @wordpress/preferences Solution
Here’s a real-world example from the Jetpack Social plugin where we needed to store two user preferences:
- Whether to show a confirmation dialog before publishing with social shares
- Whether the user has dismissed a review prompt
Here’s the entire implementation:
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback, useMemo } from '@wordpress/element';
import { store as preferencesStore } from '@wordpress/preferences';
const NAMESPACE = 'jetpack/social';
export type SocialUserPreferences = {
prePublishConfirmation: boolean | undefined;
reviewPromptDismissed: boolean | undefined;
};
const PREFERENCES: Record<string, string> = {
prePublishConfirmation: 'pre_publish_confirmation',
reviewPromptDismissed: 'review_prompt_dismissed',
} as const;
export function useSocialUserPreferences() {
const preferences = useDispatch(preferencesStore);
const data = useSelect(select => {
const store = select(preferencesStore);
return Object.fromEntries(
Object.entries(PREFERENCES).map(([key, name]) => {
const preferenceValue = store.get(NAMESPACE, name);
return [key, preferenceValue];
})
) as SocialUserPreferences;
}, []);
const toggle = useCallback(
(preference: BooleanPreferences) => {
preferences.toggle(NAMESPACE, PREFERENCES[preference]);
},
[preferences]
);
const set = useCallback(
<P extends Preference>(preference: P, value: SocialUserPreferences[P]) => {
preferences.set(NAMESPACE, PREFERENCES[preference], value);
},
[preferences]
);
return useMemo(() => ({ data, set, toggle }), [data, set, toggle]);
}
That’s it. ~40 lines of TypeScript for a fully functional, type-safe preferences system.
What Makes This So Good?
1. Zero Backend Code Required
The @wordpress/preferences package handles all the REST API communication for you. It uses the existing WordPress user meta infrastructure under the hood. No custom endpoints, no permission callbacks, no sanitization logic needed on the server.
2. Built-in Optimistic Updates
When you call preferences.set() or preferences.toggle(), the UI updates immediately. The package handles the async save to the server in the background. If the save fails, it handles that too. You don’t have to think about it.
3. Dead Simple API
Reading a preference:
const value = select(preferencesStore).get('my-namespace', 'preference-name');
Setting a preference:
dispatch(preferencesStore).set('my-namespace', 'preference-name', value);
Toggling a boolean:
dispatch(preferencesStore).toggle('my-namespace', 'preference-name');
That toggle() method alone saves you from writing set(key, !currentValue) every time you need to flip a boolean flag.
4. Namespaced by Design
Each plugin gets its own namespace, so there’s no collision with other plugins or core WordPress preferences. In the example above, we use jetpack/social as our namespace.
5. Battle-Tested in Core
This isn’t some experimental package. WordPress core uses it for the pre-publish panel preferences (you know, that “Always show pre-publish checks” toggle). If it’s stable enough for millions of WordPress sites, it’s stable enough for your plugin.
Usage in Components
Here’s how clean the consuming code looks:
function MyComponent() {
const { data, toggle } = useSocialUserPreferences();
return (
<ToggleControl
label="Show confirmation before publishing"
checked={data.prePublishConfirmation ?? true}
onChange={() => toggle('prePublishConfirmation')}
/>
);
}
No loading states. No error handling. No useEffect for fetching. The preferences store integrates with WordPress’s data layer, so the values are available as soon as the editor loads.
When to Use It
@wordpress/preferences is perfect for:
- User-specific UI preferences (collapsed panels, dismissed notices)
- Feature toggles that persist across sessions
- “Don’t show this again” flags
- Editor behavior customizations
It’s not ideal for:
- Site-wide settings (use Options API instead)
- Content or metadata (use post meta)
- Complex data structures (stick to simple values)
Conclusion
Next time you reach for register_rest_route() just to save a user preference, stop. The @wordpress/preferences package gives you:
- No backend code
- Optimistic updates out of the box
- A simple, clean API
- Built-in toggle support for booleans
- The same infrastructure WordPress core uses
It’s one of those packages that makes you wonder why you ever did it the hard way.