question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Pattern for nested block components with async data

See original GitHub issue

Hi @ianstormtaylor! This is not as much an issue as a question around recommended pattern.

Problem:

TL;DR: So, l have a use-case of a link, which when entered turns into an embed with the following nested components which are also nested (hence can’t use isVoid) . The data for those nested components is fetched in an async call.

<LinkEmbed>
  <EditableTitle />
  <EditableDescription />
</LinkEmbed>

So, the way I was set up originally was having a component for <LinkEmbed> which would accept a url prop. And in componentDidMount, it would make an XHR call to pre-fill the title and description. Once the call returns, it would pass the title and children to its respective components.


Possible Solutions:

In Slate world, when I detect a url, I see two ways, second of which is not possible today.

  1. setBlock to LinkEmbed, and insertBlock the EditableTitle and EditableDescription as well. The possible problem here would be to insert the result of the XHR call back in the components easily? I could theoretically pass the key of the component and onSuccess of the XHR, update the data attribute of the block.

  2. Just setBlock to LinkEmbed. Within the LinkEmbed, be able to spread the relevant properties to each of the children.

<LinkEmbed {...attributes}>
  <EditableTitle {...children.get('EditableTitle')} />
  <EditableDescription {...children.get('EditableDescription')} />
</LinkEmbed>

The problem in the second solution is that there is no way to specify what block types should always comprise LinkEmbed.

So, after this long story, I guess I am wondering if you have some guidance or pattern that you’d recommend or has worked for you in such cases. Is there a third way possible that I missed?

Thank you!

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Comments:6 (6 by maintainers)

github_iconTop GitHub Comments

5reactions
ianstormtaylorcommented, Mar 3, 2017

Hey @oyeanuj I’m not totally sure since I haven’t done something like this before, but it seems like there are a few different considerations…

Schema

I’d probably use schema.rules to perform normalizations so that you ensure that a LinkEmbed block always contains a EditableTitle and an EditableDescription. (I do something similar with my own Figure, Image, and FigureCaption blocks.)

Here’s an internal validation plugin I wrote for my own use case (probably has bugs) to give you a sense for what that might look like:

  const validations = {
    schema: {
      rules: [
        {
          match: (object) => {
            return (
              (object.kind == 'block' || object.kind == 'document') &&
              (object.type != FIGURE_BLOCK && object.type != EMBED_BLOCK && object.type != IMAGE_BLOCK)
            )
          },
          validate: (block) => {
            const invalids = block.nodes.filter((n) => {
              return n.type == EMBED_BLOCK || n.type == IMAGE_BLOCK
            })
            return invalids.size ? invalids : null
          },
          normalize: (transform, block, invalids) => {
            invalids.forEach(n => transform.wrapBlockByKey(n.key, FIGURE_BLOCK))
          }
        },
        {
          match: (object) => {
            return object.kind == 'block' && object.type != FIGURE_BLOCK
          },
          validate: (block) => {
            const invalids = block.nodes.filter((n) => n.type == FIGURE_CAPTION_BLOCK)
            return invalids.size ? invalids : null
          },
          normalize: (transform, block, invalids) => {
            invalids.forEach(n => transform.setNodeByKey(n.key, DEFAULT_BLOCK))
          }
        },
        {
          match: isFigureCaptionBlock,
          validate: (block) => {
            const invalids = block.nodes.filter(n => n.kind == 'block')
            return invalids.size ? invalids : null
          },
          normalize: (transform, block, invalids) => {
            invalids.forEach(n => transform.removeNodeByKey(n.key))
          }
        },
        {
          match: isFigureBlock,
          validate: (block) => {
            const first = block.nodes.first()
            if (first.kind != 'block') return true
            if (first.type != EMBED_BLOCK && first.type != IMAGE_BLOCK) return true
            return false
          },
          normalize: (transform, block) => {
            transform.removeNodeByKey(block.key)
          }
        },
        {
          match: isFigureBlock,
          validate: (block) => {
            const invalids = block.nodes.filter((n) => {
              return (
                n.type != FIGURE_CAPTION_BLOCK &&
                n.type != EMBED_BLOCK &&
                n.type != IMAGE_BLOCK
              )
            })

            return invalids.size ? invalids : null
          },
          normalize: (transform, block, invalids) => {
            invalids.forEach(n => transform.unwrapBlockByKey(n.key))
          }
        }
      ]
    }
  }

Emitters/Signals

It sounds like you could actually use event emitters to handle the loading case, such that when a link loads it emits an event, all of the associated blocks will setState when they receive that event. (They would bind and unbind in componentDidMount and componentWillUnmount I believe.)

I’ve been experimenting with using signals in a few of my components so far and it has been nice in terms of not having to worry about everything being handled in a single location.

That said, I also use the “fetch on construct” pattern too.

Here’s an example of a component that emits on hover, and then another that listens and does stuff:

  const Hovering = new Signal()
  const Inserting = new Signal()
  const Inspecting = new Signal()

  class LinkInline extends React.Component {

    onMouseEnter = () => {
      const { node, editor } = this.props
      const state = editor.getState()
      if (state.isExpanded) return
      Hovering.dispatch(node)
    }

    onMouseLeave = () => {
      Hovering.dispatch(null)
    }

    render = () => {
      const { attributes, children, node } = this.props
      const href = node.data.get(LINK_INLINE_URL_KEY)
      return (
        <a
          {...attributes}
          href={href}
          target="_blank"
          rel="noreferrer nofollow noopener"
          onMouseEnter={this.onMouseEnter}
          onMouseLeave={this.onMouseLeave}
        >
          {children}
        </a>
      )
    }

  }

  class LinksInspectMenu extends React.Component {

    static propTypes = {
      editor: React.PropTypes.object.isRequired,
      state: React.PropTypes.object.isRequired,
    }

    state = {
      url: '',
      node: null,
      hovering: false,
    }

    componentWillMount = () => {
      Hovering.add(this.onHovering)
      Inspecting.add(this.onInspecting)
    }

    componentWillUnmount = () => {
      Hovering.remove(this.onHovering)
      Inspecting.remove(this.onInspecting)
    }

    onMouseEnter = () => {
      this.setState({ hovering: true })
    }

    onMouseLeave = () => {
      this.close()
    }

    onHovering = debounce((node) => {
      if (!node && this.state.hovering) return
      Inspecting.dispatch(node)
    }, INSPECT_DELAY)

    onInspecting = (node) => {
      if (!this.state.node && node) {
        const url = node.data.get(LINK_INLINE_URL_KEY)
        this.setState({ url })
      }

      this.setState({ node })
    }

    onSubmit = (e) => {
      const { editor, state } = this.props
      const { node, url } = this.state
      const next = state
        .transform()
        .call(setLinkInlineByKey, node.key, url)
        .collapseToEndOf(node)
        .apply()
      e.preventDefault()
      this.close()
      editor.onChange(next)
    }

    onDelete = (e) => {
      const { editor, state } = this.props
      const { node } = this.state
      const next = state
        .transform()
        .unwrapInlineByKey(node.key)
        .apply()
      e.preventDefault()
      this.close()
      editor.onChange(next)
    }

    onCancel = (e) => {
      this.close()
    }

    close = () => {
      Inspecting.dispatch(null)
    }

    render = () => {
      const { url, node } = this.state
      return node && (
        <SlateNodePortal
          node={node}
          portalAnchor="bottom center"
          nodeAnchor="top center"
        >
          <InspectMenu
            onMouseEnter={this.onMouseEnter}
            onMouseLeave={this.onMouseLeave}
          >
            <InspectMenuInput
              placeholder="Enter a new URL…"
              type="url"
              defaultValue={url}
              onChange={e => this.setState({ url: e.target.value })}
              onBlur={this.onCancel}
              onEscape={this.onCancel}
              onEnter={this.onSubmit}
            />
            <MenuLeftButton empty primary>
              <Link
                to={url}
                target="_blank"
                rel="noreferrer nofollow noopener"
              >
                <Icon>launch</Icon>
              </Link>
            </MenuLeftButton>
            <MenuRightButton flushLeft empty danger onClick={this.onDelete}>
              <Icon>close</Icon>
            </MenuRightButton>
          </InspectMenu>
        </SlateNodePortal>
      )
    }

  }
0reactions
ianstormtaylorcommented, Mar 19, 2017

Yup, that sounds right!

Addressing it in the docs somewhere sounds good to me if you find a good place.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Hierarchy of React Async Components and Fetching Data
In this guide, we are going to see some React Async component hierarchy structures and learn how to fetch async data.
Read more >
How To Handle Async Data Loading, Lazy Loading, and Code ...
In future versions of React, you'll be able to use Suspense to load data in nested components without render blocking.
Read more >
how can I 'de-uglify' these nested async parts, in F# - Stack ...
The issue here is that the whole function is expected to be async, but getAsync is recursive, so it has its own async...
Read more >
Async/Await - Best Practices in Asynchronous Programming
In particular, it's usually a bad idea to block on async code by calling Task. ... this includes any code that manipulates GUI...
Read more >
Angular Reactive Templates with ngIf and the Async Pipe
Advantages of this more reactive approach · There are no manual subscriptions at the component level for observables coming out of the service ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found