</svg>
</button></figure>
Since writing CSS is outside the overall scope of this article, refer to the GitHub repository to see how it was implemented.
The settings page is rendered, however it’s still using hardcoded data that’s passed to the DataForm, and the onChange is not yet implemented:
const SettingsPage = () => {
const {
// ...
} = useSettings();
const data = {
message: '',
display: false,
size: 'small',
};
// ...
return (
<>
<SettingsTitle />
<Notices />
<DataForm
data={ data }
fields={ fields }
form={ form }
onChange={ () => {} }
/>
<SaveButton onClick={ saveSettings } />
</>
);
};
The final step is to connect the DataForm with the useSettings custom hook, which handles state management.
Refactoring the useSettings hook
Currently, the state of fields is stored separately using three useState calls, but the actual settings are fetched and saved as an object under the unadorned_announcement_bar key.
As a reference, here’s the code for the current custom hook in /src/hooks/use-settings.js:
const useSettings = () => {
const [ message, setMessage ] = useState();
const [ display, setDisplay ] = useState();
const [ size, setSize ] = useState();
const { createSuccessNotice } = useDispatch( noticesStore );
useEffect( () => {
apiFetch( { path: '/wp/v2/settings' } ).then( ( wpSettings ) => {
setMessage( wpSettings.unadorned_announcement_bar.message );
setDisplay( wpSettings.unadorned_announcement_bar.display );
setSize( wpSettings.unadorned_announcement_bar.size );
} );
}, [] );
const saveSettings = () => {
apiFetch( {
path: '/wp/v2/settings',
method: 'POST',
data: {
unadorned_announcement_bar: {
message,
display,
size,
},
},
} ).then( () => {
createSuccessNotice(
__( 'Settings saved.', 'unadorned-announcement-bar' )
);
} );
};
return {
message,
setMessage,
display,
setDisplay,
size,
setSize,
saveSettings,
};
};
The setting is registered on the server side and has some default values and its schema defined. For reference, here’s how it was registered:
function unadorned_announcement_bar_settings() {
$default = array(
'message' => __( 'Hello, World!', 'unadorned-announcement-bar' ),
'display' => true,
'size' => 'medium',
);
$schema = array(
'type' => 'object',
'properties' => array(
'message' => array(
'type' => 'string',
),
'display' => array(
'type' => 'boolean',
),
'size' => array(
'type' => 'string',
'enum' => array(
'small',
'medium',
'large',
'x-large',
),
),
),
);
register_setting(
'options',
'unadorned_announcement_bar',
array(
'type' => 'object',
'default' => $default,
'show_in_rest' => array(
'schema' => $schema,
),
)
);
}
add_action( 'init', 'unadorned_announcement_bar_settings' );
Since there are no longer individual components for the fields and the DataForm uses an object for the data prop, it’s more straightforward to store the field state as an object too. This will also match what’s actually stored in the database.
First replace the three separate state variables in use-settings.js:
const useSettings = () => {
const [ settings, setSettings ] = useState( {
message: '',
display: false,
size: 'small',
} );
// ...
return [ settings, setSettings, saveSettings ];
};
Since the data stored in the options table matches the state structure exactly, you can simplify both reading and saving.
For the initial data loading, update the success handler inside the useEffect hook:
const useSettings = () => {
// ...
useEffect( () => {
apiFetch( { path: '/wp/v2/settings' } ).then( ( wpSettings ) => {
setSettings( wpSettings.unadorned_announcement_bar )
} );
}, [] );
// ...
};
For saving the settings, update the saveSettings as follows:
const useSettings = () => {
// ...
const saveSettings = () => {
apiFetch( {
path: '/wp/v2/settings',
method: 'POST',
data: {
unadorned_announcement_bar: settings
},
} ).then( () => {
// ...
} );
};
// ...
};
Putting it all together, here’s how the useSettings should look after the previous updates:
const useSettings = () => {
const [settings, setSettings] = useState({
message: "",
display: false,
size: "small",
});
const { createSuccessNotice } = useDispatch(noticesStore);
useEffect(() => {
apiFetch({ path: "/wp/v2/settings" }).then((wpSettings) => {
setSettings(wpSettings.unadorned_announcement_bar);
});
}, []);
const saveSettings = () => {
apiFetch({
path: "/wp/v2/settings",
method: "POST",
data: {
unadorned_announcement_bar: settings,
},
}).then(() => {
createSuccessNotice(
__("Settings saved.", "unadorned-announcement-bar"),
);
});
};
return {
settings,
setSettings,
saveSettings,
};
};
Now that useSettings returns the state as a single object, update the SettingsPage component.
Change how you destructure the hook, remove the data variable, and pass settings to DataForm.
After the changes, the code in /src/components/settings-page.jsx should look like this:
const SettingsPage = () => {
const [ settings, setSettings, saveSettings ] = useSettings();
const fields = [
// ...
]
const form = {
// ..
}
return (
<>
<SettingsTitle />
<Notices />
<DataForm
data={ settings }
fields={ fields }
form={ form }
onChange={ () => {} }
/>
<SaveButton onClick={ saveSettings } />
</>
);
};
Implementing the onChange callback
The last part is updating the internal state (settings) whenever a field is updated. For this, you have to implement the onChange callback.
The onChange callback only receives the changed data (the “edits”), not the complete state. For example, when you update the message field, onChange will receive { message: 'updated value' }.
This means you can’t simply pass the changes to setSettings, as it would replace the entire state with just the changed field. Instead, you have to merge the current state with the changes.
Here’s how the onChange callback should look after the update:
const SettingsPage = () => {
// ...
return (
<>
<SettingsTitle />
<Notices />
<DataForm
data={ settings }
fields={ fields }
form={ form }
onChange={ ( edits ) =>
setSettings( ( current ) => ( {
...current,
...edits,
} ) )
}
/>
<SaveButton onClick={ saveSettings } />
</>
);
};
Essentially, the spread operator (...) first copies all properties from the current state, then overwrites any properties that exist in edits. This ensures unchanged fields keep their values while changed fields get updated.
And with this, you successfully refactored the settings page using the DataForm component and configuration driven approach!
Wrapping up
Try it yourself
You can check the final result in the GitHub repository to see the entire code together, or you can compare the two solutions.
If you want to try it out without downloading and installing it, you can do that using WordPress Playground.
Where to go from here
If you’re excited about DataForm, there are some other areas you can explore on your own.
For example, you can look into the validation feature that allows you to do client-side validation and define which fields are required, among other things.
You can also look into how to load the elements asynchronously instead of having them hardcoded. This would allow you to dynamically load the options of select fields from the database.
It’s worth checking the Storybook for the DataForm component, as it has interactive demos for all the layout types in various combinations.
Lastly, to get up to speed with the latest changes the DataViews, DataForm, et al. in WordPress 6.9 is a good starting point. For specific details, check the package documentation page.
Props to @psykro and @areziaal for their reviews, and to @juanmaguitar for ongoing education about this topic.