Add data store boilerplate

This commit is contained in:
Emili Castells Guasch 2025-01-29 17:57:50 +01:00
parent 442a33778f
commit 0e89cde688
12 changed files with 411 additions and 2 deletions

View file

@ -5,8 +5,17 @@ import * as Payment from './payment';
import * as Settings from './settings'; import * as Settings from './settings';
import * as Styling from './styling'; import * as Styling from './styling';
import * as Todos from './todos'; import * as Todos from './todos';
import * as PayLaterMessaging from './pay-later-messaging';
const stores = [ Onboarding, Common, Payment, Settings, Styling, Todos ]; const stores = [
Onboarding,
Common,
Payment,
Settings,
Styling,
Todos,
PayLaterMessaging,
];
stores.forEach( ( store ) => { stores.forEach( ( store ) => {
try { try {
@ -30,6 +39,7 @@ export const PaymentHooks = Payment.hooks;
export const SettingsHooks = Settings.hooks; export const SettingsHooks = Settings.hooks;
export const StylingHooks = Styling.hooks; export const StylingHooks = Styling.hooks;
export const TodosHooks = Todos.hooks; export const TodosHooks = Todos.hooks;
export const PayLaterMessagingHooks = PayLaterMessaging.hooks;
export const OnboardingStoreName = Onboarding.STORE_NAME; export const OnboardingStoreName = Onboarding.STORE_NAME;
export const CommonStoreName = Common.STORE_NAME; export const CommonStoreName = Common.STORE_NAME;
@ -37,6 +47,7 @@ export const PaymentStoreName = Payment.STORE_NAME;
export const SettingsStoreName = Settings.STORE_NAME; export const SettingsStoreName = Settings.STORE_NAME;
export const StylingStoreName = Styling.STORE_NAME; export const StylingStoreName = Styling.STORE_NAME;
export const TodosStoreName = Todos.STORE_NAME; export const TodosStoreName = Todos.STORE_NAME;
export const PayLaterMessagingStoreName = PayLaterMessaging.STORE_NAME;
export * from './configuration'; export * from './configuration';

View file

@ -0,0 +1,45 @@
# Store template
This template contains all files for a Redux store.
## New Store: Redux integration
1. Copy this folder, give it a correct name.
2. Check each file for `<UNKNOWN>` placeholders and `TODO` remarks.
3. Edit the main store-index file and add the relevant store integration there.
4. Check the debug-module, and add relevant debug code.
- Register the store in the `reset()` method.
---
Main store-index:
`modules/ppcp-settings/resources/js/data/index.js`
Sample store integration:
```js
import * as YourStore from './yourStore';
// ...
YourStore.initStore();
// ...
export const YourStoreHooks = YourStore.hooks;
// ...
export const YourStoreName = YourStore.STORE_NAME;
// ...
addDebugTools( window.ppcpSettings, [ ..., YourStoreName ] );
```
---
### New Store: PHP integration
1. Create the **REST endpoint** for hydrating and persisting data.
- `modules/ppcp-settings/src/Endpoint/YourStoreRestEndpoint.php`
- Extend from base class `RestEndpoint`
2. Create the **data model** class to manage the DB interaction.
- `modules/ppcp-settings/src/Data/YourStoreSettings.php`
- Extend from base class `AbstractDataModel`
3. Create relevant **DI services** for both files.
- `modules/ppcp-settings/services.php`
4. Register the REST endpoint in the **service module**.
- `modules/ppcp-settings/src/SettingsModule.php`
- Find the action `rest_api_init`

View file

@ -0,0 +1,18 @@
/**
* Action Types: Define unique identifiers for actions across all store modules.
*
* @file
*/
export default {
// Transient data.
SET_TRANSIENT: 'PAY_LATER_MESSAGING:SET_TRANSIENT',
// Persistent data.
SET_PERSISTENT: 'PAY_LATER_MESSAGING:SET_PERSISTENT',
RESET: 'PAY_LATER_MESSAGING:RESET',
HYDRATE: 'PAY_LATER_MESSAGING:HYDRATE',
// Controls - always start with "DO_".
DO_PERSIST_DATA: 'PAY_LATER_MESSAGING:DO_PERSIST_DATA',
};

View file

@ -0,0 +1,80 @@
/**
* Action Creators: Define functions to create action objects.
*
* These functions update state or trigger side effects (e.g., async operations).
* Actions are categorized as Transient, Persistent, or Side effect.
*
* @file
*/
import { select } from '@wordpress/data';
import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants';
/**
* @typedef {Object} Action An action object that is handled by a reducer or control.
* @property {string} type - The action type.
* @property {Object?} payload - Optional payload for the action.
*/
/**
* Special. Resets all values in the store to initial defaults.
*
* @return {Action} The action.
*/
export const reset = () => ( { type: ACTION_TYPES.RESET } );
/**
* Persistent. Set the full store details during app initialization.
*
* @param {{data: {}, flags?: {}}} payload
* @return {Action} The action.
*/
export const hydrate = ( payload ) => ( {
type: ACTION_TYPES.HYDRATE,
payload,
} );
/**
* Generic transient-data updater.
*
* @param {string} prop Name of the property to update.
* @param {any} value The new value of the property.
* @return {Action} The action.
*/
export const setTransient = ( prop, value ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { [ prop ]: value },
} );
/**
* Generic persistent-data updater.
*
* @param {string} prop Name of the property to update.
* @param {any} value The new value of the property.
* @return {Action} The action.
*/
export const setPersistent = ( prop, value ) => ( {
type: ACTION_TYPES.SET_PERSISTENT,
payload: { [ prop ]: value },
} );
/**
* Transient. Marks the store as "ready", i.e., fully initialized.
*
* @param {boolean} isReady
* @return {Action} The action.
*/
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
/**
* Side effect. Triggers the persistence of store data to the server.
*
* @return {Action} The action.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};

View file

@ -0,0 +1,28 @@
/**
* Name of the Redux store module.
*
* Used by: Reducer, Selector, Index
*
* @type {string}
*/
export const STORE_NAME = 'wc/paypal/pay_later_messaging';
/**
* REST path to hydrate data of this module by loading data from the WP DB.
*
* Used by: Resolvers
* See: PayLaterMessagingEndpoint.php
*
* @type {string}
*/
export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/pay_later_messaging';
/**
* REST path to persist data of this module to the WP DB.
*
* Used by: Controls
* See: PayLaterMessagingEndpoint.php
*
* @type {string}
*/
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/pay_later_messaging';

View file

@ -0,0 +1,23 @@
/**
* Controls: Implement side effects, typically asynchronous operations.
*
* Controls use ACTION_TYPES keys as identifiers.
* They are triggered by corresponding actions and handle external interactions.
*
* @file
*/
import apiFetch from '@wordpress/api-fetch';
import { REST_PERSIST_PATH } from './constants';
import ACTION_TYPES from './action-types';
export const controls = {
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
},
};

View file

@ -0,0 +1,50 @@
/**
* Hooks: Provide the main API for components to interact with the store.
*
* These encapsulate store interactions, offering a consistent interface.
* Hooks simplify data access and manipulation for components.
*
* @file
*/
import { useDispatch } from '@wordpress/data';
import { createHooksForStore } from '../utils';
import { STORE_NAME } from './constants';
const useHooks = () => {
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
const { persist } = useDispatch( STORE_NAME );
// Read-only flags and derived state.
// Nothing here yet.
// Transient accessors.
const [ isReady ] = useTransient( 'isReady' );
// Persistent accessors.
// TODO: Replace with real property.
const [ sampleValue, setSampleValue ] = usePersistent( 'sampleValue' );
return {
persist,
isReady,
sampleValue,
setSampleValue,
};
};
export const useState = () => {
const { persist, isReady } = useHooks();
return { persist, isReady };
};
// TODO: Replace with real hook.
export const useSampleValue = () => {
const { sampleValue, setSampleValue } = useHooks();
return {
sampleValue,
setSampleValue,
};
};

View file

@ -0,0 +1,32 @@
import { createReduxStore, register } from '@wordpress/data';
import { controls as wpControls } from '@wordpress/data-controls';
import { STORE_NAME } from './constants';
import reducer from './reducer';
import * as selectors from './selectors';
import * as actions from './actions';
import * as hooks from './hooks';
import { resolvers } from './resolvers';
import { controls } from './controls';
/**
* Initializes and registers the settings store with WordPress data layer.
* Combines custom controls with WordPress data controls.
*
* @return {boolean} True if initialization succeeded, false otherwise.
*/
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,
} );
register( store );
return Boolean( wp.data.select( STORE_NAME ) );
};
export { hooks, selectors, STORE_NAME };

View file

@ -0,0 +1,62 @@
/**
* Reducer: Defines store structure and state updates for this module.
*
* Manages both transient (temporary) and persistent (saved) state.
* The initial state must define all properties, as dynamic additions are not supported.
*
* @file
*/
import { createReducer, createReducerSetters } from '../utils';
import ACTION_TYPES from './action-types';
// Store structure.
// Transient: Values that are _not_ saved to the DB (like app lifecycle-flags).
const defaultTransient = Object.freeze( {
isReady: false,
} );
// Persistent: Values that are loaded from the DB.
const defaultPersistent = Object.freeze( {
// TODO: Add real DB properties here.
sampleValue: 'foo',
cart: {},
checkout: {},
product: {},
shop: {},
home: {},
custom_placement: [],
} );
// Reducer logic.
const [ changeTransient, changePersistent ] = createReducerSetters(
defaultTransient,
defaultPersistent
);
const reducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
changeTransient( state, payload ),
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) =>
changePersistent( state, payload ),
[ ACTION_TYPES.RESET ]: ( state ) => {
const cleanState = changeTransient(
changePersistent( state, defaultPersistent ),
defaultTransient
);
// Keep "read-only" details and initialization flags.
cleanState.isReady = true;
return cleanState;
},
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
changePersistent( state, payload.data ),
} );
export default reducer;

View file

@ -0,0 +1,39 @@
/**
* Resolvers: Handle asynchronous data fetching for the store.
*
* These functions update store state with data from external sources.
* Each resolver corresponds to a specific selector (selector with same name must exist).
* Resolvers are called automatically when selectors request unavailable data.
*
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
export const resolvers = {
/**
* Retrieve settings from the site's REST API.
*/
*persistentData() {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
console.log( result );
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );
} catch ( e ) {
yield dispatch( 'core/notices' ).createErrorNotice(
// TODO: Add the module name to the error message.
__(
'Error retrieving Pay Later Messaging config details.',
'woocommerce-paypal-payments'
)
);
}
},
};

View file

@ -0,0 +1,21 @@
/**
* Selectors: Extract specific pieces of state from the store.
*
* These functions provide a consistent interface for accessing store data.
* They allow components to retrieve data without knowing the store structure.
*
* @file
*/
const EMPTY_OBJ = Object.freeze( {} );
const getState = ( state ) => state || EMPTY_OBJ;
export const persistentData = ( state ) => {
return getState( state ).data || EMPTY_OBJ;
};
export const transientData = ( state ) => {
const { data, ...transientState } = getState( state );
return transientState || EMPTY_OBJ;
};

View file

@ -100,7 +100,7 @@ class PayLaterMessagingEndpoint extends RestEndpoint {
* *
* @param WP_REST_Request $request Full data about the request. * @param WP_REST_Request $request Full data about the request.
* *
* @return WP_REST_Response The updated payment methods details. * @return WP_REST_Response The updated Pay Later Messaging configuration details.
*/ */
public function update_details( WP_REST_Request $request ) : WP_REST_Response { public function update_details( WP_REST_Request $request ) : WP_REST_Response {
$this->save_config->save_config( $request->get_params() ); $this->save_config->save_config( $request->get_params() );