eddev
Blocks

Data and Editing

GraphQL props, inline content, and core block rendering

Blocks usually combine three kinds of data:

  • ACF or WordPress data selected by a paired GraphQL file
  • inline editor values stored directly on the block
  • nested content from InnerBlocks

Use the smallest tool that fits. Inline text is usually better as EditableText. Structured data, relationships, media, or repeated fields are usually better as ACF fields selected through GraphQL.

Paired GraphQL Files

Place a .graphql file beside the block component when the block needs CMS data.

query {
  block {
    content_image {
      image {
        ...ResponsiveImage
      }
    }
  }
}

For a block named content/image, eddev exposes its attached ACF fields under block.content_image. The field name is generated from the block path by camel-casing each path segment and joining them with an underscore.

The query result becomes the component props:

import { defineBlock } from "eddev/blocks"

export const meta: BlockMeta = {
  title: "Image",
  category: "media",
  tags: ["#root"],
}

export default defineBlock("content/image", (props) => {
  if (!props.image) return null
  return <ResponsiveImage {...props.image} />
})

Block queries can also fetch data that is not inside block { ... }. For example, a listing block can query posts directly, and a post-specific header block can use $postId.

query {
  teamMembers(first: 999) {
    nodes {
      title
      uri
    }
  }
}
query FilmHeader($postId: ID!) {
  film(id: $postId, idType: DATABASE_ID) {
    title
    uri
  }
}

Querying Blocks From Views

Views select page blocks with contentBlocks.

query Page($postId: ID!) {
  page(id: $postId, idType: DATABASE_ID) {
    title
    contentBlocks
  }
}

Then the view renders them with ContentBlocks.

import { ContentBlocks } from "eddev/blocks"
import { defineView } from "eddev/views"

export default defineView("page", (props) => {
  return <ContentBlocks blocks={props.page?.contentBlocks} />
})

contentBlocks also accepts a few useful filters:

  • include returns only matching block names, tags, flags, or wildcards.
  • exclude removes matching blocks.
  • flattenExcluded replaces excluded wrapper blocks with their children.
  • maxDepth limits nested block depth.
  • limit limits top-level blocks.

This is useful when a view needs to render different subsets of the same page content in different layout regions:

query Page($postId: ID!) {
  page(id: $postId, idType: DATABASE_ID) {
    title
    contentBlocks
    introBlocks: contentBlocks(include: ["content/intro"], limit: 1)
    bodyBlocks: contentBlocks(exclude: ["content/intro"], flattenExcluded: true)
  }
}

This keeps the data local to the page view. In React, you can render introBlocks and bodyBlocks in different layout regions without creating a separate runtime query.

Inline Editing

Use EditableText for text authors should edit directly in the preview.

import { defineBlock, EditableText } from "eddev/blocks"

export default defineBlock("content/quote", () => {
  return (
    <figure>
      <EditableText as="blockquote" store="quote" placeholder="Enter a quote" />
      <EditableText as="figcaption" store="citation" placeholder="Citation" plainText />
    </figure>
  )
})

The important props are:

  • store is required. Pass a string key like "title", or a tuple from useInlineEditableValue, usePostTitleEditor, usePostExcerptEditor, usePostMetaEditor, or useState.
  • as changes the rendered element or component.
  • placeholder is editor-only helper text.
  • defaultValue is used on the frontend when no value has been entered. It also becomes the editor placeholder when placeholder is not provided.
  • plainText disables rich-text formatting for labels, buttons, and short UI copy.
  • disableLineBreaks prevents multiline editing.
  • allowedFormats limits rich-text formats. Use allowedFormats={[]} to disable formatting without using plainText.

appendOnEnter and removeOnDelete are less common, but useful for text-like block flows. appendOnEnter inserts a new paragraph, or a specified block name, when the author presses Enter. removeOnDelete removes the current block when the author presses Backspace at the start of the text.

Inline Value Stores

useInlineEditableValue reads and writes values in the current block's inline data. It behaves like a scoped useState.

function useInlineEditableValue<T>(
  key: string,
  defaultValue?: T,
): [value: T | undefined, setValue: (value: T) => void]

When defaultValue is supplied, that value is returned until the author edits the field.

Use it when the component needs the current value for rendering, attributes, tracking, conditional UI, or non-text controls.

import { defineBlock, EditableText, useInlineEditableValue } from "eddev/blocks"

export default defineBlock("content/cta-button", (props) => {
  const [title] = useInlineEditableValue<string>("title")

  return (
    <div data-mixpanel-cta-title={title}>
      <EditableText store="title" defaultValue={props.link?.title} placeholder="Enter a title" />
    </div>
  )
})

You can also pass the tuple directly to EditableText so the editable field and the rest of the component share the same value.

const titleStore = useInlineEditableValue("title", "Read more")
const [title] = titleStore

return (
  <a aria-label={title}>
    <EditableText store={titleStore} plainText disableLineBreaks />
  </a>
)

The value does not need to be consumed by EditableText. For example, an inspector setting can use the same store pattern for booleans or objects.

import { InspectorControls, useInlineEditableValue } from "eddev/blocks"

function DisplayOptions() {
  const [shown, setShown] = useInlineEditableValue<Record<string, boolean>>("shown", {})

  return (
    <>
      {env.admin && (
        <InspectorControls>
          <label>
            <input
              type="checkbox"
              checked={shown.summary ?? true}
              onChange={(event) => setShown({ ...shown, summary: event.currentTarget.checked })}
            />
            Show summary
          </label>
        </InspectorControls>
      )}
    </>
  )
}

InspectorControls is a good place for small per-block controls where ACF would be too heavy. Wrap it in env.admin so editor-only controls are not bundled for the frontend.

Core Blocks

WordPress core blocks can be rendered as HTML, tagged for editor rules, or grouped into synthetic blocks. See Core Blocks for those patterns.

On this page