Source: data-engine-js-components-web/src/main/resources/META-INF/resources/js/core/hooks/useForm.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 {useThunk} from '@liferay/frontend-js-react-web';
import React, {useCallback, useContext, useReducer} from 'react';

import {createReducer} from '../reducers/createReducer.es';
import {useConfig} from './useConfig.es';
import {useDataView} from './useDataView.es';

const FormDispatchContext = React.createContext(() => {});

FormDispatchContext.displayName = 'FormDispatchContext';

const FormStateContext = React.createContext({editingLanguageId: 'en_US'});

FormStateContext.displayName = 'FormStateContext';

/**
 * This is a no-op implementation for the store, a dispatch call will
 * not call any action on the FormProvider and the state will always
 * be the value passed through the value property.
 *
 * This is a temporary implementation that works as an intermediate
 * provider and compatibility layer for the React and Metal.js frontier
 * in FormBuilder. The one who actually causes an action is the
 * LayoutProvider when that component is used.
 *
 * <LayoutProvider> -> Metal.js
 *  <FormBuilder> -> Metal.js
 *   <FormNoop> -> React.js with Adapter
 *    <FormNoopProvider> -> React.js
 *     ...
 *    </FormNoopProvider>
 *   </FormNoop>
 *  </FormBuilder>
 * </LayoutProvider>
 */
export function FormNoopProvider({children, initialState, onAction, value}) {
	const [, dispatch] = useThunk([{}, onAction]);

	return (
		<FormDispatchContext.Provider value={dispatch}>
			<FormStateContext.Provider value={{...initialState, ...value}}>
				{children}
			</FormStateContext.Provider>
		</FormDispatchContext.Provider>
	);
}

/**
 * Propagate Action is used in conjunction with useReducer that "listens"
 * for any action in a dispatch call.
 *
 * const [state, dispatch] = usePropagateAction(useReducer(...));
 */
const usePropagateAction = ([state, dispatch], onAction) => {
	const callBack = useCallback(
		(action) => {
			dispatch(action);
			onAction?.(action);
		},
		[dispatch, onAction]
	);

	return [state, callBack];
};

/**
 * Form Provider is a "store" that is compatible with the concept of
 * thunk but has no coupling to any specific reducer, the reducers
 * can be configured through the property, as well as configuring
 * the data init function and the initial state.
 *
 * <FormProvider
 *  reducers={[dragAndDropReducer, fieldEditableReducer]}
 *  value={state}
 * >
 * 	...
 * </FormProvider>
 *
 * Reducers receive the extra config property in addition to state
 * and action, it is mandatory that ConfigProvider be declared
 * before FormProvider.
 *
 * <ConfigProvider>
 *  <FormProvider>
 *   ...
 *  </FormProvider>
 * </ConfigProvider>
 *
 * FormProvider is built to be composed and can be declared more
 * than once in the application to deal with the same structure in
 * some part of the application tree. The real example is Sidebar,
 * which has the same `pages` structure that needs to be manipulated
 * but is isolated from the application and the main store.
 *
 * <ConfigProvider>
 *  <FormProvider> <- Main Store
 *   <FormBuilder>
 *    <Pages/>
 *    <Sidebar>
 *     <FormProvider> <- Isolated state
 *      <Pages />
 *     </FormProvider>
 *    </Sidebar>
 *   </FormBuilder>
 *  </FormProvider>
 * </ConfigProvider>
 */
export function FormProvider({
	children,
	init = (props) => props,
	initialState = {},
	onAction,
	reducers,
	value,
}) {
	const config = useConfig();

	const [state, dispatch] = useThunk(
		usePropagateAction(
			useReducer(
				createReducer(reducers, config),
				{...initialState, ...value},
				(props) => init(props, config)
			),
			onAction
		)
	);

	return (
		<FormDispatchContext.Provider value={dispatch}>
			<FormStateContext.Provider value={state}>
				{typeof children === 'function' ? children(state) : children}
			</FormStateContext.Provider>
		</FormDispatchContext.Provider>
	);
}

FormProvider.displayName = 'FormProvider';

export function useForm() {
	return useContext(FormDispatchContext);
}

export function useFormState({schema} = {}) {
	return useDataView(useContext(FormStateContext), schema);
}