Skip to main content

Create a settings page in WordPress with React – Part 3

Published ago
Updated ago
15 min read

During the second part of this tutorial to create a settings page in WordPress, I showed how to create a small React application that is displayed within our settings page, added a couple of fields and styles; and prepared the save button to work. Now what remains is to save the data inside our database and read it within our settings page to make sure we are not overwriting the data.

Table of Contents

Step 1: Saving our data

Starting from where we left off last time, fortunately WordPress already comes with its utilities to make our lives easier, and, if we remember that in the second part we registered our option using register_setting, we can use the ID we gave it to load the saved options in our database like this:

1/**
2 * WordPress dependencies
3 */
4import {
5 Button,
6 Card,
7 CardBody,
8 CardDivider,
9 CardFooter,
10 TextControl,
11 ToggleControl,
12} from '@wordpress/components';
13import { useEntityProp } from '@wordpress/core-data';
14import { __ } from '@wordpress/i18n';
15
16const Settings = () => {
17 const [ apiKey, setApiKey ] = useEntityProp(
18 'root',
19 'site
20 'mah_api_key'
21 );
22 const [ toggled, setToggled ] = useEntityProp(
23 'root',
24 'site',
25 'mah_function'
26 );
27 const saveSettings = () => {
28 console.log( 'Guardar' );
29 };
30
31 return (
32 <>
33 <h1>{ __( 'Select the desired options', 'mah-settings' ) }</h1>
34
35 <Card>
36 <CardBody>
37 <TextControl
38 help={ __( 'Insert your API key', 'mah-settings' ) }
39 label={ __( 'API Key', 'mah-settings' ) }
40 onChange={ setApiKey }
41 value={ apiKey || '' }
42 />
43 </CardBody>
44
45 <CardDivider />
46
47 <CardBody>
48 <ToggleControl
49 label={ __( 'Activate functionality', 'mah-settings' ) }
50 onChange={ setToggled }
51 checked={ toggled || false }
52 />
53 </CardBody>
54
55 <CardFooter>
56 <Button
57 disabled={ ! apiKey }
58 onClick={ saveSettings }
59 variant="primary"
60 >
61 { __( 'Save', 'mah-settings' ) }
62 </Button>
63 </CardFooter>
64 </Card>
65 </>
66 );
67}
68export default Settings;
69

With this, we are reading the value of our option saved earlier, let's explain a little how it works:

  • We load the useEntityProp hook that helps us read properties either from the site or from specific posts
  • This is evident in line 18 where we replace useState with useEntityProp, the hook keeps track of our value to maintain a record while it is saved
  • Finally, we add a condition to the value of our TextControl where we basically say that if this value is empty (null) then return an empty string instead ('')
  • We do the same with our Toggle, this is to maintain the same type of data even when nothing is saved earlier

Now we need to save our data once the save button is pressed, for this we need to replace the saveSettings function with a call to the functions available in the @wordpress/core-data, and @wordpress/data packages

Warning

What is the difference?

If the question is why some functions are in core-data while others are in data? unfortunately I don't have the answer, we only know that the team that added them assumed that those were the best places to put them 🤷‍♂

1/**
2 * WordPress dependencies
3 */
4import {
5 Button,
6 Card,
7 CardBody,
8 CardDivider,
9 CardFooter,
10 TextControl,
11 ToggleControl,
12} from '@wordpress/components';
13import { store as coreStore, useEntityProp } from '@wordpress/core-data';
14import { useDispatch } from '@wordpress/data';
15import { __ } from '@wordpress/i18n';
16
17const Settings = () => {
18 const [ apiKey, setApiKey ] = useEntityProp(
19 'root',
20 'site
21 'mah_api_key'
22 );
23 const [ toggled, setToggled ] = useEntityProp(
24 'root',
25 'site',
26 'mah_function'
27 );
28 const { saveEditedEntityRecord } = useDispatch( coreStore );
29 const saveSettings = () => {
30 saveEditedEntityRecord(
31 'root',
32 'site',
33 undefined,
34 {
35 mah_api_key: apiKey,
36 mah_function: toggled,
37 }
38 );
39 };
40[]
41

I have added only the important parts of our modifications, to understand a little more

  • We must import store, and give it the alias coreStore (this is a good practice when working with several) where the function we need is stored1
  • We load the saveEditedEntityRecord option from the store using the useDispatch hook
  • We replace our save function so that it saves the content of our options with the function we just loaded

And that's it, now once we make changes to our controls we can see how their value persists when reloading the page, and we can use them throughout our site:

Saving data in WordPress

Step 2: Improving the experience

Although it is already saving our data, we are not finished yet, right? Can you see how easily we have to improve the experience?

Step 2.1: Loading indicator (Spinner)

The most logical thing I can think of: we don't know when or how our options have been saved. There is no indicator that tells us that the button worked until we reload the page and see them being loaded into the fields, so we can start with a simple Spinner.

With a few modifications and with the help of the @wordpress/components package again we can easily place it:

src/components/settings.js

1/**
2 * WordPress dependencies
3 */
4import {
5 Button,
6 Card,
7 CardBody,
8 CardDivider,
9 CardFooter,
10 Spinner,
11 TextControl,
12 ToggleControl,
13} from '@wordpress/components';
14import { store as coreStore, useEntityProp } from '@wordpress/core-data';
15import { useDispatch } from '@wordpress/data';
16import { useState } from '@wordpress/element';
17import { __ } from '@wordpress/i18n';
18
19const Settings = () => {
20 const [ isLoading, setIsLoading ] = useState( false );
21 const [ apiKey, setApiKey ] = useEntityProp(
22 'root',
23 'site',
24 'mah_api_key'
25 );
26 const [ toggled, setToggled ] = useEntityProp(
27 'root',
28 'site',
29 'mah_function'
30 );
31 const { saveEditedEntityRecord } = useDispatch( coreStore );
32 const saveSettings = async () => {
33 setIsLoading( true );
34 await saveEditedEntityRecord(
35 'root',
36 'site',
37 undefined,
38 {
39 mah_api_key: apiKey,
40 mah_function: toggled,
41 }
42 );
43 setIsLoading( false );
44 };
45[]
46

src/components/settings.js#49-85

1return (
2 <>
3 <h1>{__('Select the desired options', 'mah-settings')}</h1>
4
5 <Card>
6 <CardBody>
7 <TextControl
8 help={__('Insert your API key', 'mah-settings')}
9 label={__('API Key', 'mah-settings')}
10 onChange={setApiKey}
11 value={apiKey || ''}
12 />
13 </CardBody>
14
15 <CardDivider />
16
17 <CardBody>
18 <ToggleControl
19 label={__('Activate functionality', 'mah-settings')}
20 onChange={setToggled}
21 checked={toggled || false}
22 />
23 </CardBody>
24
25 <CardFooter>
26 <Button disabled={!apiKey} onClick={saveSettings} variant='primary'>
27 {isLoading && <Spinner />}
28 {!isLoading && __('Save', 'mah-settings')}
29 </Button>
30 </CardFooter>
31 </Card>
32 </>
33);
34

And the result would be:

Spinner in WordPress

Let's see what we did:

  1. We imported Spinner from our WordPress components, this is what will give us the loading indicator
  2. We brought back useState, we will use it to create a loading flag, which is disabled by default
  3. We added the async condition to our save function, this tells the browser that an await is coming
  4. We changed our loading flag to enabled (this will show the Spinner), and we tell the browser to wait for the result of the function with await, before disabling our flag again
  5. Finally, we indicate the conditions so that if the flag is enabled show our spinner, otherwise, show our text
Note

Extra!

In addition to our spinner, it is often good to disable the button so that it cannot be used while our function loads. Can you figure out how to do this?

Tip: It will be included in the other examples and the repository at the end of the article.

Step 2.2: Success (or error) message

Another simple thing we can do is provide a success message (or error) when the process is finished, this will be a little different from what the WordPress editor does by default2, but we still have a component we can use.

We make some modifications to the first part of our file:

1/**
2 * WordPress dependencies
3 */
4import {
5 Button,
6 Card,
7 CardBody,
8 CardDivider,
9 CardFooter,
10 Notice,
11 Spinner,
12 TextControl,
13 ToggleControl,
14} from '@wordpress/components';
15import { store as coreStore, useEntityProp } from '@wordpress/core-data';
16import { useDispatch } from '@wordpress/data';
17import { useState } from '@wordpress/element';
18import { __ } from '@wordpress/i18n';
19
20const Settings = () => {
21 const [ isLoading, setIsLoading ] = useState( false );
22 const [ showNotice, setShowNotice ] = useState( false );
23 const [ apiKey, setApiKey ] = useEntityProp(
24 'root',
25 'site',
26 'mah_api_key'
27 );
28 const [ toggled, setToggled ] = useEntityProp(
29 'root',
30 'site',
31 'mah_function'
32 );
33 const { saveEditedEntityRecord } = useDispatch( coreStore );
34 const saveSettings = async () => {
35 setIsLoading( true );
36 await saveEditedEntityRecord(
37 'root',
38 'site',
39 undefined,
40 {
41 mah_api_key: apiKey,
42 mah_function: toggled,
43 }
44 );
45 setShowNotice( true );
46 setIsLoading( false );
47 };
48
49 return (
50 <>
51 <h1>{ __( 'Select the desired options', 'mah-settings' ) }</h1>
52
53 <Card>
54 { showNotice && (
55 <CardBody>
56 <Notice
57 isDismissible
58 onDismiss={ () => setShowNotice( false ) }
59 status="success"
60 >
61 { __( 'Saved Succesfully', 'mah-settings' ) }
62 </Notice>
63 </CardBody>
64 ) }
65[…]
66

Basically what we are doing is:

  1. Import the Notice component from @wordpress/components
  2. Create a flag that helps us know when to show the message using useState again
  3. When the save process is finished, we enable the flag to show the message
  4. Above our options, prepare the place where the message will be shown and specify that it should only be shown when the flag is enabled
  5. Inside the Notice component we specify what the message will be and add a function that disables our flag when the close button is clicked

And now when we save our options, we will have a nice success message like this:

Success message in WordPress
Caution

What if something goes wrong?

In this example, I have only added a success message, but, in case we also want to know if something has failed (recommended) we could log it with the following modifications.

  • Create a state that saves the message we need, as well as its status
  • Replace our async await function for a promise of format .then().catch()
  • Save the error message in our variable inside the catch function
  • Show our message changing the status depending on whether it was success or error

Conclusion

I think our options page is now in a good place, I didn't expect this tutorial to be so long but I hope it was useful for someone. Finally, we managed to create our options page, to recap, this is what we have done so far:

  • We created our environment, and our plugin
  • We created our options page, and registered a script that runs only on it
  • We registered our global options, and added our controls in React
  • We saved our options in React, and added UX improvements

Finally, if you want to see the complete project you can do it in my repository on Github. Una vez más, todos los comentarios son bienvenidos y cualquier sugerencia sobre qué escribir después también. ¡Hasta la próxima!

Read more…

Footnotes

  1. Stores are where the application logic lives, in a Redux format; it is where the application state and reducers are stored.

  2. Inside the editor we would use createNotice that would show our message along with the others in the editor, but here there is no editor so we have to improvise.