remirror/remirror

View on GitHub
packages/remirror__react-core/src/hooks/use-remirror.tsx

Summary

Maintainability
A
0 mins
Test Coverage
B
81%
import { useCallback, useMemo, useState } from 'react';
import {
  AnyExtension,
  CreateEditorStateProps,
  EditorState,
  InvalidContentHandler,
  RemirrorEventListener,
  RemirrorManager,
  StringHandler,
} from '@remirror/core';

import type {
  CreateReactManagerOptions,
  ReactExtensions,
  ReactFrameworkOutput,
} from '../react-types';
import { useManager } from './use-manager';

/**
 * Props which are passed into the `useRemirror` hook.
 */
export interface UseRemirrorProps<Extension extends AnyExtension>
  extends CreateReactManagerOptions,
    Partial<CreateEditorStateProps> {
  /**
   * Provide a function that returns an array of extensions which will be used
   * to create the manager. If you prefer you can directly provide your own
   * `RemirrorManager` to override this. The manager you provide will be cloned
   * and used within your editor.
   *
   * When a `Manager` is provided then several settings are ignored like
   * [[`stringHandler`]] and [[`onError`]].
   */
  extensions?: (() => Extension[]) | RemirrorManager<any>;

  /**
   * This is called when the editor has invalid content.
   *
   * @remarks
   *
   * To add this to the editor the following is needed.
   *
   * ```tsx
   * import React from 'react';
   * import { Remirror, InvalidContentHandler } from 'remirror';
   * import { Remirror, useManager } from '@remirror/react';
   * import { WysiwygPreset } from 'remirror/extensions';
   *
   * const Framework = () => {
   *   const onError: InvalidContentHandler = useCallback(({ json, invalidContent, transformers }) => {
   *     // Automatically remove all invalid nodes and marks.
   *     return transformers.remove(json, invalidContent);
   *   }, []);
   *
   *   const manager = useManager(() => [new WysiwygPreset()]);
   *
   *   return (
   *     <Remirror manager={manager} onError={onError}>
   *       <div />
   *     </Remirror>
   *   );
   * };
   * ```
   */
  onError?: InvalidContentHandler;

  /**
   * A function which transforms a string into a prosemirror node.
   *
   * @remarks
   *
   * Can be used to transform markdown / html or any other string format into a
   * prosemirror node.
   *
   * See [[`fromHTML`]] for an example of how this could work.
   */
  stringHandler?: keyof Remirror.StringHandlers | StringHandler;
}

export interface UseRemirrorReturn<Extension extends AnyExtension> {
  /**
   * The manager which is required by the `<Remirror />` component.
   */
  manager: RemirrorManager<Extension>;

  /**
   * The initial editor state based on the provided `content` and `selection`
   * properties. If none were passed in then the state is created from the
   * default empty doc node as defined by the editor Schema.
   */
  state: EditorState;

  /**
   * A function to update the state when you intend to make the editor
   * controlled.
   *
   * ```ts
   * import React, { useCallback } from 'react';
   * import { useRemirror, Provider } from '@remirror/react';
   * import { htmlToProsemirrorNode } from 'remirror';
   *
   * const Editor = () => {
   *   const { manager, setState, state } = useRemirror({
   *     content: '<p>Some content</p>',
   *     stringHandler: htmlToProsemirrorNode
   *   });
   *
   *   return (
   *     <Remirror
   *       onChange={useCallback((changeProps) => setState(changeProps.state), [setState])}
   *       state={state}
   *       manager={manager}
   *     />
   *   );
   * }
   * ```
   */
  setState: (state: EditorState) => void;

  /**
   * Syntactic sugar for using the `setState` method directly on the `<Remirror
   * />` component.
   *
   * ```ts
   * import React from 'react';
   * import { useRemirror, Provider } from '@remirror/react';
   * import { htmlToProsemirrorNode } from 'remirror';
   *
   * const Editor = () => {
   *   const { manager, onChange, state } = useRemirror({
   *     content: '<p>Some content</p>',
   *     stringHandler: htmlToProsemirrorNode
   *   });
   *
   *   return <Remirror onChange={onChange} state={state} manager={manager} />
   * }
   * ```
   */
  onChange: RemirrorEventListener<Extension>;

  /**
   * A function that provides the editor context. This is only available after
   * the `<Remirror />` component is mounted. Calling it in the very first
   * render phase will cause an error to be thrown.
   */
  getContext: () => ReactFrameworkOutput<Extension> | undefined;
}

/**
 * A hook which replaces the [[`Remirror`]] and gives you total control of
 * the editor you can create.
 *
 * This is very experimental and if successful could replace the current
 * patterns being used.
 */
export function useRemirror<Extension extends AnyExtension>(
  props: UseRemirrorProps<Extension> = {},
): UseRemirrorReturn<ReactExtensions<Extension>> {
  const { content, document, selection, extensions, ...settings } = props;
  const manager = useManager(extensions ?? (() => []), settings);

  const [state, setState] = useState(() =>
    manager.createState({
      selection,
      content: content ?? manager.createEmptyDoc(),
    }),
  );

  const onChange: RemirrorEventListener<Extension> = useCallback(({ state }) => {
    setState(state);
  }, []);

  const getContext = useCallback(() => {
    const context: unknown = manager.output;
    return context as ReactFrameworkOutput<ReactExtensions<Extension>>;
  }, [manager]);

  // Memoize the return to prevent unnecessary re-renders when props change.
  return useMemo(
    () => ({ state, setState, manager, onChange, getContext }),
    [getContext, manager, onChange, state],
  );
}