@portabletext/editor 1.1.5 → 1.1.7

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.
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: !0 });
3
- var jsxRuntime = require("react/jsx-runtime"), isEqual = require("lodash/isEqual.js"), noop = require("lodash/noop.js"), react = require("react"), slate = require("slate"), slateReact = require("slate-react"), debug$m = require("debug"), types = require("@sanity/types"), styledComponents = require("styled-components"), uniq = require("lodash/uniq.js"), rxjs = require("rxjs"), xstate = require("xstate"), schema = require("@sanity/schema"), patches = require("@portabletext/patches"), get = require("lodash/get.js"), isUndefined = require("lodash/isUndefined.js"), omitBy = require("lodash/omitBy.js"), flatten = require("lodash/flatten.js"), isHotkeyEsm = require("is-hotkey-esm"), blockTools = require("@sanity/block-tools"), isPlainObject = require("lodash/isPlainObject.js"), throttle = require("lodash/throttle.js"), debounce = require("lodash/debounce.js"), content = require("@sanity/util/content");
3
+ var jsxRuntime = require("react/jsx-runtime"), isEqual = require("lodash/isEqual.js"), noop = require("lodash/noop.js"), react = require("react"), slate = require("slate"), slateReact = require("slate-react"), debug$m = require("debug"), types = require("@sanity/types"), styledComponents = require("styled-components"), uniq = require("lodash/uniq.js"), rxjs = require("rxjs"), xstate = require("xstate"), schema = require("@sanity/schema"), isHotkeyEsm = require("is-hotkey-esm"), patches = require("@portabletext/patches"), get = require("lodash/get.js"), isUndefined = require("lodash/isUndefined.js"), omitBy = require("lodash/omitBy.js"), flatten = require("lodash/flatten.js"), blockTools = require("@sanity/block-tools"), isPlainObject = require("lodash/isPlainObject.js"), throttle = require("lodash/throttle.js"), debounce = require("lodash/debounce.js"), content = require("@sanity/util/content");
4
4
  function _interopDefaultCompat(e) {
5
5
  return e && typeof e == "object" && "default" in e ? e : { default: e };
6
6
  }
@@ -791,7 +791,28 @@ function compileType(rawType) {
791
791
  types: [rawType]
792
792
  }).get(rawType.name);
793
793
  }
794
- const debug$k = debugWithName("operationToPatches");
794
+ function getFocusBlock(context) {
795
+ const key = context.selection && types.isKeySegment(context.selection.focus.path[0]) ? context.selection.focus.path[0]._key : void 0, node = key ? context.value.find((block) => block._key === key) : void 0;
796
+ return node && key ? { node, path: [{ _key: key }] } : void 0;
797
+ }
798
+ function getFocusBlockObject(context) {
799
+ const focusBlock = getFocusBlock(context);
800
+ return focusBlock && !types.isPortableTextTextBlock(focusBlock.node) ? { node: focusBlock.node, path: focusBlock.path } : void 0;
801
+ }
802
+ const overwriteSoftReturn = {
803
+ on: "key down",
804
+ guard: ({ event }) => isHotkeyEsm.isHotkey("shift+enter", event.nativeEvent),
805
+ actions: [
806
+ ({ event }) => (event.nativeEvent.preventDefault(), { type: "insert text", text: `
807
+ ` })
808
+ ]
809
+ }, enterOnVoidBlock = {
810
+ on: "key down",
811
+ guard: ({ context, event }) => isHotkeyEsm.isHotkey("enter", event.nativeEvent) ? !!getFocusBlockObject(context) : !1,
812
+ actions: [
813
+ ({ event }) => (event.nativeEvent.preventDefault(), { type: "insert text block", decorators: [] })
814
+ ]
815
+ }, coreBehaviors = [overwriteSoftReturn, enterOnVoidBlock], debug$k = debugWithName("operationToPatches");
795
816
  function createOperationToPatches(types2) {
796
817
  const textBlockName = types2.block.name;
797
818
  function insertTextPatch(editor, operation, beforeValue) {
@@ -3251,9 +3272,6 @@ function createWithPortableTextLists(types2) {
3251
3272
  }, editor;
3252
3273
  };
3253
3274
  }
3254
- function isPortableTextSpan(node) {
3255
- return node._type === "span" && "text" in node && typeof node.text == "string" && (typeof node.marks > "u" || Array.isArray(node.marks) && node.marks.every((mark) => typeof mark == "string"));
3256
- }
3257
3275
  function isPortableTextBlock(node) {
3258
3276
  return (
3259
3277
  // A block doesn't _have_ to be named 'block' - to differentiate between
@@ -3266,6 +3284,34 @@ function isPortableTextBlock(node) {
3266
3284
  node.children.every((child) => typeof child == "object" && "_type" in child)
3267
3285
  );
3268
3286
  }
3287
+ function getPreviousSpan({
3288
+ editor,
3289
+ blockPath,
3290
+ spanPath
3291
+ }) {
3292
+ let previousSpan;
3293
+ for (const [child, childPath] of slate.Node.children(editor, blockPath, {
3294
+ reverse: !0
3295
+ }))
3296
+ if (editor.isTextSpan(child) && slate.Path.isBefore(childPath, spanPath)) {
3297
+ previousSpan = child;
3298
+ break;
3299
+ }
3300
+ return previousSpan;
3301
+ }
3302
+ function getNextSpan({
3303
+ editor,
3304
+ blockPath,
3305
+ spanPath
3306
+ }) {
3307
+ let nextSpan;
3308
+ for (const [child, childPath] of slate.Node.children(editor, blockPath))
3309
+ if (editor.isTextSpan(child) && slate.Path.isAfter(childPath, spanPath)) {
3310
+ nextSpan = child;
3311
+ break;
3312
+ }
3313
+ return nextSpan;
3314
+ }
3269
3315
  const debug$c = debugWithName("plugin:withPortableTextMarkModel");
3270
3316
  function createWithPortableTextMarkModel(editorActor, types2) {
3271
3317
  return function(editor) {
@@ -3405,25 +3451,13 @@ function createWithPortableTextMarkModel(editorActor, types2) {
3405
3451
  })
3406
3452
  )[0] ?? [void 0, void 0], marks = span.marks ?? [], marksWithoutAnnotations = marks.filter(
3407
3453
  (mark) => decorators.includes(mark)
3408
- ), spanHasAnnotations = marks.length > marksWithoutAnnotations.length, spanIsEmpty = span.text.length === 0, atTheBeginningOfSpan = selection.anchor.offset === 0, atTheEndOfSpan = selection.anchor.offset === span.text.length;
3409
- let previousSpan, nextSpan;
3410
- for (const [child, childPath] of slate.Node.children(editor, blockPath, {
3411
- reverse: !0
3412
- }))
3413
- if (editor.isTextSpan(child) && slate.Path.isBefore(childPath, spanPath)) {
3414
- previousSpan = child;
3415
- break;
3416
- }
3417
- for (const [child, childPath] of slate.Node.children(editor, blockPath))
3418
- if (editor.isTextSpan(child) && slate.Path.isAfter(childPath, spanPath)) {
3419
- nextSpan = child;
3420
- break;
3421
- }
3422
- const previousSpanHasSameAnnotation = previousSpan ? previousSpan.marks?.some(
3423
- (mark) => !decorators.includes(mark) && marks.includes(mark)
3424
- ) : !1, previousSpanHasSameMarks = previousSpan ? previousSpan.marks?.every((mark) => marks.includes(mark)) : !1, nextSpanHasSameAnnotation = nextSpan ? nextSpan.marks?.some(
3454
+ ), spanHasAnnotations = marks.length > marksWithoutAnnotations.length, spanIsEmpty = span.text.length === 0, atTheBeginningOfSpan = selection.anchor.offset === 0, atTheEndOfSpan = selection.anchor.offset === span.text.length, previousSpan = getPreviousSpan({ editor, blockPath, spanPath }), nextSpan = getNextSpan({ editor, blockPath, spanPath }), nextSpanAnnotations = nextSpan?.marks?.filter((mark) => !decorators.includes(mark)) ?? [], spanAnnotations = marks.filter(
3455
+ (mark) => !decorators.includes(mark)
3456
+ ), previousSpanHasSameAnnotation = previousSpan ? previousSpan.marks?.some(
3425
3457
  (mark) => !decorators.includes(mark) && marks.includes(mark)
3426
- ) : !1, nextSpanHasSameMarks = nextSpan ? nextSpan.marks?.every((mark) => marks.includes(mark)) : !1;
3458
+ ) : !1, previousSpanHasSameMarks = previousSpan ? previousSpan.marks?.every((mark) => marks.includes(mark)) : !1, nextSpanSharesSomeAnnotations = spanAnnotations.some(
3459
+ (mark) => nextSpanAnnotations?.includes(mark)
3460
+ );
3427
3461
  if (spanHasAnnotations && !spanIsEmpty) {
3428
3462
  if (atTheBeginningOfSpan) {
3429
3463
  previousSpanHasSameMarks ? slate.Transforms.insertNodes(editor, {
@@ -3440,63 +3474,62 @@ function createWithPortableTextMarkModel(editorActor, types2) {
3440
3474
  return;
3441
3475
  }
3442
3476
  if (atTheEndOfSpan) {
3443
- nextSpanHasSameMarks ? slate.Transforms.insertNodes(editor, {
3444
- _type: "span",
3445
- _key: editorActor.getSnapshot().context.keyGenerator(),
3446
- text: op.text,
3447
- marks: nextSpan?.marks ?? []
3448
- }) : nextSpanHasSameAnnotation ? apply2(op) : slate.Transforms.insertNodes(editor, {
3449
- _type: "span",
3450
- _key: editorActor.getSnapshot().context.keyGenerator(),
3451
- text: op.text,
3452
- marks: []
3453
- });
3454
- return;
3477
+ if (nextSpan && nextSpanSharesSomeAnnotations && nextSpanAnnotations.length < spanAnnotations.length || !nextSpanSharesSomeAnnotations) {
3478
+ slate.Transforms.insertNodes(editor, {
3479
+ _type: "span",
3480
+ _key: editorActor.getSnapshot().context.keyGenerator(),
3481
+ text: op.text,
3482
+ marks: nextSpan?.marks ?? []
3483
+ });
3484
+ return;
3485
+ }
3486
+ if (!nextSpan) {
3487
+ slate.Transforms.insertNodes(editor, {
3488
+ _type: "span",
3489
+ _key: editorActor.getSnapshot().context.keyGenerator(),
3490
+ text: op.text,
3491
+ marks: []
3492
+ });
3493
+ return;
3494
+ }
3455
3495
  }
3456
3496
  }
3457
3497
  }
3458
3498
  }
3459
3499
  if (op.type === "remove_text") {
3460
- const node = Array.from(
3461
- slate.Editor.nodes(editor, {
3462
- mode: "lowest",
3463
- at: { path: op.path, offset: op.offset },
3464
- match: (n) => n._type === types2.span.name,
3465
- voids: !1
3466
- })
3467
- )[0][0], block = slate.Editor.node(editor, slate.Path.parent(op.path))[0];
3468
- if (node && isPortableTextSpan(node) && block && isPortableTextBlock(block)) {
3469
- const markDefs = block.markDefs ?? [], nodeHasAnnotations = (node.marks ?? []).some(
3470
- (mark) => markDefs.find((markDef) => markDef._key === mark)
3471
- ), deletingPartOfTheNode = op.offset !== 0, deletingFromTheEnd = op.offset + op.text.length === node.text.length;
3472
- if (nodeHasAnnotations && deletingPartOfTheNode && deletingFromTheEnd) {
3473
- slate.Editor.withoutNormalizing(editor, () => {
3474
- slate.Transforms.splitNodes(editor, {
3475
- match: slate.Text.isText,
3476
- at: { path: op.path, offset: op.offset }
3477
- }), slate.Transforms.removeNodes(editor, { at: slate.Path.next(op.path) });
3478
- }), editor.onChange();
3479
- return;
3480
- }
3481
- const deletingAllText = op.offset === 0 && deletingFromTheEnd;
3482
- if (nodeHasAnnotations && deletingAllText) {
3483
- const marksWithoutAnnotationMarks = ({
3484
- ...slate.Editor.marks(editor) || {}
3485
- }.marks || []).filter((mark) => decorators.includes(mark));
3486
- slate.Editor.withoutNormalizing(editor, () => {
3487
- apply2(op), slate.Transforms.setNodes(
3488
- editor,
3489
- { marks: marksWithoutAnnotationMarks },
3490
- { at: op.path }
3491
- );
3492
- }), editor.onChange();
3493
- return;
3494
- }
3495
- if (node.marks !== void 0 && node.marks.length > 0 && deletingAllText) {
3496
- slate.Editor.withoutNormalizing(editor, () => {
3497
- apply2(op), slate.Transforms.setNodes(editor, { marks: [] }, { at: op.path });
3498
- }), editor.onChange();
3499
- return;
3500
+ const { selection } = editor;
3501
+ if (selection && slate.Range.isExpanded(selection)) {
3502
+ const [block, blockPath] = slate.Editor.node(editor, selection, {
3503
+ depth: 1
3504
+ }), [span, spanPath] = Array.from(
3505
+ slate.Editor.nodes(editor, {
3506
+ mode: "lowest",
3507
+ at: { path: op.path, offset: op.offset },
3508
+ match: (n) => editor.isTextSpan(n),
3509
+ voids: !1
3510
+ })
3511
+ )[0] ?? [void 0, void 0];
3512
+ if (span && block && isPortableTextBlock(block)) {
3513
+ const markDefs = block.markDefs ?? [], marks = span.marks ?? [], spanHasAnnotations = marks.some(
3514
+ (mark) => markDefs.find((markDef) => markDef._key === mark)
3515
+ ), deletingFromTheEnd = op.offset + op.text.length === span.text.length, deletingAllText = op.offset === 0 && deletingFromTheEnd, previousSpan = getPreviousSpan({ editor, blockPath, spanPath }), nextSpan = getNextSpan({ editor, blockPath, spanPath }), previousSpanHasSameAnnotation = previousSpan ? previousSpan.marks?.some(
3516
+ (mark) => !decorators.includes(mark) && marks.includes(mark)
3517
+ ) : !1, nextSpanHasSameAnnotation = nextSpan ? nextSpan.marks?.some(
3518
+ (mark) => !decorators.includes(mark) && marks.includes(mark)
3519
+ ) : !1;
3520
+ if (spanHasAnnotations && deletingAllText && !previousSpanHasSameAnnotation && !nextSpanHasSameAnnotation) {
3521
+ const marksWithoutAnnotationMarks = ({
3522
+ ...slate.Editor.marks(editor) || {}
3523
+ }.marks || []).filter((mark) => decorators.includes(mark));
3524
+ slate.Editor.withoutNormalizing(editor, () => {
3525
+ apply2(op), slate.Transforms.setNodes(
3526
+ editor,
3527
+ { marks: marksWithoutAnnotationMarks },
3528
+ { at: op.path }
3529
+ );
3530
+ }), editor.onChange();
3531
+ return;
3532
+ }
3500
3533
  }
3501
3534
  }
3502
3535
  }
@@ -3855,22 +3888,7 @@ function createWithHotkeys(types$1, portableTextEditor, hotkeysFromOptions) {
3855
3888
  return;
3856
3889
  }
3857
3890
  }
3858
- if (focusBlock && slate.Editor.isVoid(editor, focusBlock)) {
3859
- slate.Editor.insertNode(editor, editor.pteCreateTextBlock({ decorators: [] })), event.preventDefault(), editor.onChange();
3860
- return;
3861
- }
3862
- event.preventDefault(), editor.insertBreak(), editor.onChange();
3863
- }
3864
- if (isShiftEnter) {
3865
- event.preventDefault(), editor.insertText(`
3866
- `);
3867
- return;
3868
- }
3869
- if (isHotkeyEsm.isHotkey("mod+z", event.nativeEvent)) {
3870
- event.preventDefault(), editor.undo();
3871
- return;
3872
3891
  }
3873
- (isHotkeyEsm.isHotkey("mod+y", event.nativeEvent) || isHotkeyEsm.isHotkey("mod+shift+z", event.nativeEvent)) && (event.preventDefault(), editor.redo());
3874
3892
  }, editor;
3875
3893
  };
3876
3894
  }
@@ -4795,6 +4813,29 @@ ${JSON.stringify(pendingPatches.current, null, 2)}`);
4795
4813
  debug$4("Value from props changed, syncing new value"), syncValue(value), isInitialValueFromProps.current && (editorActor.send({ type: "ready" }), isInitialValueFromProps.current = !1);
4796
4814
  }, [editorActor, syncValue, value]), null;
4797
4815
  }
4816
+ function inserText({
4817
+ event
4818
+ }) {
4819
+ slate.Editor.insertText(event.editor, event.text);
4820
+ }
4821
+ function inserTextBlock({
4822
+ context,
4823
+ event
4824
+ }) {
4825
+ slate.Editor.insertNode(event.editor, {
4826
+ _key: context.keyGenerator(),
4827
+ _type: context.schema.block.name,
4828
+ style: context.schema.styles[0].value ?? "normal",
4829
+ markDefs: [],
4830
+ children: [
4831
+ {
4832
+ _key: context.keyGenerator(),
4833
+ _type: "span",
4834
+ text: ""
4835
+ }
4836
+ ]
4837
+ });
4838
+ }
4798
4839
  const networkLogic = xstate.fromCallback(({ sendBack }) => {
4799
4840
  const onlineHandler = () => {
4800
4841
  sendBack({ type: "online" });
@@ -4812,6 +4853,15 @@ const networkLogic = xstate.fromCallback(({ sendBack }) => {
4812
4853
  input: {}
4813
4854
  },
4814
4855
  actions: {
4856
+ "apply:insert text": ({ context, event }) => {
4857
+ xstate.assertEvent(event, "insert text"), inserText({ context, event });
4858
+ },
4859
+ "apply:insert text block": ({ context, event }) => {
4860
+ xstate.assertEvent(event, "insert text block"), inserTextBlock({ context, event });
4861
+ },
4862
+ "assign schema": xstate.assign({
4863
+ schema: ({ event }) => (xstate.assertEvent(event, "update schema"), event.schema)
4864
+ }),
4815
4865
  "emit patch event": xstate.emit(({ event }) => (xstate.assertEvent(event, "patch"), event)),
4816
4866
  "emit mutation event": xstate.emit(({ event }) => (xstate.assertEvent(event, "mutation"), event)),
4817
4867
  "defer event": xstate.assign({
@@ -4823,6 +4873,50 @@ const networkLogic = xstate.fromCallback(({ sendBack }) => {
4823
4873
  }),
4824
4874
  "clear pending events": xstate.assign({
4825
4875
  pendingEvents: []
4876
+ }),
4877
+ "handle behavior event": xstate.enqueueActions(({ context, event, enqueue }) => {
4878
+ xstate.assertEvent(event, ["key down"]);
4879
+ const eventBehaviors = context.behaviors.filter(
4880
+ (behavior) => behavior.on === event.type
4881
+ );
4882
+ if (eventBehaviors.length === 0)
4883
+ return;
4884
+ const value = fromSlateValue(
4885
+ event.editor.children,
4886
+ context.schema.block.name,
4887
+ KEY_TO_VALUE_ELEMENT.get(event.editor)
4888
+ ), selection = toPortableTextRange(
4889
+ value,
4890
+ event.editor.selection,
4891
+ context.schema
4892
+ );
4893
+ if (!selection) {
4894
+ console.warn(
4895
+ `Unable to handle event ${event.type} due to missing selection`
4896
+ );
4897
+ return;
4898
+ }
4899
+ const behaviorContext = {
4900
+ schema: context.schema,
4901
+ value,
4902
+ selection
4903
+ };
4904
+ for (const eventBehavior of eventBehaviors) {
4905
+ const shouldRun = eventBehavior.guard?.({
4906
+ context: behaviorContext,
4907
+ event
4908
+ }) ?? !0;
4909
+ if (!shouldRun)
4910
+ continue;
4911
+ const actions = eventBehavior.actions.map(
4912
+ (action) => action({ context: behaviorContext, event }, shouldRun)
4913
+ );
4914
+ for (const action of actions)
4915
+ typeof action == "object" && enqueue.raise({
4916
+ ...action,
4917
+ editor: event.editor
4918
+ });
4919
+ }
4826
4920
  })
4827
4921
  },
4828
4922
  actors: {
@@ -4831,8 +4925,10 @@ const networkLogic = xstate.fromCallback(({ sendBack }) => {
4831
4925
  }).createMachine({
4832
4926
  id: "editor",
4833
4927
  context: ({ input }) => ({
4928
+ behaviors: input.behaviors,
4834
4929
  keyGenerator: input.keyGenerator,
4835
- pendingEvents: []
4930
+ pendingEvents: [],
4931
+ schema: input.schema
4836
4932
  }),
4837
4933
  invoke: {
4838
4934
  id: "networkLogic",
@@ -4850,7 +4946,17 @@ const networkLogic = xstate.fromCallback(({ sendBack }) => {
4850
4946
  online: { actions: xstate.emit({ type: "online" }) },
4851
4947
  offline: { actions: xstate.emit({ type: "offline" }) },
4852
4948
  loading: { actions: xstate.emit({ type: "loading" }) },
4853
- "done loading": { actions: xstate.emit({ type: "done loading" }) }
4949
+ "done loading": { actions: xstate.emit({ type: "done loading" }) },
4950
+ "update schema": { actions: "assign schema" },
4951
+ "key down": {
4952
+ actions: ["handle behavior event"]
4953
+ },
4954
+ "insert text": {
4955
+ actions: ["apply:insert text"]
4956
+ },
4957
+ "insert text block": {
4958
+ actions: ["apply:insert text block"]
4959
+ }
4854
4960
  },
4855
4961
  initial: "pristine",
4856
4962
  states: {
@@ -4945,18 +5051,23 @@ class PortableTextEditor extends react.Component {
4945
5051
  throw new Error('PortableTextEditor: missing "schemaType" property');
4946
5052
  props.incomingPatches$ && console.warn(
4947
5053
  "The prop 'incomingPatches$' is deprecated and renamed to 'patches$'"
5054
+ ), this.schemaTypes = getPortableTextMemberSchemaTypes(
5055
+ props.schemaType.hasOwnProperty("jsonType") ? props.schemaType : compileType(props.schemaType)
4948
5056
  ), this.editorActor = xstate.createActor(editorMachine, {
4949
5057
  input: {
4950
- keyGenerator: props.keyGenerator || defaultKeyGenerator
5058
+ behaviors: coreBehaviors,
5059
+ keyGenerator: props.keyGenerator || defaultKeyGenerator,
5060
+ schema: this.schemaTypes
4951
5061
  }
4952
- }), this.editorActor.start(), this.schemaTypes = getPortableTextMemberSchemaTypes(
4953
- props.schemaType.hasOwnProperty("jsonType") ? props.schemaType : compileType(props.schemaType)
4954
- );
5062
+ }), this.editorActor.start();
4955
5063
  }
4956
5064
  componentDidUpdate(prevProps) {
4957
5065
  this.props.schemaType !== prevProps.schemaType && (this.schemaTypes = getPortableTextMemberSchemaTypes(
4958
5066
  this.props.schemaType.hasOwnProperty("jsonType") ? this.props.schemaType : compileType(this.props.schemaType)
4959
- )), this.props.editorRef !== prevProps.editorRef && this.props.editorRef && (this.props.editorRef.current = this);
5067
+ ), this.editorActor.send({
5068
+ type: "update schema",
5069
+ schema: this.schemaTypes
5070
+ })), this.props.editorRef !== prevProps.editorRef && this.props.editorRef && (this.props.editorRef.current = this);
4960
5071
  }
4961
5072
  setEditable = (editable) => {
4962
5073
  this.editable = { ...this.editable, ...editable };
@@ -5513,7 +5624,11 @@ const debug$1 = debugWithName("components:Leaf"), EMPTY_MARKS = [], Leaf = (prop
5513
5624
  }, [validateSelection, editableElement]);
5514
5625
  const handleKeyDown = react.useCallback(
5515
5626
  (event) => {
5516
- props.onKeyDown && props.onKeyDown(event), event.isDefaultPrevented() || slateEditor.pteWithHotKeys(event);
5627
+ props.onKeyDown && props.onKeyDown(event), event.isDefaultPrevented() || (editorActor.send({
5628
+ type: "key down",
5629
+ nativeEvent: event.nativeEvent,
5630
+ editor: slateEditor
5631
+ }), slateEditor.pteWithHotKeys(event));
5517
5632
  },
5518
5633
  [props, slateEditor]
5519
5634
  ), scrollSelectionIntoViewToSlate = react.useMemo(() => {