Source: layout-content-page-editor-web/src/main/resources/META-INF/resources/js/store/ConnectedComponent.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import {Config} from 'metal-state';
import React, {useContext, useEffect, useState} from 'react';

import StoreContext from './StoreContext.es';
import INITIAL_STATE from './state.es';
import {connect, disconnect, Store} from './store.es';

/**
 * HOC that returns a component that connects automatically
 * to a Store parameter.
 * @param {Component} Component
 * @param {string[]} [properties=[]] List of properties to be fetched from store
 * @return {Component}
 */
const getConnectedComponent = (Component, properties) => {
	/**
	 * ConnectedComponent
	 */
	class ConnectedComponent extends Component {
		/**
		 * @inheritdoc
		 * @param {object} props
		 * @param  {...any} ...args
		 */
		constructor(props, ...args) {
			super(props, ...args);

			this.on('storeChanged', change => {
				const newStore = change.newVal;
				const prevStore = change.prevVal;

				if (newStore !== prevStore) {
					disconnect(this);

					if (newStore instanceof Store) {
						connect(
							this,
							newStore
						);
					}
				}
			});

			this.on('disposed', () => {
				disconnect(this);
			});

			if (props.store instanceof Store) {
				connect(
					this,
					props.store
				);
			}
		}
	}

	/**
	 * Connected component state
	 */
	ConnectedComponent.STATE = {
		store: Config.instanceOf(Store).value(null)
	};

	properties.forEach(property => {
		try {
			ConnectedComponent.STATE[property] = INITIAL_STATE[
				property
			].internal();
		} catch (e) {
			throw new Error(
				`${property} is not available from ${Component.name}`
			);
		}
	});

	return ConnectedComponent;
};

/**
 * Second order function to produce a Connected Component Wrapper
 * @param {Function} mapStateToProps - Recieves the state and returns mapped version of it ready for consumption by the Wrapped Component
 * @param {Function} mapDispatchToProps - Recieves the dispatch and returns a set of component props that use it to call the modify the state tree
 * @returns {object}
 */
function getConnectedReactComponent(mapStateToProps, mapDispatchToProps) {
	/**
	 *
	 * @param {React.Component} WrappedComponent - The Component to connect to the store
	 * @returns {React.Component} - Wrapper Connected Component that propagates every store change mapped to the component
	 */
	return function _getConnectedWrapperComponent(WrappedComponent) {
		return function ConnectedWrapperComponent(props) {
			const store = useContext(StoreContext);

			const [storeState, setStoreState] = useState(
				store ? store.getState() : {}
			);

			useEffect(() => {
				if (store) {
					const handleStoreChange = () =>
						setStoreState(store.getState());
					const subscriber = store.on('change', handleStoreChange);

					return () => subscriber.removeListener();
				}
			}, [store]);

			return store ? (
				<WrappedComponent
					{...props}
					{...mapStateToProps(storeState, props)}
					{...mapDispatchToProps(store.dispatch, props)}
				/>
			) : null;
		};
	};
}

export {getConnectedComponent, getConnectedReactComponent};
export default getConnectedComponent;