Skip to content

Commit

Permalink
Rename DeferredLayoutEffectsProvider to LayoutGroup
Browse files Browse the repository at this point in the history
  • Loading branch information
smoores-dev committed Mar 7, 2023
1 parent 46b08a0 commit ab5c88f
Show file tree
Hide file tree
Showing 12 changed files with 76 additions and 75 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/check.yml
Expand Up @@ -8,9 +8,11 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0

- uses: actions/setup-node@v1
- uses: actions/setup-node@v3
with:
node-version: 18.x

Expand Down
2 changes: 2 additions & 0 deletions .yarn/versions/87656097.yml
@@ -0,0 +1,2 @@
declined:
- "@nytimes/react-prosemirror"
62 changes: 30 additions & 32 deletions README.md
Expand Up @@ -20,21 +20,21 @@ yarn add @nytimes/react-prosemirror

- [The Problem](#the-problem)
- [The Solution](#the-solution)
* [Rendering ProseMirror Views within React](#rendering-prosemirror-views-within-react)
+ [`useEditorViewLayoutEffect`](#useeditorviewlayouteffect)
+ [`useEditorViewEvent`](#useeditorviewevent)
+ [`useEditorView`, `EditorViewContext` and `DeferredLayoutEffectsProvider`](#useeditorview-editorviewcontext-and-deferredlayouteffectsprovider)
* [Building NodeViews with React](#building-nodeviews-with-react)
- [Rendering ProseMirror Views within React](#rendering-prosemirror-views-within-react)
- [`useEditorViewLayoutEffect`](#useeditorviewlayouteffect)
- [`useEditorViewEvent`](#useeditorviewevent)
- [`useEditorView`, `EditorViewContext` and `LayoutGroup`](#useeditorview-editorviewcontext-and-layoutgroup)
- [Building NodeViews with React](#building-nodeviews-with-react)
- [API](#api)
* [`ProseMirror`](#prosemirror)
* [`EditorViewContext`](#editorviewcontext)
* [`DeferredLayoutEffectsProvider`](#deferredlayouteffectsprovider)
* [`useDeferredLayoutEffect`](#usedeferredlayouteffect)
* [`useEditorState`](#useeditorstate)
* [`useEditorView`](#useeditorview)
* [`useEditorViewEvent`](#useeditorviewevent-1)
* [`useEditorViewLayoutEffect`](#useeditorviewlayouteffect-1)
* [`useNodeViews`](#usenodeviews)
- [`ProseMirror`](#prosemirror)
- [`EditorViewContext`](#editorviewcontext)
- [`LayoutGroup`](#layoutgroup)
- [`useLayoutGroupEffect`](#uselayoutgroupeffect)
- [`useEditorState`](#useeditorstate)
- [`useEditorView`](#useeditorview)
- [`useEditorViewEvent`](#useeditorviewevent-1)
- [`useEditorViewLayoutEffect`](#useeditorviewlayouteffect-1)
- [`useNodeViews`](#usenodeviews)

<!-- tocstop -->

Expand Down Expand Up @@ -253,21 +253,21 @@ export function ProseMirrorEditor() {
}
```

#### `useEditorView`, `EditorViewContext` and `DeferredLayoutEffectsProvider`
#### `useEditorView`, `EditorViewContext` and `LayoutGroup`

Under the hood, the `ProseMirror` component essentially just composes three
separate tools: `useEditorView`, `EditorViewContext`, and
`DeferredLayoutEffectsProvider`. If you find yourself in need of more control
over these, they can also be used independently.
separate tools: `useEditorView`, `EditorViewContext`, and `LayoutGroup`. If you
find yourself in need of more control over these, they can also be used
independently.

`useEditorView` is a relatively simple hook that takes a mount point and
`EditorProps` as arguments and returns an EditorView instance.

`EditorViewContext` is a simple React context, which should be provided the
current EditorView and EditorState.

`DeferredLayoutEffectsProvider` _must_ be rendered as a parent of the component
using `useEditorView`.
`LayoutGroup` _must_ be rendered as a parent of the component using
`useEditorView`.

### Building NodeViews with React

Expand Down Expand Up @@ -385,39 +385,37 @@ See [ProseMirrorInner.tsx](./src/components/ProseMirrorInner.tsx) for example
usage. Note that if you are using the [`ProseMirror`](#prosemirror) component,
you don't need to use this context directly.

### `DeferredLayoutEffectsProvider`
### `LayoutGroup`

```tsx
type DeferredLayoutEffectsProvider = (props: {
children: React.ReactNode;
}) => JSX.Element;
type LayoutGroup = (props: { children: React.ReactNode }) => JSX.Element;
```

Provides a deferral point for deferred layout effects. All effects registered
with `useDeferredLayoutEffect` by children of this provider will execute _after_
Provides a deferral point for grouped layout effects. All effects registered
with `useLayoutGroupEffect` by children of this provider will execute _after_
all effects registered by `useLayoutEffect` by children of this provider.

See [ProseMirror.tsx](./src/components/ProseMirror.tsx) for example usage. Note
that if you are using the [`ProseMirror`](#prosemirror) component, you don't
need to use this context directly.

### `useDeferredLayoutEffect`
### `useLayoutGroupEffect`

```tsx
type useDeferredlayoutEffect = (
type useLayoutGroupEffect = (
effect: React.EffectCallback,
deps?: React.DependencyList
) => void;
```

Like `useLayoutEffect`, but all effect executions are run _after_ the
`DeferredLayoutEffectsProvider` layout effects phase.
`LayoutGroup` layout effects phase.

This hook allows child components to enqueue layout effects that won't be safe
to run until after a parent component's layout effects have run.

Note that components that use this hook must be descendants of the
[`DeferredLayoutEffectsProvider`](#deferredlayouteffectsprovider) component.
[`LayoutGroup`](#layoutgroup) component.

### `useEditorState`

Expand Down Expand Up @@ -460,8 +458,8 @@ Returns a stable function reference to be used as an event handler callback.
The callback will be called with the EditorView instance as its first argument.

This hook is dependent on both the `EditorViewContext.Provider` and the
`DeferredLayoutEffectProvider`. It can only be used in a component that is
mounted as a child of both of these providers.
`LayoutGroup`. It can only be used in a component that is mounted as a child of
both of these providers.

### `useEditorViewLayoutEffect`

Expand Down
9 changes: 6 additions & 3 deletions demo/main.tsx
Expand Up @@ -4,7 +4,7 @@ import { Schema } from "prosemirror-model";
import { EditorState } from "prosemirror-state";
import "prosemirror-view/style/prosemirror.css";
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import { createRoot } from "react-dom/client";

import { NodeViewComponentProps, ProseMirror, useNodeViews } from "../src";

Expand Down Expand Up @@ -40,7 +40,7 @@ function DemoEditor() {

return (
<main>
<h1>React-ProseMirror Demo</h1>
<h1>React ProseMirror Demo</h1>
<ProseMirror mount={mount} state={editorState} nodeViews={nodeViews}>
<div ref={setMount} />
{renderNodeViews()}
Expand All @@ -49,7 +49,10 @@ function DemoEditor() {
);
}

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const root = createRoot(document.getElementById("root")!);

root.render(
<React.StrictMode>
<DemoEditor />
</React.StrictMode>
Expand Down
7 changes: 4 additions & 3 deletions lint-staged.config.cjs
Expand Up @@ -3,7 +3,7 @@ module.exports = {
// files. lint-staged automatically adds any updated files
// to git, so it's safe to use `--fix` and `--write` flags,
// which change source files.
"*.{ts,js,json}": ["eslint --cache --fix", "prettier --write"],
"*.{tsx,ts,js,cjs,json}": ["eslint --cache --fix", "prettier --write"],
// If typescript files or json files (Typescript statically types .json
// files, and package.json and tsconfig.json files can change type
// correctness) change, we run tsc on the whole project. We use
Expand All @@ -15,9 +15,10 @@ module.exports = {
// with an array of filenames and expects us to produce an entire command
// (including filename arguments). Since we just want to run check:types
// on the whole project, not some specific files, we ignore this file list.
"*.{ts,json}": () => "yarn check:types",
"*.{tsx,ts,json}": () => "yarn check:types",
// Keep the table of contents up to date in the README file
"README.md": () => "yarn fix:toc",
// For markdown, HTML, and YAML files, we just run Prettier. ESLint doesn't have
// anything to say about these.
"*.{md,yml,html}": "prettier --write",
"README.md": () => "yarn fix:toc",
};
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -32,7 +32,7 @@
"dev": "yarn build -- --watch",
"fix:format": "prettier --write .",
"fix:lint": "eslint --cache --fix .",
"fix:toc": "markdown-toc -i README.md",
"fix:toc": "markdown-toc --bullets='-' -i README.md",
"fix": "concurrently -P 'npm:fix:* {@}' --",
"prepack": "yarn build",
"test": "jest"
Expand Down
6 changes: 3 additions & 3 deletions src/components/ProseMirror.tsx
@@ -1,6 +1,6 @@
import React from "react";

import { DeferredLayoutEffectsProvider } from "../contexts/DeferredLayoutEffects.js";
import { LayoutGroup } from "../contexts/LayoutGroup.js";

import { ProseMirrorInner, ProseMirrorProps } from "./ProseMirrorInner.js";

Expand All @@ -27,8 +27,8 @@ import { ProseMirrorInner, ProseMirrorProps } from "./ProseMirrorInner.js";
*/
export function ProseMirror(props: ProseMirrorProps) {
return (
<DeferredLayoutEffectsProvider>
<LayoutGroup>
<ProseMirrorInner {...props} />
</DeferredLayoutEffectsProvider>
</LayoutGroup>
);
}
Expand Up @@ -19,7 +19,7 @@ import { useForceUpdate } from "../hooks/useForceUpdate.js";
* layout effects have run.
*
*/
const useDeferredLayoutEffects = () => {
const useLayoutGroupEffectsRegistry = () => {
const createQueue = useRef(new Set<() => void>()).current;
const destroyQueue = useRef(new Set<() => void>()).current;

Expand Down Expand Up @@ -70,9 +70,7 @@ const useDeferredLayoutEffects = () => {
type Destructor = () => void;
type Register = (e: EffectCallback) => Destructor;

const DeferredLayoutEffectsContext = createContext<Register>(
null as unknown as Register
);
const LayoutGroupContext = createContext<Register>(null as unknown as Register);

interface DeferredLayoutEffectsContextProviderProps {
children: React.ReactNode;
Expand All @@ -85,14 +83,14 @@ interface DeferredLayoutEffectsContextProviderProps {
* effects registered by `useLayoutEffect` by children of
* this provider.
*/
export function DeferredLayoutEffectsProvider({
export function LayoutGroup({
children,
}: DeferredLayoutEffectsContextProviderProps) {
const register = useDeferredLayoutEffects();
const register = useLayoutGroupEffectsRegistry();
return (
<DeferredLayoutEffectsContext.Provider value={register}>
<LayoutGroupContext.Provider value={register}>
{children}
</DeferredLayoutEffectsContext.Provider>
</LayoutGroupContext.Provider>
);
}

Expand All @@ -105,11 +103,11 @@ export function DeferredLayoutEffectsProvider({
* that won't be safe to run until after a parent component's
* layout effects have run.
*/
export function useDeferredLayoutEffect(
export function useLayoutGroupEffect(
effect: EffectCallback,
deps?: DependencyList
) {
const register = useContext(DeferredLayoutEffectsContext);
const register = useContext(LayoutGroupContext);
// The rule for hooks wants to statically verify the deps,
// but the dependencies are up to the caller, not this implementation.
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
11 changes: 4 additions & 7 deletions src/contexts/__tests__/DeferredLayoutEffects.test.tsx
@@ -1,20 +1,17 @@
import { act, render, screen } from "@testing-library/react";
import React, { useLayoutEffect, useState } from "react";

import {
DeferredLayoutEffectsProvider,
useDeferredLayoutEffect,
} from "../DeferredLayoutEffects.js";
import { LayoutGroup, useLayoutGroupEffect } from "../LayoutGroup.js";

describe("DeferredLayoutEffects", () => {
jest.useFakeTimers("modern");

it("registers multiple effects and runs them", () => {
function Parent() {
return (
<DeferredLayoutEffectsProvider>
<LayoutGroup>
<Child />
</DeferredLayoutEffectsProvider>
</LayoutGroup>
);
}

Expand All @@ -33,7 +30,7 @@ describe("DeferredLayoutEffects", () => {
}
}, [double]);

useDeferredLayoutEffect(() => {
useLayoutGroupEffect(() => {
const timeout = setTimeout(() => {
setDouble((d) => d * 2);
}, 1000);
Expand Down
22 changes: 11 additions & 11 deletions src/hooks/__tests__/useEditorViewLayoutEffect.test.tsx
Expand Up @@ -3,8 +3,8 @@ import type { EditorState } from "prosemirror-state";
import type { EditorView } from "prosemirror-view";
import React from "react";

import { DeferredLayoutEffectsProvider } from "../../contexts/DeferredLayoutEffects.js";
import { EditorViewContext } from "../../contexts/EditorViewContext.js";
import { LayoutGroup } from "../../contexts/LayoutGroup.js";
import { useEditorViewLayoutEffect } from "../useEditorViewLayoutEffect.js";

function TestComponent({
Expand All @@ -25,11 +25,11 @@ describe("useEditorViewLayoutEffect", () => {
const editorState = {} as EditorState;

render(
<DeferredLayoutEffectsProvider>
<LayoutGroup>
<EditorViewContext.Provider value={{ editorView, editorState }}>
<TestComponent effect={effect} />
</EditorViewContext.Provider>
</DeferredLayoutEffectsProvider>
</LayoutGroup>
);

expect(effect).toHaveBeenCalled();
Expand All @@ -42,19 +42,19 @@ describe("useEditorViewLayoutEffect", () => {
const editorState = {} as EditorState;

const { rerender } = render(
<DeferredLayoutEffectsProvider>
<LayoutGroup>
<EditorViewContext.Provider value={{ editorView, editorState }}>
<TestComponent effect={effect} dependencies={[]} />
</EditorViewContext.Provider>
</DeferredLayoutEffectsProvider>
</LayoutGroup>
);

rerender(
<DeferredLayoutEffectsProvider>
<LayoutGroup>
<EditorViewContext.Provider value={{ editorView, editorState }}>
<TestComponent effect={effect} dependencies={[]} />
</EditorViewContext.Provider>
</DeferredLayoutEffectsProvider>
</LayoutGroup>
);

expect(effect).toHaveBeenCalledTimes(1);
Expand All @@ -66,19 +66,19 @@ describe("useEditorViewLayoutEffect", () => {
const editorState = {} as EditorState;

const { rerender } = render(
<DeferredLayoutEffectsProvider>
<LayoutGroup>
<EditorViewContext.Provider value={{ editorView, editorState }}>
<TestComponent effect={effect} dependencies={["one"]} />
</EditorViewContext.Provider>
</DeferredLayoutEffectsProvider>
</LayoutGroup>
);

rerender(
<DeferredLayoutEffectsProvider>
<LayoutGroup>
<EditorViewContext.Provider value={{ editorView, editorState }}>
<TestComponent effect={effect} dependencies={["two"]} />
</EditorViewContext.Provider>
</DeferredLayoutEffectsProvider>
</LayoutGroup>
);

expect(effect).toHaveBeenCalledTimes(2);
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useEditorViewLayoutEffect.ts
Expand Up @@ -2,8 +2,8 @@ import type { EditorView } from "prosemirror-view";
import { useContext } from "react";
import type { DependencyList } from "react";

import { useDeferredLayoutEffect } from "../contexts/DeferredLayoutEffects.js";
import { EditorViewContext } from "../contexts/EditorViewContext.js";
import { useLayoutGroupEffect } from "../contexts/LayoutGroup.js";

/**
* Registers a layout effect to run after the EditorView has
Expand Down Expand Up @@ -32,7 +32,7 @@ export function useEditorViewLayoutEffect(
// Note: we specifically don't want to re-run the effect
// every time it changes, because it will most likely
// be defined inline and run on every re-render.
useDeferredLayoutEffect(
useLayoutGroupEffect(
() => effect(editorView),
// The rules of hooks want to be able to statically
// verify the dependencies for the effect, but this will
Expand Down

0 comments on commit ab5c88f

Please sign in to comment.