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:
includereturns only matching block names, tags, flags, or wildcards.excluderemoves matching blocks.flattenExcludedreplaces excluded wrapper blocks with their children.maxDepthlimits nested block depth.limitlimits 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:
storeis required. Pass a string key like"title", or a tuple fromuseInlineEditableValue,usePostTitleEditor,usePostExcerptEditor,usePostMetaEditor, oruseState.aschanges the rendered element or component.placeholderis editor-only helper text.defaultValueis used on the frontend when no value has been entered. It also becomes the editor placeholder whenplaceholderis not provided.plainTextdisables rich-text formatting for labels, buttons, and short UI copy.disableLineBreaksprevents multiline editing.allowedFormatslimits rich-text formats. UseallowedFormats={[]}to disable formatting without usingplainText.
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.