eddev 2.3.12 → 2.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/app/entry/MetaTags.d.ts.map +1 -1
  2. package/dist/app/entry/MetaTags.js +2 -0
  3. package/dist/app/lib/blocks/block-utils.d.ts +2 -2
  4. package/dist/app/lib/blocks/block-utils.d.ts.map +1 -1
  5. package/dist/app/lib/blocks/block-utils.js +10 -3
  6. package/dist/app/lib/blocks/editor/root-blocks.d.ts.map +1 -1
  7. package/dist/app/lib/blocks/editor/root-blocks.js +2 -2
  8. package/dist/app/lib/devtools/hooks/useTailwind.d.ts +45 -45
  9. package/dist/app/lib/hooks/query-hooks.d.ts +8 -8
  10. package/dist/app/lib/hooks/query-hooks.d.ts.map +1 -1
  11. package/dist/app/lib/hooks/query-hooks.js +2 -1
  12. package/dist/app/lib/runtime/apiConfig.d.ts +15 -0
  13. package/dist/app/lib/runtime/apiConfig.d.ts.map +1 -1
  14. package/dist/app/server/proxy-wp-admin.d.ts.map +1 -1
  15. package/dist/app/server/proxy-wp-admin.js +1 -0
  16. package/dist/app/server/server-context.d.ts +2 -2
  17. package/dist/app/server/server-context.d.ts.map +1 -1
  18. package/dist/app/utils/trpc-client.d.ts.map +1 -1
  19. package/dist/app/utils/trpc-client.js +19 -3
  20. package/dist/node/cli/version.d.ts +1 -1
  21. package/dist/node/cli/version.js +1 -1
  22. package/dist/node/compiler/dev-server.js +1 -1
  23. package/dist/node/compiler/get-vite-config.d.ts.map +1 -1
  24. package/dist/node/compiler/get-vite-config.js +1 -0
  25. package/dist/node/compiler/vinxi-app.d.ts +1 -1
  26. package/dist/node/compiler/vinxi-app.d.ts.map +1 -1
  27. package/dist/node/utils/fetch-wp.d.ts.map +1 -1
  28. package/dist/node/utils/fetch-wp.js +2 -1
  29. package/dist/node/utils/fs.d.ts +22 -19
  30. package/dist/node/utils/fs.d.ts.map +1 -1
  31. package/package.json +2 -2
  32. package/skills/eddev/SKILL.md +156 -0
  33. package/skills/eddev/docs/acf/admin-panel-widgets.mdx +99 -0
  34. package/skills/eddev/docs/acf/custom-enums.mdx +75 -0
  35. package/skills/eddev/docs/acf/custom-fields.mdx +131 -0
  36. package/skills/eddev/docs/acf.mdx +31 -0
  37. package/skills/eddev/docs/blocks/block-definition.mdx +189 -0
  38. package/skills/eddev/docs/blocks/core-blocks.mdx +86 -0
  39. package/skills/eddev/docs/blocks/data-and-editing.mdx +219 -0
  40. package/skills/eddev/docs/blocks/editor-config.mdx +157 -0
  41. package/skills/eddev/docs/blocks/nested-blocks.mdx +129 -0
  42. package/skills/eddev/docs/blocks/overview.mdx +58 -0
  43. package/skills/eddev/docs/blocks/template-parts.mdx +131 -0
  44. package/skills/eddev/docs/config.mdx +200 -0
  45. package/skills/eddev/docs/design/color.mdx +185 -0
  46. package/skills/eddev/docs/design/favicons.mdx +103 -0
  47. package/skills/eddev/docs/design/grid.mdx +120 -0
  48. package/skills/eddev/docs/design/icons.mdx +197 -0
  49. package/skills/eddev/docs/design/responsive-scaling.mdx +312 -0
  50. package/skills/eddev/docs/design/type.mdx +125 -0
  51. package/skills/eddev/docs/devtool/cli.mdx +201 -0
  52. package/skills/eddev/docs/devtool/overlay.mdx +5 -0
  53. package/skills/eddev/docs/getting-started.mdx +53 -0
  54. package/skills/eddev/docs/graphql/extending.mdx +186 -0
  55. package/skills/eddev/docs/graphql/fragments.mdx +107 -0
  56. package/skills/eddev/docs/graphql/global-data.mdx +47 -0
  57. package/skills/eddev/docs/graphql/infinite-queries.mdx +95 -0
  58. package/skills/eddev/docs/graphql/mutation-hooks.mdx +111 -0
  59. package/skills/eddev/docs/graphql/query-hooks.mdx +122 -0
  60. package/skills/eddev/docs/graphql/tooling.mdx +50 -0
  61. package/skills/eddev/docs/graphql.mdx +97 -0
  62. package/skills/eddev/docs/guides/color-schemes.mdx +204 -0
  63. package/skills/eddev/docs/guides/integrations.mdx +3 -0
  64. package/skills/eddev/docs/guides/page-transitions.mdx +5 -0
  65. package/skills/eddev/docs/guides/seo.mdx +5 -0
  66. package/skills/eddev/docs/guides/state-management.mdx +5 -0
  67. package/skills/eddev/docs/infra/caching.mdx +9 -0
  68. package/skills/eddev/docs/infra/deployment.mdx +13 -0
  69. package/skills/eddev/docs/infra/local.mdx +5 -0
  70. package/skills/eddev/docs/infra/security.mdx +11 -0
  71. package/skills/eddev/docs/routing/api.mdx +731 -0
  72. package/skills/eddev/docs/routing/custom.mdx +123 -0
  73. package/skills/eddev/docs/routing/full-details.mdx +37 -0
  74. package/skills/eddev/docs/routing/wordpress.mdx +70 -0
  75. package/skills/eddev/docs/routing.mdx +18 -0
  76. package/skills/eddev/docs/serverless/functions.mdx +436 -0
  77. package/skills/eddev/docs/serverless.mdx +202 -0
  78. package/skills/eddev/docs/snippets/automated-block-layouts.mdx +97 -0
  79. package/skills/eddev/docs/snippets/custom-routes-and-urls.mdx +91 -0
  80. package/skills/eddev/docs/snippets/multiple-editable-zones.mdx +87 -0
  81. package/skills/eddev/docs/snippets/querying-specific-blocks.mdx +164 -0
  82. package/skills/eddev/docs/snippets/submitting-forms-to-rpc.mdx +91 -0
  83. package/skills/eddev/docs/snippets/svgs.mdx +38 -0
  84. package/skills/eddev/docs/snippets/type-safe-acf-dropdowns.mdx +72 -0
  85. package/skills/eddev/docs/snippets.mdx +19 -0
  86. package/skills/eddev/docs/software.mdx +19 -0
  87. package/skills/eddev/docs/stack/how-it-works.mdx +50 -0
  88. package/skills/eddev/docs/stack/overview.mdx +56 -0
  89. package/skills/eddev/docs/stack/spa-vs-ssr.mdx +52 -0
  90. package/skills/eddev/docs/views/app-view.mdx +97 -0
  91. package/skills/eddev/docs/views/page-templates.mdx +82 -0
  92. package/skills/eddev/docs/views/queries.mdx +116 -0
  93. package/skills/eddev/docs/views.mdx +63 -0
  94. package/skills/eddev/index.mdx +79 -0
  95. package/tsconfig.app.json +2 -2
@@ -0,0 +1,53 @@
1
+ # Getting Started (/docs/getting-started)
2
+
3
+ **Set up a new eddev project from the starter theme.**
4
+
5
+ ## Initial Setup [#initial-setup]
6
+
7
+ All new sites should use the following process when setting up a new site:
8
+
9
+ 1. [Create a new repo on Github](https://github.com/new)
10
+ 1. Set `Owner` to `ed-digital`, and name your repo like `some-client-name`.
11
+ 2. Set `Repository Template` to `ed-digital/eddev-starter-theme`
12
+ 3. Hit `Create repository`
13
+ 2. Create a new site in [Local](/docs/infra/local), on your computer.
14
+ 1. Use the default settings
15
+ 2. Be sure to use `ed_admin` as the username, and generate a secure password using 1Password or similar, and the email address should be `web@ed.com.au`
16
+ 3. Save the username and password to 1Pass.
17
+ 3. Install the following plugins:
18
+ 1. Advanced Custom Fields Pro ([download](https://www.advancedcustomfields.com/my-account/view-licenses/)) (login details in 1Password)
19
+ 2. Add the licence key as the key for ACF Pro under 'Custom Fields > Updates'. The licence key is also in 1Password for convenience.
20
+ 3. Slim SEO — install from plugins dashboard
21
+ 4. Nested Pages — install from plugins dashboard
22
+ 4. Under Settings -> General, set Timezone to Sydney (or the appropriate timezone).
23
+ 5. Make sure that all the installed plugins have been activated.
24
+ 6. Clone your newly created theme to `wp-content/themes/`, making sure that the repo name and the theme folder name are identical.
25
+ 7. Run the following commands inside the theme folder
26
+ 1. `composer update`
27
+ 2. `yarn`
28
+ 3. `yarn add --dev eddev@latest`
29
+ 4. `yarn setup`
30
+ 8. Activate the theme under Appearance > Themes
31
+ 9. Run `yarn dev`
32
+ 10. Add, Commit and Push all code changes back to Github.
33
+
34
+ ## Initial WP Engine Setup [#initial-wp-engine-setup]
35
+
36
+ After you've followed the steps above, and the site is running correctly locally, you can create a site on WP Engine:
37
+
38
+ 1. [Log into WP Engine](https://my.wpengine.com/sites)
39
+ 2. Navigate through **Add Site → Build a new site → Get Started**
40
+ 3. Ensure 'ED.' is selected under the account picker.
41
+ 4. Select 'I will own it'
42
+ 5. Once complete, restart Local, run `yarn build` once, and do an initial push to the new site, with all files and the database.
43
+
44
+ ## Initial Vercel Setup [#initial-vercel-setup]
45
+
46
+ Once the WP Engine site is up and running, you can create the Vercel project:
47
+
48
+ 1. Log into [Vercel](https://vercel.com/)
49
+ 2. On the 'Overview' tab, navigate to **Add New\... → Project**
50
+ 3. Select our team from the dropdown, and choose the site repo, then hit 'Import'.
51
+ 4. Ensure the project name looks nice
52
+ 5. Add an Environment Variable with `Key: SITE_URL`, `Value: https://{staging-site-url}`
53
+ 6. Hit `Deploy`
@@ -0,0 +1,186 @@
1
+ # Extending the GraphQL Schema (/docs/graphql/extending)
2
+
3
+ **Extend WPGraphQL when frontend data needs custom schema fields.**
4
+
5
+ Most projects only need fields that come from WordPress, ACF, registered post
6
+ types, and blocks. When the frontend needs a value that does not naturally exist
7
+ in the schema, extend WPGraphQL in the WordPress theme or plugin code.
8
+
9
+ Use the GraphiQL IDE while writing schema extensions. It is the quickest way to
10
+ check the type names you are extending, the fields you have registered, and the
11
+ shape returned by your resolver.
12
+
13
+ ## Add A Field [#add-a-field]
14
+
15
+ Use `register_graphql_field` when you need to add a computed field to an
16
+ existing schema type.
17
+
18
+ ```php
19
+ add_action('graphql_register_types', function () {
20
+ register_graphql_field('Film', 'totalSessions', [
21
+ 'type' => 'Int',
22
+ 'description' => 'The number of sessions for this film.',
23
+ 'resolve' => function ($root) {
24
+ $sessions = get_post_meta($root->ID, 'total_sessions', true);
25
+ return $sessions ? (int)$sessions : null;
26
+ },
27
+ ]);
28
+ });
29
+ ```
30
+
31
+ The resolver receives the current GraphQL root object first. For post types this
32
+ usually gives you access to `$root->ID`, which can be used with `get_post_meta`,
33
+ ACF, or project model helpers.
34
+
35
+ ## Return Posts [#return-posts]
36
+
37
+ When a resolver returns a WordPress post and you want GraphQL subfields to keep
38
+ working, wrap the post in `WPGraphQL\Model\Post`.
39
+
40
+ ```php
41
+ add_action('graphql_register_types', function () {
42
+ register_graphql_field('RootQuery', 'currentProgram', [
43
+ 'type' => 'Program',
44
+ 'resolve' => function () {
45
+ $program = get_field('current_program', 'option');
46
+
47
+ if ($program) {
48
+ return new WPGraphQL\Model\Post($program);
49
+ }
50
+
51
+ return null;
52
+ },
53
+ ]);
54
+ });
55
+ ```
56
+
57
+ The same applies to lists of posts:
58
+
59
+ ```php
60
+ register_graphql_field('Film', 'guests', [
61
+ 'type' => ['list_of' => 'Guest'],
62
+ 'resolve' => function ($root) {
63
+ $guests = Film::fromPost($root->ID)->getGuests();
64
+
65
+ return array_map(function ($id) {
66
+ return new WPGraphQL\Model\Post(get_post($id));
67
+ }, $guests);
68
+ },
69
+ ]);
70
+ ```
71
+
72
+ ## Add A Root Query [#add-a-root-query]
73
+
74
+ Root fields are useful when the frontend needs a named project-level entry
75
+ point, such as the current programme, search results, or a custom data system.
76
+
77
+ ```php
78
+ add_action('graphql_register_types', function () {
79
+ register_graphql_field('RootQuery', 'siteSearch', [
80
+ 'type' => ['list_of' => 'ContentNode'],
81
+ 'args' => [
82
+ 'search' => ['type' => 'String'],
83
+ ],
84
+ 'resolve' => function ($root, $args) {
85
+ $query = new WP_Query([
86
+ 's' => $args['search'] ?? '',
87
+ 'post_type' => ['page', 'post'],
88
+ ]);
89
+
90
+ return array_map(function ($post) {
91
+ return new WPGraphQL\Model\Post($post);
92
+ }, $query->posts);
93
+ },
94
+ ]);
95
+ });
96
+ ```
97
+
98
+ Keep root fields narrow and intentional. If a field starts to behave like a
99
+ feature API, consider whether it should be a query hook, a serverless function,
100
+ or a dedicated WordPress endpoint instead.
101
+
102
+ ## Add Object Types [#add-object-types]
103
+
104
+ Use `register_graphql_object_type` when the field returns structured data that
105
+ does not already have a GraphQL type.
106
+
107
+ ```php
108
+ class FilmAssetProgram {
109
+ public function __construct(public WP_Post $program) {}
110
+
111
+ static function registerType() {
112
+ register_graphql_object_type('FilmAssetProgram', [
113
+ 'fields' => [
114
+ 'program' => [
115
+ 'type' => 'Program',
116
+ 'resolve' => function (FilmAssetProgram $program) {
117
+ return new WPGraphQL\Model\Post($program->program);
118
+ },
119
+ ],
120
+ 'buckets' => [
121
+ 'type' => ['list_of' => 'FilmAssetBucket'],
122
+ 'resolve' => function (FilmAssetProgram $program) {
123
+ return $program->getBuckets();
124
+ },
125
+ ],
126
+ ],
127
+ ]);
128
+ }
129
+ }
130
+ ```
131
+
132
+ Then return that object type from a field:
133
+
134
+ ```php
135
+ add_action('graphql_register_types', function () {
136
+ FilmAssetProgram::registerType();
137
+ FilmAssetBucket::registerType();
138
+
139
+ register_graphql_field('RootQuery', 'assetBuckets', [
140
+ 'type' => 'FilmAssetProgram',
141
+ 'args' => [
142
+ 'programId' => ['type' => 'ID'],
143
+ ],
144
+ 'resolve' => function ($root, $args) {
145
+ $program = get_post($args['programId']);
146
+
147
+ if (!$program) {
148
+ throw new \GraphQL\Error\UserError('Program not found');
149
+ }
150
+
151
+ return new FilmAssetProgram($program);
152
+ },
153
+ ]);
154
+ });
155
+ ```
156
+
157
+ ## Add Enums [#add-enums]
158
+
159
+ Enums are useful when a field has a small, known set of values.
160
+
161
+ ```php
162
+ enum EventType: string {
163
+ case Film = 'Film';
164
+ case Event = 'Event';
165
+ case Short = 'Short';
166
+ case Subscription = 'Subscription';
167
+ }
168
+
169
+ add_action('graphql_register_types', function () {
170
+ register_graphql_enum_type('EventType', [
171
+ 'description' => 'Standard event types',
172
+ 'values' => php_enum_cases_to_graphql_cases(EventType::cases()),
173
+ ]);
174
+ });
175
+ ```
176
+
177
+ Enums make the generated TypeScript types more useful than a loose `string`.
178
+
179
+ ## Keep Extensions Small [#keep-extensions-small]
180
+
181
+ Schema extensions are easiest to maintain when they sit close to the model they
182
+ describe. Prefer small fields on the relevant type over one large root field
183
+ that returns unrelated data.
184
+
185
+ When a resolver is expensive, cache the underlying data in WordPress or expose a
186
+ runtime query with an explicit `# ttl` comment in the `.graphql` file.
@@ -0,0 +1,107 @@
1
+ # GraphQL Fragments (/docs/graphql/fragments)
2
+
3
+ **Share GraphQL selections and generated fragment types across queries.**
4
+
5
+ [GraphQL fragments](https://graphql.org/learn/queries/#fragments) are reusable selections. Use them when the same object shape appears in multiple view, block, or runtime queries.
6
+
7
+ Fragments live in `queries/fragments/**/*.graphql`.
8
+
9
+ ```graphql filename="queries/fragments/ResponsiveImage.graphql"
10
+ fragment ResponsiveImage on MediaItem {
11
+ mediaItemUrl
12
+ caption
13
+ srcSet
14
+ altText
15
+ mediaDetails {
16
+ width
17
+ height
18
+ }
19
+ }
20
+ ```
21
+
22
+ Then spread the fragment anywhere the schema returns that type:
23
+
24
+ ```graphql filename="views/single.graphql"
25
+ query Single($postId: ID!) {
26
+ post(id: $postId, idType: DATABASE_ID) {
27
+ title
28
+ featuredImage {
29
+ node {
30
+ ...ResponsiveImage
31
+ }
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ eddev automatically includes fragment files during codegen, so the fragment type is available in the generated `/types.graphql.ts` file.
38
+
39
+ ## Fragment Types [#fragment-types]
40
+
41
+ Import generated fragment types from `@generated-types`. This is a project alias for the generated `/types.graphql.ts` file.
42
+
43
+ ```tsx filename="components/atoms/ResponsiveImage.tsx"
44
+ import { ResponsiveImageFragment } from "@generated-types"
45
+
46
+ type Props = Partial<ResponsiveImageFragment>
47
+ ```
48
+
49
+ ## Tiles And Repeated Shapes [#tiles-and-repeated-shapes]
50
+
51
+ Fragments are especially useful for cards, tiles, and reusable listing components.
52
+
53
+ ```graphql filename="queries/fragments/CaseStudyTile.graphql"
54
+ fragment CaseStudyTile on CaseStudy {
55
+ uri
56
+ title
57
+ subtitle
58
+ info {
59
+ heroImage {
60
+ ...ResponsiveImage
61
+ }
62
+ categories {
63
+ name
64
+ slug
65
+ uri
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ Then an archive view can select a list of tiles without repeating the fields:
72
+
73
+ ```graphql filename="views/archive-case-study.graphql"
74
+ query ArchiveCaseStudies {
75
+ caseStudies(first: 100) {
76
+ nodes {
77
+ ...CaseStudyTile
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ The matching component can use the generated fragment type as its prop shape:
84
+
85
+ ```tsx filename="components/tiles/CaseStudyTile.tsx"
86
+ import { CaseStudyTileFragment } from "@generated-types"
87
+ import { Link } from "eddev/routing"
88
+
89
+ type Props = {
90
+ caseStudy: CaseStudyTileFragment
91
+ }
92
+
93
+ export function CaseStudyTile(props: Props) {
94
+ const { caseStudy } = props
95
+
96
+ return (
97
+ <article>
98
+ <h2>
99
+ <Link href={caseStudy.uri!}>{caseStudy.title}</Link>
100
+ </h2>
101
+ {caseStudy.subtitle && <p>{caseStudy.subtitle}</p>}
102
+ </article>
103
+ )
104
+ }
105
+ ```
106
+
107
+ Prefer one fragment per reusable display shape, not one fragment per database table. A `CaseStudyTile` fragment is more useful than a generic fragment that tries to include every case-study field.
@@ -0,0 +1,47 @@
1
+ # Global Data (/docs/graphql/global-data)
2
+
3
+ **Fetch shared site shell data through views/_app.graphql.**
4
+
5
+ `views/_app.graphql` fetches data needed by the site shell and shared systems. Typical examples are menus, settings, trackers, global callouts, and template parts.
6
+
7
+ ```graphql filename="views/_app.graphql"
8
+ query CommonData {
9
+ menus {
10
+ nodes {
11
+ locations
12
+ slug
13
+ name
14
+ menuItems {
15
+ nodes {
16
+ label
17
+ title
18
+ url
19
+ parentId
20
+ }
21
+ }
22
+ }
23
+ }
24
+ templateParts {
25
+ siteFooter {
26
+ contentBlocks
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ The result is available to `views/_app.tsx` as props and anywhere in React through `useAppData()`.
33
+
34
+ ```tsx filename="features/site/Footer.tsx"
35
+ import { ContentBlocks } from "eddev/blocks"
36
+ import { useAppData } from "eddev/hooks"
37
+
38
+ export function Footer() {
39
+ const footer = useAppData((data) => data.templateParts?.siteFooter)
40
+
41
+ return <ContentBlocks blocks={footer?.contentBlocks} />
42
+ }
43
+ ```
44
+
45
+ App data is loaded with the initial route payload and reused during normal client-side navigation. Do not put per-page content in `_app.graphql`; use a paired [view query](/docs/views/queries) instead.
46
+
47
+ For the layout side of `_app.tsx`, see [`_app.tsx` Layout](/docs/views/app-view).
@@ -0,0 +1,95 @@
1
+ # Infinite Queries (/docs/graphql/infinite-queries)
2
+
3
+ **Build load-more interfaces with generated infinite query hooks.**
4
+
5
+ Infinite queries are runtime query hooks for "load more" interfaces. They build on normal [Query Hooks](/docs/graphql/query-hooks), but codegen adds pagination handling around a WPGraphQL connection.
6
+
7
+ <Callout type="info">
8
+ WPGraphQL uses cursor pagination. Instead of asking for page 2, you ask for the next items after the last `endCursor`.
9
+ </Callout>
10
+
11
+ ## Codegen Requirements [#codegen-requirements]
12
+
13
+ Codegen treats a query as infinite when the operation name contains `Infinite`, for example `UseInfinitePosts`.
14
+
15
+ The query must:
16
+
17
+ * live under `queries/`
18
+ * follow the normal `Use*` filename and operation-name rules
19
+ * define `$limit: Int` with a default value
20
+ * define `$cursor: String` with no default value
21
+ * use `first: $limit` and `after: $cursor` on the connection
22
+ * select one `pageInfo` object
23
+ * select `pageInfo { endCursor hasNextPage }`
24
+ * select `nodes` next to that `pageInfo`
25
+
26
+ ```graphql filename="queries/UseInfinitePosts.graphql"
27
+ query UseInfinitePosts($limit: Int = 6, $cursor: String) {
28
+ posts(first: $limit, after: $cursor) {
29
+ pageInfo {
30
+ endCursor
31
+ hasNextPage
32
+ }
33
+ nodes {
34
+ uri
35
+ title
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Hook API [#hook-api]
42
+
43
+ The generated hook returns a TanStack infinite-query result, with the page data flattened for normal rendering.
44
+
45
+ ```ts
46
+ type InfiniteResult<TNode> = {
47
+ data?: {
48
+ nodes: TNode[]
49
+ total?: number | null
50
+ }
51
+ hasNextPage: boolean
52
+ isFetchingNextPage: boolean
53
+ fetchNextPage: () => Promise<unknown>
54
+ }
55
+ ```
56
+
57
+ Use `fetchNextPage()` to load the next page.
58
+
59
+ ## Example Usage [#example-usage]
60
+
61
+ ```tsx filename="components/LatestPosts.tsx"
62
+ import { useInfinitePosts } from "@hooks/queries"
63
+
64
+ export function LatestPosts() {
65
+ const lookup = useInfinitePosts(undefined, {
66
+ refetchOnWindowFocus: false,
67
+ })
68
+
69
+ return (
70
+ <div>
71
+ {lookup.isLoading && <p>Loading...</p>}
72
+
73
+ {lookup.data?.nodes?.length ? (
74
+ <ul>
75
+ {lookup.data.nodes.map((post) => (
76
+ <li key={post.uri}>{post.title}</li>
77
+ ))}
78
+ </ul>
79
+ ) : null}
80
+
81
+ {lookup.hasNextPage && (
82
+ <button disabled={lookup.isFetchingNextPage} onClick={() => lookup.fetchNextPage()}>
83
+ {lookup.isFetchingNextPage ? "Loading..." : "Load more"}
84
+ </button>
85
+ )}
86
+ </div>
87
+ )
88
+ }
89
+ ```
90
+
91
+ ## Initial Data [#initial-data]
92
+
93
+ Infinite hooks accept `initialData`, but it must match the original GraphQL query structure, including the selected `nodes`, `pageInfo.hasNextPage`, and `pageInfo.endCursor` fields.
94
+
95
+ Only use `initialData` when you already have a matching connection result from a view, block, or app query. Otherwise, let the hook load normally in the browser.
@@ -0,0 +1,111 @@
1
+ # Mutation Hooks (/docs/graphql/mutation-hooks)
2
+
3
+ **Generate mutation hooks for runtime GraphQL operations with side effects.**
4
+
5
+ Mutations are GraphQL operations with side effects, such as submitting a form, creating a shared planner, saving user data, or sending an enquiry.
6
+
7
+ Mutation files live under `queries/` and follow the same filename rules as query hooks: the file and operation should start with `Use`, and the operation name should match the filename.
8
+
9
+ ```graphql filename="queries/shared-planner/UseCreateSharedPlanner.graphql"
10
+ mutation UseCreateSharedPlanner($ids: [Int!], $name: String, $userId: String) {
11
+ createSharedPlanner(input: { ferveIds: $ids, name: $name, ferveUserId: $userId }) {
12
+ planner {
13
+ id
14
+ name
15
+ }
16
+ }
17
+ }
18
+ ```
19
+
20
+ This generates `useCreateSharedPlanner` in `hooks/queries.ts`.
21
+
22
+ Unlike query hooks, mutation hooks do not run when rendered. You call `mutate()` or `mutateAsync()` from an event handler.
23
+
24
+ ## Hook Usage [#hook-usage]
25
+
26
+ ```tsx
27
+ import { useState } from "react"
28
+ import { useCreateSharedPlanner } from "@hooks/queries"
29
+
30
+ export function CreateSharedPlanner() {
31
+ const [name, setName] = useState("")
32
+ const create = useCreateSharedPlanner()
33
+
34
+ if (create.isSuccess) {
35
+ return <p>Planner created: {create.data.createSharedPlanner?.planner?.id}</p>
36
+ }
37
+
38
+ return (
39
+ <form
40
+ onSubmit={(e) => {
41
+ e.preventDefault()
42
+ if (create.isPending) return
43
+
44
+ create.mutate({
45
+ name,
46
+ ids: [123, 456],
47
+ })
48
+ }}
49
+ >
50
+ {create.isError && <p>{create.error.message}</p>}
51
+ <input disabled={create.isPending} value={name} onChange={(e) => setName(e.currentTarget.value)} />
52
+ <button disabled={create.isPending} type="submit">Create Planner</button>
53
+ </form>
54
+ )
55
+ }
56
+ ```
57
+
58
+ <Callout>
59
+ Many built-in WPGraphQL mutations require WordPress admin privileges. Public-facing mutations usually need to be registered by the project. See [Extending GraphQL](./extending).
60
+ </Callout>
61
+
62
+ ## Hook API [#hook-api]
63
+
64
+ The generated hook returns a TanStack mutation result:
65
+
66
+ ```ts
67
+ const mutation = useCreateSharedPlanner({
68
+ onSuccess(data) {
69
+ // Optional success handler
70
+ },
71
+ onError(error) {
72
+ // Optional error handler
73
+ },
74
+ })
75
+ ```
76
+
77
+ Useful returned properties include:
78
+
79
+ ```ts
80
+ type MutationResult<TData, TVars> = {
81
+ status: "idle" | "pending" | "error" | "success"
82
+ isPending: boolean
83
+ isSuccess: boolean
84
+ isError: boolean
85
+ error: QueryError | null
86
+ data: TData | undefined
87
+ mutate: (variables: TVars, options?: MutationOptions<TData>) => void
88
+ mutateAsync: (variables: TVars, options?: MutationOptions<TData>) => Promise<TData>
89
+ }
90
+ ```
91
+
92
+ You can pass mutation options to the hook itself, or to `mutate()` / `mutateAsync()`.
93
+
94
+ ## Manual Mutation [#manual-mutation]
95
+
96
+ Each generated mutation hook also has a static `.mutate()` method for non-hook usage.
97
+
98
+ ```ts
99
+ import { useCreateSharedPlanner } from "@hooks/queries"
100
+
101
+ async function createPlanner() {
102
+ const result = await useCreateSharedPlanner.mutate({
103
+ name: "Weekend picks",
104
+ ids: [123, 456],
105
+ })
106
+
107
+ return result.createSharedPlanner?.planner
108
+ }
109
+ ```
110
+
111
+ Prefer the hook in React components so loading and error states stay tied to the UI.